diff --git a/.env.example b/.env.example index f85269e..124c479 100644 --- a/.env.example +++ b/.env.example @@ -1,29 +1,57 @@ -# App +# ------------------------------ +# Application +# ------------------------------ +# Display name used in API docs/UI. APP_NAME=NexaPG Monitor +# Runtime environment: dev | staging | prod | test ENVIRONMENT=dev +# Backend log level: DEBUG | INFO | WARNING | ERROR LOG_LEVEL=INFO -# Core DB +# ------------------------------ +# Core Database (internal metadata DB) +# ------------------------------ +# Database that stores users, targets, metrics, query stats, and audit logs. DB_NAME=nexapg DB_USER=nexapg DB_PASSWORD=nexapg +# Host port mapped to the internal PostgreSQL container port 5432. DB_PORT=5433 -# Backend +# ------------------------------ +# Backend API +# ------------------------------ +# Host port mapped to backend container port 8000. BACKEND_PORT=8000 +# JWT signing secret. Change this in every non-local environment. JWT_SECRET_KEY=change_this_super_secret JWT_ALGORITHM=HS256 +# Access token lifetime in minutes. JWT_ACCESS_TOKEN_MINUTES=15 +# Refresh token lifetime in minutes (10080 = 7 days). JWT_REFRESH_TOKEN_MINUTES=10080 -# Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" +# Key used to encrypt monitored target passwords at rest. +# Generate with: +# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" ENCRYPTION_KEY=REPLACE_WITH_FERNET_KEY -# Dev: set to * to allow all origins (credentials disabled automatically) +# Allowed CORS origins for browser clients. +# Use comma-separated values, e.g.: +# CORS_ORIGINS=http://localhost:5173,https://nexapg.example.com +# Dev-only shortcut: +# CORS_ORIGINS=* CORS_ORIGINS=http://localhost:5173,http://localhost:8080 +# Target polling interval in seconds. POLL_INTERVAL_SECONDS=30 +# Initial admin bootstrap user (created on first startup if not present). INIT_ADMIN_EMAIL=admin@example.com INIT_ADMIN_PASSWORD=ChangeMe123! +# ------------------------------ # Frontend +# ------------------------------ +# Host port mapped to frontend container port 80. FRONTEND_PORT=5173 -# For reverse proxy + SSL prefer relative path to avoid mixed-content. +# Base API URL used at frontend build time. +# For reverse proxy + SSL, keep this relative to avoid mixed-content issues. +# Example direct mode: VITE_API_URL=http://localhost:8000/api/v1 VITE_API_URL=/api/v1 diff --git a/README.md b/README.md index f65259e..aeb4fb4 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,176 @@ # NexaPG - PostgreSQL Monitoring Stack -Docker-basierte Monitoring-Loesung fuer mehrere PostgreSQL-Targets mit FastAPI + React. +NexaPG is a Docker-based PostgreSQL monitoring platform for multiple remote targets, built with FastAPI + React. -## Features +## What it includes -- Multi-target PostgreSQL Monitoring (remote) -- Polling Collector fuer: +- Multi-target PostgreSQL monitoring (remote instances) +- Polling collector for: - `pg_stat_database` - `pg_stat_activity` - `pg_stat_bgwriter` - `pg_locks` - - `pg_stat_statements` (falls auf Target aktiviert) -- Core-DB fuer: - - User/Auth/RBAC (`admin`, `operator`, `viewer`) - - Targets (Credentials verschluesselt via Fernet) - - Metrics / Query Stats - - Audit Logs -- Auth mit JWT Access/Refresh Tokens -- FastAPI + SQLAlchemy async + Alembic -- React (Vite) Frontend mit: - - Login/Logout - - Dashboard - - Target Detail mit Charts - - Query Insights - - Admin User Management -- Health Endpoints: + - `pg_stat_statements` (if enabled on target) +- Core metadata database for: + - Authentication and RBAC (`admin`, `operator`, `viewer`) + - Monitored target configuration (encrypted credentials) + - Metrics and query stats + - Audit logs +- JWT auth (access + refresh) +- FastAPI + SQLAlchemy async + Alembic migrations +- React (Vite) frontend with: + - Login/logout + - Dashboard overview + - Target detail with charts and database overview + - Query insights + - Admin user management +- Health endpoints: - `/api/v1/healthz` - `/api/v1/readyz` -## Struktur +## Repository structure -- `backend/` FastAPI App -- `frontend/` React (Vite) App -- `ops/` Scripts -- `docker-compose.yml` Stack -- `.env.example` Konfigurationsvorlage +- `backend/` FastAPI application +- `frontend/` React (Vite) application +- `ops/` helper scripts and env template copy +- `docker-compose.yml` full stack definition +- `.env.example` environment template -## Schnellstart +## Prerequisites -1. Env-Datei erstellen: +Install these before starting: + +- Docker Engine 24+ +- Docker Compose v2+ +- GNU Make (optional, for `make up/down/logs/migrate`) +- Open ports on your host: + - frontend: `5173` (default) + - backend: `8000` (or your `BACKEND_PORT`) + - core DB: `5433` (or your `DB_PORT`) + +Optional but recommended: + +- `psql` client for troubleshooting target connectivity + +## Quick start + +1. Create local env file: ```bash cp .env.example .env ``` -2. Fernet Key setzen: +2. Generate an encryption key and set `ENCRYPTION_KEY` in `.env`: ```bash python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" ``` -Wert in `.env` bei `ENCRYPTION_KEY` eintragen. - -3. Stack starten: +3. Start services: ```bash make up ``` -4. URLs: +4. Open: - Frontend: `http://localhost:5173` - Backend API: `http://localhost:8000/api/v1` -- OpenAPI: `http://localhost:8000/docs` +- OpenAPI docs: `http://localhost:8000/docs` + +Default initial admin (from `.env`): -Default Admin (aus `.env`): - Email: `admin@example.com` -- Passwort: `ChangeMe123!` +- Password: `ChangeMe123!` -## Commands +## Common commands ```bash -make up -make down -make logs -make migrate +make up # build + start all services +make down # stop all services +make logs # follow logs +make migrate # run Alembic migrations in backend container ``` -## API (Minimum) +## Environment variables reference -- `POST /api/v1/auth/login` -- `POST /api/v1/auth/refresh` -- `POST /api/v1/auth/logout` -- `GET /api/v1/me` -- CRUD: `GET/POST/PUT/DELETE /api/v1/targets` -- `GET /api/v1/targets/{id}/metrics?from=&to=&metric=` -- `GET /api/v1/targets/{id}/locks` -- `GET /api/v1/targets/{id}/activity` -- `GET /api/v1/targets/{id}/top-queries` -- Admin-only CRUD users: +All variables are defined in `.env.example`. + +### Application + +- `APP_NAME`: Display name used by backend/docs +- `ENVIRONMENT`: `dev | staging | prod | test` +- `LOG_LEVEL`: `DEBUG | INFO | WARNING | ERROR` + +### Core database (internal) + +- `DB_NAME`: Internal metadata DB name +- `DB_USER`: Internal metadata DB user +- `DB_PASSWORD`: Internal metadata DB password +- `DB_PORT`: Host port mapped to internal PostgreSQL `5432` + +### Backend API + +- `BACKEND_PORT`: Host port mapped to backend container `8000` +- `JWT_SECRET_KEY`: JWT signing key (must be changed) +- `JWT_ALGORITHM`: JWT algorithm (default `HS256`) +- `JWT_ACCESS_TOKEN_MINUTES`: access token lifetime +- `JWT_REFRESH_TOKEN_MINUTES`: refresh token lifetime +- `ENCRYPTION_KEY`: Fernet key for encrypting target passwords at rest +- `CORS_ORIGINS`: comma-separated allowed origins or `*` (dev-only) +- `POLL_INTERVAL_SECONDS`: collector polling interval +- `INIT_ADMIN_EMAIL`: bootstrap admin email +- `INIT_ADMIN_PASSWORD`: bootstrap admin password + +### Frontend + +- `FRONTEND_PORT`: Host port mapped to frontend container `80` +- `VITE_API_URL`: API base URL baked into frontend build + - Proxy/SSL setup: use `/api/v1` + - Direct local setup: use `http://localhost:8000/api/v1` + +## API overview (minimum) + +- Auth: + - `POST /api/v1/auth/login` + - `POST /api/v1/auth/refresh` + - `POST /api/v1/auth/logout` + - `GET /api/v1/me` +- Targets: + - `GET/POST /api/v1/targets` + - `GET/PUT/DELETE /api/v1/targets/{id}` + - `GET /api/v1/targets/{id}/metrics?from=&to=&metric=` + - `GET /api/v1/targets/{id}/locks` + - `GET /api/v1/targets/{id}/activity` + - `GET /api/v1/targets/{id}/top-queries` + - `GET /api/v1/targets/{id}/overview` +- Admin users (admin-only): - `GET /api/v1/admin/users` - `POST /api/v1/admin/users` - `PUT /api/v1/admin/users/{user_id}` - `DELETE /api/v1/admin/users/{user_id}` -## Security Notes +## Security notes -- Keine Secrets hardcoded -- Passwoerter als Argon2 Hash -- Target-Credentials verschluesselt (Fernet) -- CORS via Env steuerbar -- Audit Logs fuer Login / Logout / Target- und User-Aenderungen -- Rate limiting: Platzhalter (kann spaeter middleware-basiert ergaenzt werden) +- No secrets are hardcoded in source +- Passwords are hashed with Argon2 +- Target credentials are encrypted with Fernet +- CORS is environment-configurable +- Audit logs include auth, target, and user management events +- Rate limiting is currently a placeholder for future middleware integration -## Wichtiger Hinweis zu `pg_stat_statements` +## Important: `pg_stat_statements` -Auf jedem monitored Target muss `pg_stat_statements` aktiviert sein, sonst bleiben Query Insights leer. -Beispiel: +Query Insights requires `pg_stat_statements` on each monitored target. ```sql CREATE EXTENSION IF NOT EXISTS pg_stat_statements; ``` + +## Reverse proxy and SSL + +For production-like deployments behind HTTPS: + +- Set frontend API to relative path: `VITE_API_URL=/api/v1` +- Route `/api/` from proxy to backend service +- Keep frontend and API on the same public origin to avoid CORS/mixed-content problems diff --git a/frontend/src/pages/AdminUsersPage.jsx b/frontend/src/pages/AdminUsersPage.jsx index 81e64ec..1619075 100644 --- a/frontend/src/pages/AdminUsersPage.jsx +++ b/frontend/src/pages/AdminUsersPage.jsx @@ -16,7 +16,7 @@ export function AdminUsersPage() { if (me?.role === "admin") load().catch((e) => setError(String(e.message || e))); }, [me]); - if (me?.role !== "admin") return
Nur fuer Admin.
; + if (me?.role !== "admin") return
Admins only.
; const create = async (e) => { e.preventDefault(); @@ -47,7 +47,7 @@ export function AdminUsersPage() { setForm({ ...form, password: e.target.value })} /> - +
diff --git a/frontend/src/pages/DashboardPage.jsx b/frontend/src/pages/DashboardPage.jsx index 461ecbb..5d0fb04 100644 --- a/frontend/src/pages/DashboardPage.jsx +++ b/frontend/src/pages/DashboardPage.jsx @@ -26,7 +26,7 @@ export function DashboardPage() { }; }, [tokens, refresh]); - if (loading) return
Lade Dashboard...
; + if (loading) return
Loading dashboard...
; if (error) return
{error}
; return ( @@ -54,7 +54,7 @@ export function DashboardPage() { - + diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx index afaf67b..4e2e0ed 100644 --- a/frontend/src/pages/LoginPage.jsx +++ b/frontend/src/pages/LoginPage.jsx @@ -18,7 +18,7 @@ export function LoginPage() { await login(email, password); navigate("/"); } catch { - setError("Login fehlgeschlagen"); + setError("Login failed"); } finally { setLoading(false); } @@ -28,12 +28,12 @@ export function LoginPage() {
NexaPG Monitor
-

Willkommen zurück

-

Melde dich an, um Monitoring und Query Insights zu öffnen.

+

Welcome back

+

Sign in to access monitoring and query insights.

setEmail(e.target.value)} autoComplete="username" @@ -49,7 +49,7 @@ export function LoginPage() { />
{error &&

{error}

} - +
); diff --git a/frontend/src/pages/QueryInsightsPage.jsx b/frontend/src/pages/QueryInsightsPage.jsx index 1752a6c..7bc8760 100644 --- a/frontend/src/pages/QueryInsightsPage.jsx +++ b/frontend/src/pages/QueryInsightsPage.jsx @@ -36,7 +36,7 @@ export function QueryInsightsPage() { return (

Query Insights

-

Hinweis: Benötigt aktivierte Extension pg_stat_statements auf dem Zielsystem.

+

Note: This section requires the pg_stat_statements extension on the monitored target.

{error &&
{error}
}
diff --git a/frontend/src/pages/TargetDetailPage.jsx b/frontend/src/pages/TargetDetailPage.jsx index f6f7983..49dc8bc 100644 --- a/frontend/src/pages/TargetDetailPage.jsx +++ b/frontend/src/pages/TargetDetailPage.jsx @@ -135,7 +135,7 @@ export function TargetDetailPage() { [series] ); - if (loading) return
Lade Target Detail...
; + if (loading) return
Loading target detail...
; if (error) return
{error}
; const role = overview?.instance?.role || "-"; @@ -157,7 +157,7 @@ export function TargetDetailPage() { Role {role}
-
+
Uptime{formatSeconds(overview.instance.uptime_seconds)}
Database{overview.instance.current_database || "-"}
@@ -165,16 +165,16 @@ export function TargetDetailPage() { Target Port {targetMeta?.port ?? "-"}
-
+
Current DB Size{formatBytes(overview.storage.current_database_size_bytes)}
-
+
WAL Size{formatBytes(overview.storage.wal_directory_size_bytes)}
-
+
Free Disk{formatBytes(overview.storage.disk_space.free_bytes)}
-
+
Replay Lag 5 ? "lag-bad" : ""}> {formatSeconds(overview.replication.replay_lag_seconds)} @@ -262,7 +262,7 @@ export function TargetDetailPage() { ))}
-

Connections / TPS approx / Cache hit ratio

+

Connections / TPS (approx) / Cache Hit Ratio

diff --git a/frontend/src/pages/TargetsPage.jsx b/frontend/src/pages/TargetsPage.jsx index 324a793..15890aa 100644 --- a/frontend/src/pages/TargetsPage.jsx +++ b/frontend/src/pages/TargetsPage.jsx @@ -51,7 +51,7 @@ export function TargetsPage() { }; const deleteTarget = async (id) => { - if (!confirm("Target loeschen?")) return; + if (!confirm("Delete target?")) return; try { await apiFetch(`/targets/${id}`, { method: "DELETE" }, tokens, refresh); await load(); @@ -69,42 +69,42 @@ export function TargetsPage() {
-

Neues Target

-

Verbindungsdaten fuer eine PostgreSQL-Instanz.

+

New Target

+

Connection settings for a PostgreSQL instance.

- +
- setForm({ ...form, name: e.target.value })} required /> - Eindeutiger Anzeigename im Dashboard. + setForm({ ...form, name: e.target.value })} required /> + Unique display name in the dashboard.
- setForm({ ...form, host: e.target.value })} required /> - Wichtig: Muss vom Backend-Container aus erreichbar sein. + setForm({ ...form, host: e.target.value })} required /> + Must be reachable from the backend container.
setForm({ ...form, port: Number(e.target.value) })} type="number" required /> - Standard PostgreSQL Port ist 5432 (oder gemappter Host-Port). + Default PostgreSQL port is 5432 (or your mapped host port).
- setForm({ ...form, dbname: e.target.value })} required /> - Name der Datenbank, die ueberwacht werden soll. + setForm({ ...form, dbname: e.target.value })} required /> + Database name to monitor.
- setForm({ ...form, username: e.target.value })} required /> - DB User mit Leserechten auf Stats-Views. + setForm({ ...form, username: e.target.value })} required /> + DB user with read permissions on stats views.
- setForm({ ...form, password: e.target.value })} required /> - Wird verschluesselt in der Core-DB gespeichert. + setForm({ ...form, password: e.target.value })} required /> + Stored encrypted in the core database.
@@ -114,12 +114,12 @@ export function TargetsPage() { - Bei Fehler "rejected SSL upgrade" auf disable stellen. + If you see "rejected SSL upgrade", switch to disable.
- +
@@ -130,25 +130,25 @@ export function TargetsPage() {

Troubleshooting

-

Typische Verbindungsfehler schnell erkennen.

+

Quick checks for the most common connection issues.

- +

- Connection refused: Host/Port falsch oder DB nicht erreichbar. + Connection refused: host/port is wrong or database is unreachable.

- rejected SSL upgrade: SSL Mode auf disable setzen. + rejected SSL upgrade: set SSL mode to disable.

- localhost im Target zeigt aus Backend-Container-Sicht auf den Container selbst. + localhost points to the backend container itself, not your host machine.

)}
{loading ? ( -

Lade Targets...

+

Loading targets...

) : (
Name Host DBAktionAction
@@ -156,7 +156,7 @@ export function TargetsPage() { - + diff --git a/ops/.env.example b/ops/.env.example index 4d6446c..124c479 100644 --- a/ops/.env.example +++ b/ops/.env.example @@ -1,27 +1,57 @@ -# App +# ------------------------------ +# Application +# ------------------------------ +# Display name used in API docs/UI. APP_NAME=NexaPG Monitor +# Runtime environment: dev | staging | prod | test ENVIRONMENT=dev +# Backend log level: DEBUG | INFO | WARNING | ERROR LOG_LEVEL=INFO -# Core DB +# ------------------------------ +# Core Database (internal metadata DB) +# ------------------------------ +# Database that stores users, targets, metrics, query stats, and audit logs. DB_NAME=nexapg DB_USER=nexapg DB_PASSWORD=nexapg +# Host port mapped to the internal PostgreSQL container port 5432. DB_PORT=5433 -# Backend +# ------------------------------ +# Backend API +# ------------------------------ +# Host port mapped to backend container port 8000. BACKEND_PORT=8000 +# JWT signing secret. Change this in every non-local environment. JWT_SECRET_KEY=change_this_super_secret JWT_ALGORITHM=HS256 +# Access token lifetime in minutes. JWT_ACCESS_TOKEN_MINUTES=15 +# Refresh token lifetime in minutes (10080 = 7 days). JWT_REFRESH_TOKEN_MINUTES=10080 -# Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" +# Key used to encrypt monitored target passwords at rest. +# Generate with: +# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" ENCRYPTION_KEY=REPLACE_WITH_FERNET_KEY +# Allowed CORS origins for browser clients. +# Use comma-separated values, e.g.: +# CORS_ORIGINS=http://localhost:5173,https://nexapg.example.com +# Dev-only shortcut: +# CORS_ORIGINS=* CORS_ORIGINS=http://localhost:5173,http://localhost:8080 +# Target polling interval in seconds. POLL_INTERVAL_SECONDS=30 +# Initial admin bootstrap user (created on first startup if not present). INIT_ADMIN_EMAIL=admin@example.com INIT_ADMIN_PASSWORD=ChangeMe123! +# ------------------------------ # Frontend +# ------------------------------ +# Host port mapped to frontend container port 80. FRONTEND_PORT=5173 -VITE_API_URL=http://localhost:8000/api/v1 +# Base API URL used at frontend build time. +# For reverse proxy + SSL, keep this relative to avoid mixed-content issues. +# Example direct mode: VITE_API_URL=http://localhost:8000/api/v1 +VITE_API_URL=/api/v1
Name Host DBAktionenActions