Prometheus and Grafana — Production Monitoring Stack Setup
Advertisement
Introduction
Why This Matters
Prometheus and Grafana form the open-source monitoring foundation for most cloud-native applications. Prometheus collects time-series metrics by scraping HTTP endpoints, stores them efficiently, and evaluates alert rules. Grafana queries Prometheus via PromQL and renders dashboards, making it possible to visualize system health, debug performance regressions, and alert on-call engineers before users notice problems.
Prometheus Installation and Configuration
# Docker Compose stack
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
prometheus:
image: prom/prometheus:v2.48.0
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- ./rules/:/etc/prometheus/rules/
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=30d'
- '--web.enable-lifecycle'
- '--web.enable-admin-api'
restart: unless-stopped
grafana:
image: grafana/grafana:10.2.0
container_name: grafana
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
environment:
- GF_SECURITY_ADMIN_PASSWORD=secure-password
- GF_USERS_ALLOW_SIGN_UP=false
restart: unless-stopped
alertmanager:
image: prom/alertmanager:v0.26.0
container_name: alertmanager
ports:
- "9093:9093"
volumes:
- ./alertmanager.yml:/etc/alertmanager/alertmanager.yml
volumes:
prometheus_data:
grafana_data:
EOF
docker compose up -dPrometheus Configuration
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
cluster: production
region: us-east-1
rule_files:
- "rules/*.yml"
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
scrape_configs:
# Prometheus self-monitoring
- job_name: prometheus
static_configs:
- targets: ['localhost:9090']
# Node exporter — OS metrics
- job_name: node
static_configs:
- targets:
- web1.example.com:9100
- web2.example.com:9100
- db1.example.com:9100
# Application metrics
- job_name: my-app
metrics_path: /metrics
static_configs:
- targets: ['app:3000']
# Kubernetes pods (auto-discovery)
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)Instrumenting Applications
// Node.js with prom-client
const promClient = require('prom-client');
const express = require('express');
const app = express();
// Collect default metrics (CPU, memory, event loop)
promClient.collectDefaultMetrics({ prefix: 'myapp_' });
// Custom counters
const httpRequestsTotal = new promClient.Counter({
name: 'myapp_http_requests_total',
help: 'Total HTTP requests',
labelNames: ['method', 'route', 'status_code']
});
// Custom histograms (latency)
const httpDuration = new promClient.Histogram({
name: 'myapp_http_request_duration_seconds',
help: 'HTTP request duration',
labelNames: ['method', 'route'],
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5]
});
// Middleware
app.use((req, res, next) => {
const end = httpDuration.startTimer({ method: req.method, route: req.path });
res.on('finish', () => {
httpRequestsTotal.inc({ method: req.method, route: req.path, status_code: res.statusCode });
end();
});
next();
});
// Expose metrics endpoint
app.get('/metrics', async (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.end(await promClient.register.metrics());
});PromQL Queries
# Request rate (requests per second, 5-minute window)
rate(myapp_http_requests_total[5m])
# Error rate (percentage)
sum(rate(myapp_http_requests_total{status_code=~"5.."}[5m]))
/
sum(rate(myapp_http_requests_total[5m])) * 100
# 95th percentile latency
histogram_quantile(0.95, rate(myapp_http_request_duration_seconds_bucket[5m]))
# CPU usage per node
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
# Memory usage
node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100
# Disk usage
(node_filesystem_size_bytes - node_filesystem_free_bytes) / node_filesystem_size_bytes * 100Alert Rules
# rules/application.yml
groups:
- name: application
rules:
- alert: HighErrorRate
expr: |
sum(rate(myapp_http_requests_total{status_code=~"5.."}[5m]))
/
sum(rate(myapp_http_requests_total[5m])) > 0.05
for: 2m
labels:
severity: critical
team: backend
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value | humanizePercentage }} (threshold 5%)"
runbook: "https://wiki.example.com/runbooks/high-error-rate"
- alert: HighLatency
expr: |
histogram_quantile(0.95,
rate(myapp_http_request_duration_seconds_bucket[5m])
) > 1.0
for: 5m
labels:
severity: warning
annotations:
summary: "P95 latency above 1 second"
- alert: InstanceDown
expr: up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Instance {{ $labels.instance }} is down"Alertmanager Configuration
# alertmanager.yml
global:
resolve_timeout: 5m
slack_api_url: 'https://hooks.slack.com/services/...'
route:
group_by: ['alertname', 'cluster']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: default-receiver
routes:
- match:
severity: critical
receiver: pagerduty-critical
continue: true
- match:
team: backend
receiver: backend-slack
receivers:
- name: default-receiver
slack_configs:
- channel: '#alerts'
text: '{{ template "slack.default.text" . }}'
- name: pagerduty-critical
pagerduty_configs:
- routing_key: YOUR_PAGERDUTY_KEY
- name: backend-slack
slack_configs:
- channel: '#backend-alerts'Common Mistakes
- Setting scrape intervals too low (1-5s) on high-cardinality metrics — causes CPU and memory issues in Prometheus
- Using too many label values (e.g., user IDs as labels) — high cardinality causes memory explosion
- Not setting retention time — Prometheus default 15-day retention fills disk on busy systems
- Writing alerts that trigger on brief spikes — always use
for: Xmto confirm condition persists before alerting - Not configuring
external_labelson Prometheus — required when federation or remote write is used
Best Practices
- Follow the RED method for service metrics: Rate, Errors, Duration per endpoint
- Use recording rules to pre-compute expensive PromQL queries used frequently in dashboards
- Design dashboards at three levels: overview (cluster), service (per-service SLOs), and debug (detailed per-instance)
- Store Grafana dashboards as JSON in Git — use provisioning to load them automatically
- Set alert
severitylabels (critical, warning, info) to route to appropriate channels - Use Thanos or Cortex for long-term metric storage and multi-cluster federation
Key Takeaways
- Prometheus pulls (scrapes) metrics from HTTP
/metricsendpoints on a configurable interval - PromQL is a functional query language —
rate(),histogram_quantile(), andsum by()are the most-used functions - High cardinality labels (user IDs, request IDs) cause memory exhaustion — labels must have bounded value sets
- Recording rules pre-compute expensive queries into new time series for dashboard performance
- Alertmanager handles deduplication, grouping, silencing, and routing of Prometheus alerts to notification channels
- The
forclause in alert rules prevents alerting on transient spikes — only fire after the condition persists - Grafana provisions dashboards, data sources, and alert contacts from YAML files in version control
- The Prometheus Operator on Kubernetes uses
ServiceMonitorCRDs to automate scrape configuration for pods and services
Advertisement
Related reading
Monitoring and Observability Guide 2026 — Prometheus, Grafana, and OpenTelemetry5 min readThe Grafana LGTM Stack — Logs, Metrics, Traces, and Profiles in One Platform8 min readGrafana Loki Log Aggregation 2026 — The Prometheus-Native Logging Stack6 min readNo Observability Strategy — Flying Blind in Production4 min readMonitoring RAG in Production — What to Track When Your Chatbot Goes Live12 min readAI Analytics Backend — Tracking User Behavior, Query Patterns, and Business Metrics11 min read