You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A production-grade PostgreSQL observability platform built with F# 10 (ASP.NET Core Minimal APIs) and Svelte 4 + Tailwind CSS, served behind a Traefik load balancer.
Features
Feature
Details
Real-time metrics
CPU, disk I/O, cache hit ratio pushed via SignalR WebSocket every 3 seconds
PgMonitor creates its own tables in the monitored database on first startup:
Table
Contents
pgm_users
Application users (username, BCrypt hash, role)
pgm_connections
Additional monitored database connections
pgm_alert_rules
Alert rule definitions
pgm_alerts
Fired alert history (last 500 retained in memory)
API Reference
All endpoints except /api/auth/login require Authorization: Bearer <token>.
Endpoints marked admin additionally require the admin role.
Auth
Method
Path
Description
POST
/api/auth/login
Returns JWT { token, expiresAt }
Users (admin)
Method
Path
Description
GET
/api/users
List all users
POST
/api/users
Create user { username, password, role }
DELETE
/api/users/{id}
Delete user
PATCH
/api/users/{id}/password
Change password { newPassword }
Connections
Method
Path
Description
GET
/api/connections
List connections (passwords redacted)
POST
/api/connections
Add connection (admin)
DELETE
/api/connections/{id}
Remove connection (admin)
POST
/api/connections/test
Test connectivity (admin)
Monitoring
Method
Path
Description
GET
/api/snapshot
CPU + disk + IO + connections
GET
/api/queries/active
Live queries
GET
/api/queries/slow?limit=50
Top slow queries
POST
/api/queries/explain
EXPLAIN ANALYZE { query, database }
POST
/api/sql/run
Run SQL { connectionId?, sql }
GET
/api/replication
Replicas + slots
GET
/api/locks
All locks
GET
/api/vacuum/activity
Active vacuum progress
GET
/api/vacuum/bloat
Table bloat
GET
/healthz
Health check (public)
Alerts
Method
Path
Description
GET
/api/alerts
Fired alerts
GET
/api/alerts/rules
Alert rules
POST
/api/alerts/rules
Create rule (admin)
DELETE
/api/alerts/rules/{id}
Delete rule (admin)
POST
/api/alerts/{id}/acknowledge
Acknowledge alert
SignalR
Endpoint
Description
WS /hubs/monitor?access_token=<jwt>
Real-time feed
Event
Payload
snapshot
SystemSnapshot — CPU, disks, IO, connections
activeQueries
ActiveQuery[]
alerts
Alert[] — newly fired alerts only
Correlation IDs
Every HTTP request is assigned a correlation ID (X-Correlation-Id header). If the caller provides this header it is reused, otherwise a new UUID is generated. The ID is:
Echoed back in the response as X-Correlation-Id
Attached to every Serilog log event as the CorrelationId property
Visible in Seq — use @Properties['CorrelationId'] = '...' to filter all events for a single request
Data Protection
ASP.NET Core Data Protection keys are persisted to /var/lib/pgmonitor/keys (Docker volume dpkeys). Keys are encrypted at rest using a self-signed RSA-2048 X.509 certificate (dp-cert.pfx) stored in the same directory and generated automatically on first startup.
The certificate has a 10-year validity. Do not delete the dpkeys volume — doing so would invalidate all existing encrypted sessions.
Load Balancer
Traefik v3 is used as the ingress and load balancer:
Routes /api/* and /hubs/* → backend (priority 10)
Routes /* → frontend (priority 1)
Sticky sessions via cookie pgm_instance keep each browser's SignalR connection on the same backend instance
Backend health check on /healthz — traffic only routed to healthy instances
-- Required for slow query tracking
ALTER SYSTEM SET shared_preload_libraries ='pg_stat_statements';
ALTER SYSTEM SETpg_stat_statements.track='all';
ALTER SYSTEM SET track_io_timing ='on';
SELECT pg_reload_conf();
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
Production Checklist
Change Jwt__Key to a strong random secret (min 32 chars)
Change default admin / viewer passwords via the Users UI
Set SEQ_FIRSTRUN_ADMINPASSWORD to a strong password
Set POSTGRES_PASSWORD to a strong password
Back up the dpkeys volume (contains encryption certificate)
Put a TLS-terminating reverse proxy (Nginx, Caddy) in front of Traefik for HTTPS
Set Cors__Origins to your actual domain
Restrict port 5445 (Postgres) and 5341 / 8080 (Seq) to internal networks only