Prometheus and Grafana — Production Monitoring Stack Setup

Sanjeev SharmaSanjeev Sharma
5 min read

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 -d

Prometheus 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 * 100

Alert 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: Xm to confirm condition persists before alerting
  • Not configuring external_labels on 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 severity labels (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 /metrics endpoints on a configurable interval
  • PromQL is a functional query language — rate(), histogram_quantile(), and sum 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 for clause 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 ServiceMonitor CRDs to automate scrape configuration for pods and services

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading