Database Monitoring: On-Prem (K8s & EC2)
Prometheus + Grafana stack for MySQL, PostgreSQL, Redis, MongoDB running on-prem or EC2/self-managed K8s.
Architecture
graph TD
classDef blue fill:#3498db,stroke:#2980b9,color:#fff
classDef orange fill:#e67e22,stroke:#d35400,color:#fff
classDef green fill:#2ecc71,stroke:#27ae60,color:#fff
classDef teal fill:#1abc9c,stroke:#16a085,color:#fff
classDef red fill:#e74c3c,stroke:#c0392b,color:#fff
subgraph DBLayer["Database Layer (K8s pods or EC2 VMs)"]
PG["PostgreSQL"]:::blue
MY["MySQL"]:::blue
RD["Redis"]:::blue
MG["MongoDB"]:::blue
end
subgraph Exporters["Exporters (sidecar or separate pod/process)"]
PGE["postgres_exporter<br/>:9187"]:::teal
MYE["mysqld_exporter<br/>:9104"]:::teal
RDE["redis_exporter<br/>:9121"]:::teal
MGE["mongodb_exporter<br/>:9216"]:::teal
end
subgraph Observability["Observability Stack"]
PROM["Prometheus<br/>scrapes /metrics every 15s"]:::orange
ALERT["Alertmanager<br/>routes alerts"]:::red
GRAF["Grafana<br/>dashboards"]:::green
LOKI["Loki (optional)<br/>log aggregation"]:::orange
end
NOTIF["PagerDuty / Slack / Email"]:::red
PG -->|queries| PGE
MY -->|queries| MYE
RD -->|queries| RDE
MG -->|queries| MGE
PGE & MYE & RDE & MGE -->|scrape /metrics| PROM
PROM --> ALERT
PROM --> GRAF
ALERT --> NOTIF
Exporters are the bridge — they query the DB using native protocol and expose Prometheus /metrics endpoint.
Setup on Kubernetes
Prometheus + Grafana via kube-prometheus-stack
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
--namespace monitoring --create-namespace \
--set grafana.adminPassword=changeme \
--set prometheus.prometheusSpec.retention=30d \
--set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.storageClassName=gp3 \
--set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage=50Gi
This installs: Prometheus, Grafana, Alertmanager, node-exporter DaemonSet, kube-state-metrics.
PostgreSQL Exporter
# postgres-exporter deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres-exporter
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app: postgres-exporter
template:
metadata:
labels:
app: postgres-exporter
annotations:
prometheus.io/scrape: "true" # auto-discovery
prometheus.io/port: "9187"
spec:
containers:
- name: exporter
image: prometheuscommunity/postgres-exporter:v0.15.0
env:
- name: DATA_SOURCE_NAME
valueFrom:
secretKeyRef:
name: postgres-exporter-secret
key: dsn # postgresql://monitor_user:pass@postgres-svc:5432/mydb?sslmode=disable
ports:
- containerPort: 9187
---
# Read-only monitoring user (least privilege)
# CREATE USER monitor WITH PASSWORD 'pass';
# GRANT pg_monitor TO monitor; -- PostgreSQL 10+
# ServiceMonitor tells Prometheus to scrape this service
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: postgres-exporter
namespace: monitoring
spec:
selector:
matchLabels:
app: postgres-exporter
endpoints:
- port: metrics
interval: 15s
Redis Exporter
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-exporter
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app: redis-exporter
template:
metadata:
labels:
app: redis-exporter
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9121"
spec:
containers:
- name: exporter
image: oliver006/redis_exporter:v1.55.0
env:
- name: REDIS_ADDR
value: "redis://redis-svc:6379"
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-secret
key: password
ports:
- containerPort: 9121
Setup on EC2 (bare metal / VM)
# Download and run postgres_exporter as a systemd service
wget https://github.com/prometheus-community/postgres_exporter/releases/download/v0.15.0/postgres_exporter-0.15.0.linux-amd64.tar.gz
tar xvf postgres_exporter*.tar.gz
# Create systemd unit
cat > /etc/systemd/system/postgres-exporter.service << EOF
[Unit]
Description=Postgres Exporter
[Service]
Environment="DATA_SOURCE_NAME=postgresql://monitor:pass@localhost:5432/mydb?sslmode=disable"
ExecStart=/usr/local/bin/postgres_exporter
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl enable --now postgres-exporter
# Tell Prometheus to scrape this EC2 instance
# In prometheus.yml on the Prometheus server:
# scrape_configs:
# - job_name: postgres
# static_configs:
# - targets: ['10.0.1.50:9187'] # EC2 private IP
Key Metrics to Monitor
PostgreSQL
| Metric |
What it tells you |
Alert threshold |
pg_up |
Is exporter connected to DB |
= 0 → DB down |
pg_database_size_bytes |
Database disk usage |
> 80% of disk |
pg_stat_activity_count |
Active connections |
> 80% of max_connections |
pg_stat_activity_max_tx_duration |
Longest running transaction |
> 300s → long-running query |
pg_stat_bgwriter_checkpoint_write_time |
Checkpoint I/O time |
Sustained high → I/O bottleneck |
pg_stat_replication_pg_wal_lsn_diff |
Replication lag (bytes) |
> 50MB → replica falling behind |
pg_locks_count |
Lock contention |
Sudden spike → deadlock risk |
pg_stat_user_tables_n_dead_tup |
Dead tuples (bloat) |
High → needs VACUUM |
rate(pg_stat_user_tables_seq_scan[5m]) |
Sequential scans |
High on large tables → missing index |
pg_stat_statements_mean_exec_time_seconds |
Slow query avg time |
Spike → bad query plan / missing index |
MySQL
| Metric |
What it tells you |
Alert threshold |
mysql_up |
DB reachable |
= 0 → alert |
mysql_global_status_threads_connected |
Active connections |
> 80% of max_connections |
mysql_global_status_innodb_buffer_pool_read_requests vs reads |
Buffer pool hit rate |
hit rate < 95% → add RAM |
rate(mysql_global_status_slow_queries[5m]) |
Slow query rate |
> 0 sustained → investigate |
mysql_global_status_innodb_row_lock_waits |
Row lock waits |
Sudden spike → contention |
mysql_slave_status_seconds_behind_master |
Replication lag |
> 30s → alert |
mysql_global_status_aborted_connects |
Failed connections |
Spike → auth issues / network |
Redis
| Metric |
What it tells you |
Alert threshold |
redis_up |
Redis reachable |
= 0 → alert |
redis_memory_used_bytes vs redis_memory_max_bytes |
Memory usage |
> 80% of maxmemory |
redis_connected_clients |
Active connections |
Spike → connection leak |
redis_keyspace_hits_total / (hits+misses) |
Cache hit rate |
< 90% → hot keys missing |
redis_rejected_connections_total |
Rejected (maxclients hit) |
> 0 → raise maxclients |
redis_replication_backlog_first_byte_offset |
Replication lag |
High → replica falling behind |
redis_rdb_last_bgsave_status |
Last RDB snapshot status |
!= ok → snapshot failing |
redis_blocked_clients |
Clients blocked on BLPOP etc |
High → consumers not draining |
rate(redis_commands_processed_total[1m]) |
Ops/sec |
Baseline for anomaly detection |
MongoDB
| Metric |
What it tells you |
Alert threshold |
mongodb_up |
DB reachable |
= 0 → alert |
mongodb_connections_current |
Active connections |
> 80% of maxIncomingConnections |
mongodb_opcounters_total |
Ops/sec (insert/query/update) |
Baseline for anomaly |
mongodb_memory_resident_mb |
RAM used by mongod |
> 80% of system RAM |
mongodb_globalLock_currentQueue_total |
Global lock queue |
> 0 sustained → severe contention |
mongodb_repl_lag |
Replica set replication lag |
> 10s → alert |
mongodb_wiredTiger_cache_bytes_currently_in_cache |
WiredTiger cache usage |
> 80% of cache size |
rate(mongodb_mongod_op_latencies_latency_total[5m]) |
Operation latency |
Spike → slow queries |
Alerting Rules
# prometheus-rules.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: database-alerts
namespace: monitoring
spec:
groups:
- name: postgres
rules:
- alert: PostgresDown
expr: pg_up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "PostgreSQL is down on {{ $labels.instance }}"
- alert: PostgresHighConnections
expr: pg_stat_activity_count / pg_settings_max_connections > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "PostgreSQL connections above 80% ({{ $value | humanizePercentage }})"
- alert: PostgresLongRunningQuery
expr: pg_stat_activity_max_tx_duration > 300
for: 2m
labels:
severity: warning
annotations:
summary: "Query running > 5min on {{ $labels.instance }}"
- alert: PostgresReplicationLag
expr: pg_stat_replication_pg_wal_lsn_diff > 52428800 # 50MB
for: 5m
labels:
severity: warning
annotations:
summary: "Postgres replica lag {{ $value | humanize1024 }}B"
- name: redis
rules:
- alert: RedisDown
expr: redis_up == 0
for: 1m
labels:
severity: critical
- alert: RedisHighMemory
expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "Redis memory above 80% on {{ $labels.instance }}"
- alert: RedisCacheHitRateLow
expr: |
rate(redis_keyspace_hits_total[5m]) /
(rate(redis_keyspace_hits_total[5m]) + rate(redis_keyspace_misses_total[5m])) < 0.9
for: 10m
labels:
severity: warning
annotations:
summary: "Redis hit rate below 90%: {{ $value | humanizePercentage }}"
Grafana Dashboards
Import pre-built dashboards from grafana.com by ID:
| Dashboard |
Grafana ID |
For |
| PostgreSQL Database |
9628 |
postgres_exporter |
| MySQL Overview |
7362 |
mysqld_exporter |
| Redis Dashboard |
11835 |
redis_exporter |
| MongoDB Overview |
2583 |
mongodb_exporter |
| Node Exporter Full |
1860 |
host-level CPU/disk/network |
# Import via Grafana API
curl -X POST http://admin:changeme@localhost:3000/api/dashboards/import \
-H "Content-Type: application/json" \
-d '{"gnetId": 9628, "overwrite": true, "folderId": 0}'
What to Put on the DB Overview Dashboard
Row 1: Health
[ pg_up / redis_up / mysql_up ] [ active connections ] [ replication lag ]
Row 2: Performance
[ queries/sec ] [ avg query latency ] [ slow queries/min ]
Row 3: Resources
[ memory used vs limit ] [ disk used % ] [ CPU % ]
Row 4: Cache / Buffer
[ buffer pool hit rate (MySQL) ] [ Redis hit rate ] [ dead tuples (PG) ]
Row 5: Alerts firing (Alertmanager panel)