Set up comprehensive PostgreSQL monitoring using Prometheus PostgreSQL exporter and Grafana dashboards. Configure performance metrics collection, visualization, and alerting for database optimization and troubleshooting.
Prerequisites
- PostgreSQL 12+ installed and running
- Prometheus server installed
- Grafana installed
- sudo access
What this solves
PostgreSQL monitoring is essential for maintaining database performance, identifying bottlenecks, and preventing downtime in production environments. This tutorial shows you how to implement comprehensive PostgreSQL monitoring using Prometheus to collect metrics and Grafana to visualize performance data with custom dashboards and alerts.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you get the latest versions of monitoring tools.
sudo apt update && sudo apt upgrade -y
Install PostgreSQL exporter
Download and install the PostgreSQL exporter that will collect database metrics for Prometheus. This exporter connects to PostgreSQL and exposes metrics on port 9187.
cd /tmp
wget https://github.com/prometheus-community/postgres_exporter/releases/download/v0.15.0/postgres_exporter-0.15.0.linux-amd64.tar.gz
tar -xzf postgres_exporter-0.15.0.linux-amd64.tar.gz
sudo cp postgres_exporter-0.15.0.linux-amd64/postgres_exporter /usr/local/bin/
sudo chmod +x /usr/local/bin/postgres_exporter
Create PostgreSQL monitoring user
Create a dedicated PostgreSQL user for the exporter with minimal required permissions. This follows the principle of least privilege for security.
sudo -u postgres psql -c "CREATE USER postgres_exporter WITH PASSWORD 'secure_monitoring_password';"
sudo -u postgres psql -c "GRANT CONNECT ON DATABASE postgres TO postgres_exporter;"
sudo -u postgres psql -c "GRANT pg_monitor TO postgres_exporter;"
Configure PostgreSQL exporter environment
Create the environment file that contains the database connection string. The exporter uses this to connect to PostgreSQL and collect metrics.
DATA_SOURCE_NAME="postgresql://postgres_exporter:secure_monitoring_password@localhost:5432/postgres?sslmode=disable"
PG_EXPORTER_WEB_LISTEN_ADDRESS=":9187"
PG_EXPORTER_EXTEND_QUERY_PATH="/etc/postgres_exporter/queries.yaml"
Create systemd service for PostgreSQL exporter
Set up the exporter as a systemd service to ensure it starts automatically and can be managed like other system services.
[Unit]
Description=PostgreSQL Exporter
Wants=network-online.target
After=network-online.target
Requires=postgresql.service
After=postgresql.service
[Service]
Type=simple
User=postgres
Group=postgres
EnvironmentFile=/etc/default/postgres_exporter
ExecStart=/usr/local/bin/postgres_exporter
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Create extended queries configuration
Configure custom queries for detailed PostgreSQL monitoring. This file defines additional metrics beyond the default ones.
sudo mkdir -p /etc/postgres_exporter
sudo chown postgres:postgres /etc/postgres_exporter
pg_database_size:
query: "SELECT pg_database.datname, pg_database_size(pg_database.datname) as size FROM pg_database"
master: true
metrics:
- datname:
usage: "LABEL"
description: "Name of the database"
- size:
usage: "GAUGE"
description: "Disk space used by the database"
pg_slow_queries:
query: "SELECT schemaname, tablename, attname, n_distinct, correlation FROM pg_stats"
master: true
metrics:
- schemaname:
usage: "LABEL"
description: "Schema name"
- tablename:
usage: "LABEL"
description: "Table name"
- attname:
usage: "LABEL"
description: "Attribute name"
- n_distinct:
usage: "GAUGE"
description: "Number of distinct values"
- correlation:
usage: "GAUGE"
description: "Statistical correlation"
pg_table_stats:
query: "SELECT schemaname, tablename, n_tup_ins, n_tup_upd, n_tup_del FROM pg_stat_user_tables"
master: true
metrics:
- schemaname:
usage: "LABEL"
description: "Schema name"
- tablename:
usage: "LABEL"
description: "Table name"
- n_tup_ins:
usage: "COUNTER"
description: "Number of tuples inserted"
- n_tup_upd:
usage: "COUNTER"
description: "Number of tuples updated"
- n_tup_del:
usage: "COUNTER"
description: "Number of tuples deleted"
Set correct file permissions
Ensure the PostgreSQL user can read the configuration files while maintaining security. The postgres user needs read access to the environment file and queries configuration.
sudo chown postgres:postgres /etc/default/postgres_exporter
sudo chmod 640 /etc/default/postgres_exporter
sudo chown postgres:postgres /etc/postgres_exporter/queries.yaml
sudo chmod 644 /etc/postgres_exporter/queries.yaml
Start and enable PostgreSQL exporter
Enable the service to start automatically on boot and start it immediately. This ensures continuous monitoring of your PostgreSQL instance.
sudo systemctl daemon-reload
sudo systemctl enable --now postgres_exporter
sudo systemctl status postgres_exporter
Configure Prometheus to scrape PostgreSQL metrics
Add the PostgreSQL exporter as a target in your Prometheus configuration. This tells Prometheus where to collect PostgreSQL metrics from.
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'postgresql'
static_configs:
- targets: ['localhost:9187']
scrape_interval: 30s
scrape_timeout: 10s
metrics_path: /metrics
Restart Prometheus to apply configuration
Reload Prometheus configuration to start collecting PostgreSQL metrics. The service will now scrape metrics every 30 seconds.
sudo systemctl restart prometheus
sudo systemctl status prometheus
Install Grafana dashboard
Import a pre-built PostgreSQL dashboard into Grafana for immediate visualization. This dashboard provides comprehensive PostgreSQL monitoring out of the box.
curl -X POST \
http://admin:admin@localhost:3000/api/dashboards/import \
-H 'Content-Type: application/json' \
-d '{
"dashboard": {
"id": null,
"title": "PostgreSQL Database Monitoring",
"tags": ["postgresql", "database"],
"timezone": "browser",
"panels": [
{
"id": 1,
"title": "Database Connections",
"type": "stat",
"targets": [
{
"expr": "pg_stat_database_numbackends",
"legendFormat": "{{datname}}"
}
],
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0}
},
{
"id": 2,
"title": "Database Size (bytes)",
"type": "graph",
"targets": [
{
"expr": "pg_database_size_bytes",
"legendFormat": "{{datname}}"
}
],
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0}
},
{
"id": 3,
"title": "Query Performance",
"type": "graph",
"targets": [
{
"expr": "rate(pg_stat_database_xact_commit[5m])",
"legendFormat": "Commits/sec - {{datname}}"
},
{
"expr": "rate(pg_stat_database_xact_rollback[5m])",
"legendFormat": "Rollbacks/sec - {{datname}}"
}
],
"gridPos": {"h": 8, "w": 24, "x": 0, "y": 8}
}
]
}
}'
Configure Grafana alerting rules
Set up alerting for critical PostgreSQL performance thresholds. These alerts will notify you when database performance degrades or issues occur.
groups:
- name: postgresql_alerts
interval: 30s
rules:
- alert: PostgreSQLDown
expr: pg_up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "PostgreSQL instance is down"
description: "PostgreSQL instance {{ $labels.instance }} has been down for more than 1 minute."
- alert: PostgreSQLTooManyConnections
expr: (pg_stat_database_numbackends / pg_settings_max_connections) * 100 > 80
for: 2m
labels:
severity: warning
annotations:
summary: "PostgreSQL has too many connections"
description: "PostgreSQL instance {{ $labels.instance }} has {{ $value }}% connections used."
- alert: PostgreSQLSlowQueries
expr: rate(pg_stat_database_xact_rollback[5m]) > 0.1
for: 2m
labels:
severity: warning
annotations:
summary: "PostgreSQL has slow queries"
description: "PostgreSQL instance {{ $labels.instance }} has high rollback rate: {{ $value }} per second."
- alert: PostgreSQLDatabaseSizeGrowth
expr: increase(pg_database_size_bytes[1h]) > 1073741824
for: 0m
labels:
severity: warning
annotations:
summary: "PostgreSQL database growing rapidly"
description: "Database {{ $labels.datname }} grew by {{ $value | humanize1024 }}B in the last hour."
Configure notification channels
Set up email notifications for PostgreSQL alerts. This ensures you receive immediate notification when database issues occur.
notifiers:
- name: email-notifications
type: email
uid: email-notifier
settings:
addresses: admin@example.com
subject: "[Grafana] PostgreSQL Alert - {{ .CommonLabels.alertname }}"
body: |
{{ range .Alerts }}
Alert: {{ .Annotations.summary }}
Description: {{ .Annotations.description }}
Severity: {{ .Labels.severity }}
Instance: {{ .Labels.instance }}
{{ end }}
Restart Grafana to apply configurations
Reload Grafana to enable the new alerting rules and notification channels. The monitoring setup is now complete.
sudo systemctl restart grafana-server
sudo systemctl status grafana-server
Verify your setup
Check that all components are working correctly and metrics are being collected.
# Verify PostgreSQL exporter is running
sudo systemctl status postgres_exporter
Check exporter metrics endpoint
curl http://localhost:9187/metrics | grep pg_up
Verify Prometheus is collecting PostgreSQL metrics
curl 'http://localhost:9090/api/v1/query?query=pg_up'
Check Grafana is accessible
curl -I http://localhost:3000/
Test PostgreSQL connectivity from exporter
sudo -u postgres psql -c "SELECT current_database(), current_user;"
Open Grafana at http://your-server-ip:3000 (admin/admin) and verify the PostgreSQL dashboard shows database metrics and connection information.
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Exporter won't start | Database connection failed | Check credentials in /etc/default/postgres_exporter and test connection manually |
| No metrics in Prometheus | Firewall blocking port 9187 | sudo ufw allow 9187 or check iptables rules |
| Permission denied errors | Wrong file ownership | sudo chown postgres:postgres /etc/default/postgres_exporter |
| Dashboard shows no data | Prometheus not scraping | Check Prometheus targets at http://localhost:9090/targets |
| Authentication failed | PostgreSQL user missing permissions | Grant pg_monitor role to postgres_exporter user |
| Alerts not firing | Notification channel misconfigured | Test notification channel in Grafana settings |
Next steps
- Set up Prometheus and Grafana monitoring stack with Docker compose
- Configure advanced Grafana dashboards and alerting with Prometheus integration
- Implement Grafana alerting with Prometheus and InfluxDB for comprehensive monitoring
- Configure PostgreSQL backup automation with monitoring and alerting
- Set up PostgreSQL performance tuning with pgbench and query optimization
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Default values
DB_PASSWORD=${1:-"secure_monitoring_password"}
POSTGRES_VERSION=""
EXPORTER_VERSION="0.15.0"
usage() {
echo "Usage: $0 [monitoring_user_password]"
echo " monitoring_user_password: Password for PostgreSQL monitoring user (default: secure_monitoring_password)"
exit 1
}
log() {
echo -e "${GREEN}[INFO]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1"
exit 1
}
cleanup() {
warn "Installation failed. Cleaning up..."
systemctl stop postgres_exporter 2>/dev/null || true
systemctl disable postgres_exporter 2>/dev/null || true
rm -f /etc/systemd/system/postgres_exporter.service
rm -f /usr/local/bin/postgres_exporter
rm -f /etc/default/postgres_exporter
rm -rf /etc/postgres_exporter
sudo -u postgres psql -c "DROP USER IF EXISTS postgres_exporter;" 2>/dev/null || true
systemctl daemon-reload
}
trap cleanup ERR
check_prerequisites() {
log "[1/9] Checking prerequisites..."
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root"
fi
if ! command -v wget &> /dev/null; then
error "wget is required but not installed"
fi
if ! command -v tar &> /dev/null; then
error "tar is required but not installed"
fi
if ! systemctl is-active --quiet postgresql; then
error "PostgreSQL is not running. Please install and start PostgreSQL first"
fi
}
detect_distro() {
log "[2/9] Detecting Linux distribution..."
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update && apt upgrade -y"
FIREWALL_CMD="ufw"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
FIREWALL_CMD="firewall-cmd"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
FIREWALL_CMD="firewall-cmd"
;;
*)
error "Unsupported distribution: $ID"
;;
esac
else
error "Cannot detect Linux distribution"
fi
log "Detected: $PRETTY_NAME"
}
update_system() {
log "[3/9] Updating system packages..."
$PKG_UPDATE
}
install_postgres_exporter() {
log "[4/9] Installing PostgreSQL exporter..."
cd /tmp
wget -q "https://github.com/prometheus-community/postgres_exporter/releases/download/v${EXPORTER_VERSION}/postgres_exporter-${EXPORTER_VERSION}.linux-amd64.tar.gz"
tar -xzf "postgres_exporter-${EXPORTER_VERSION}.linux-amd64.tar.gz"
cp "postgres_exporter-${EXPORTER_VERSION}.linux-amd64/postgres_exporter" /usr/local/bin/
chmod 755 /usr/local/bin/postgres_exporter
chown root:root /usr/local/bin/postgres_exporter
rm -rf postgres_exporter-*
}
create_postgres_user() {
log "[5/9] Creating PostgreSQL monitoring user..."
sudo -u postgres psql -c "DROP USER IF EXISTS postgres_exporter;" || true
sudo -u postgres psql -c "CREATE USER postgres_exporter WITH PASSWORD '$DB_PASSWORD';"
sudo -u postgres psql -c "GRANT CONNECT ON DATABASE postgres TO postgres_exporter;"
sudo -u postgres psql -c "GRANT pg_monitor TO postgres_exporter;"
}
configure_exporter_env() {
log "[6/9] Configuring PostgreSQL exporter environment..."
cat > /etc/default/postgres_exporter << EOF
DATA_SOURCE_NAME="postgresql://postgres_exporter:$DB_PASSWORD@localhost:5432/postgres?sslmode=disable"
PG_EXPORTER_WEB_LISTEN_ADDRESS=":9187"
PG_EXPORTER_EXTEND_QUERY_PATH="/etc/postgres_exporter/queries.yaml"
EOF
chmod 640 /etc/default/postgres_exporter
chown root:postgres /etc/default/postgres_exporter
}
create_systemd_service() {
log "[7/9] Creating systemd service..."
cat > /etc/systemd/system/postgres_exporter.service << EOF
[Unit]
Description=PostgreSQL Exporter
Wants=network-online.target
After=network-online.target
Requires=postgresql.service
After=postgresql.service
[Service]
Type=simple
User=postgres
Group=postgres
EnvironmentFile=/etc/default/postgres_exporter
ExecStart=/usr/local/bin/postgres_exporter
Restart=always
RestartSec=5
NoNewPrivileges=yes
ProtectHome=yes
ProtectSystem=strict
ProtectKernelTunables=yes
ProtectControlGroups=yes
RestrictRealtime=yes
[Install]
WantedBy=multi-user.target
EOF
chmod 644 /etc/systemd/system/postgres_exporter.service
systemctl daemon-reload
}
create_queries_config() {
log "[8/9] Creating extended queries configuration..."
mkdir -p /etc/postgres_exporter
chown postgres:postgres /etc/postgres_exporter
chmod 755 /etc/postgres_exporter
cat > /etc/postgres_exporter/queries.yaml << 'EOF'
pg_database_size:
query: "SELECT pg_database.datname, pg_database_size(pg_database.datname) as size FROM pg_database"
master: true
metrics:
- datname:
usage: "LABEL"
description: "Name of the database"
- size:
usage: "GAUGE"
description: "Disk space used by the database"
pg_table_stats:
query: "SELECT schemaname, tablename, n_tup_ins, n_tup_upd, n_tup_del FROM pg_stat_user_tables"
master: true
metrics:
- schemaname:
usage: "LABEL"
description: "Schema name"
- tablename:
usage: "LABEL"
description: "Table name"
- n_tup_ins:
usage: "COUNTER"
description: "Number of tuples inserted"
- n_tup_upd:
usage: "COUNTER"
description: "Number of tuples updated"
- n_tup_del:
usage: "COUNTER"
description: "Number of tuples deleted"
pg_connection_stats:
query: "SELECT datname, numbackends, xact_commit, xact_rollback FROM pg_stat_database"
master: true
metrics:
- datname:
usage: "LABEL"
description: "Database name"
- numbackends:
usage: "GAUGE"
description: "Number of backends connected to this database"
- xact_commit:
usage: "COUNTER"
description: "Number of transactions committed"
- xact_rollback:
usage: "COUNTER"
description: "Number of transactions rolled back"
EOF
chmod 644 /etc/postgres_exporter/queries.yaml
chown postgres:postgres /etc/postgres_exporter/queries.yaml
}
start_and_verify() {
log "[9/9] Starting services and verifying installation..."
systemctl enable postgres_exporter
systemctl start postgres_exporter
sleep 3
if systemctl is-active --quiet postgres_exporter; then
log "PostgreSQL exporter is running successfully"
else
error "Failed to start PostgreSQL exporter"
fi
# Test metrics endpoint
if curl -s http://localhost:9187/metrics | grep -q "pg_up"; then
log "Metrics endpoint is responding correctly"
else
warn "Metrics endpoint may not be working properly"
fi
# Configure firewall if available
if command -v $FIREWALL_CMD &> /dev/null; then
case "$FIREWALL_CMD" in
"ufw")
ufw allow 9187/tcp comment "PostgreSQL Exporter" || warn "Failed to configure UFW rule"
;;
"firewall-cmd")
firewall-cmd --permanent --add-port=9187/tcp || warn "Failed to configure firewall rule"
firewall-cmd --reload || warn "Failed to reload firewall"
;;
esac
fi
}
main() {
if [[ "${1:-}" == "-h" ]] || [[ "${1:-}" == "--help" ]]; then
usage
fi
check_prerequisites
detect_distro
update_system
install_postgres_exporter
create_postgres_user
configure_exporter_env
create_systemd_service
create_queries_config
start_and_verify
log "PostgreSQL monitoring setup completed successfully!"
log "PostgreSQL exporter is running on port 9187"
log "Add this target to your Prometheus configuration:"
log " - targets: ['$(hostname -f):9187']"
log "Monitor logs with: journalctl -u postgres_exporter -f"
}
main "$@"
Review the script before running. Execute with: bash install.sh