🔥 Prometheus and Grafana Monitoring Stack
This is where the last two lessons come together. Prometheus collects and stores your metrics; Grafana turns them into dashboards; Alertmanager pages the right person when something breaks. It is the de facto open-source monitoring stack for cloud-native systems — and by the end of this lesson you will have stood the whole thing up with Docker Compose.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Describe Prometheus's pull-based architecture and time-series data model
- Choose correctly between the four metric types — counter, gauge, histogram, summary
- Write useful PromQL queries for rate, error ratio, and percentile latency
- Stand up Prometheus + Grafana + Alertmanager with Docker Compose and provisioning
- Build a Grafana dashboard and route alerts by severity
Estimated Time: 40–50 minutes • Difficulty: Intermediate
Hands-on: Run the full stack locally, scrape a service, and build a RED dashboard.
In This Lesson
The Complementary Pair
Prometheus and Grafana do different jobs, and that is exactly why they pair so well.
- Prometheus is the meter — it continuously measures, stores time-series data, evaluates rules, and fires alerts. Originally built at SoundCloud, it is now a graduated CNCF project.
- Grafana is the display — it queries data (from Prometheus and many other sources) and turns it into dashboards, and can alert too.
🏠 A smart-home analogy: Prometheus is the electricity meter on the wall — always measuring, recording, and tripping the breaker at a threshold. Grafana is the smart-home app on your phone — pulling readings from the meter and every other sensor into one clear screen.
How Prometheus Works
Prometheus is pull-based: instead of your apps pushing data to it, Prometheus reaches out and scrapes a /metrics HTTP endpoint on each target at a fixed interval. This makes targets simple (they just expose a page) and lets Prometheus automatically detect a target that has gone down.
The core loop:
- Discover targets — statically listed, or dynamically via Kubernetes, Consul, or cloud APIs.
- Scrape each target's
/metricsendpoint on thescrape_interval. - Store samples in a local time-series database optimized for this shape of data.
- Query with PromQL, and evaluate alert rules on the same data.
- Fire triggered alerts to Alertmanager, which dedupes, groups, and routes them.
📖 The time-series data model
Every sample belongs to a time series uniquely identified by a metric name plus a set of labels. For example:
http_requests_total{method="GET", status="200", route="/api/users"}
Changing any label value creates a distinct series — which is why bounded label cardinality (from the monitoring lesson) matters so much here.
The Four Metric Types
Picking the right metric type is the most common early mistake. Here is when to use each:
| Type | Behavior | Use for | Example |
|---|---|---|---|
| Counter | Only goes up (or resets to 0) | Cumulative totals | http_requests_total |
| Gauge | Goes up and down | Current snapshots | memory_usage_bytes |
| Histogram | Buckets observations | Distributions (query-side quantiles) | http_request_duration_seconds |
| Summary | Pre-computes quantiles | Client-side percentiles | request_latency_summary |
💡 Histogram or summary?
Prefer histograms in almost all cases. Because they store raw bucket counts, you can compute any percentile at query time and aggregate across many instances. Summaries pre-compute quantiles on the client, which is cheaper to query but cannot be aggregated across instances — a p95 of five servers' p95s is meaningless.
Declaring one of each with prom-client:
import client from 'prom-client';
const register = new client.Registry();
// Counter — cumulative request count
const requests = new client.Counter({
name: 'http_requests_total',
help: 'Total HTTP requests',
labelNames: ['method', 'route', 'status'],
registers: [register],
});
// Gauge — a value that moves both ways
const activeSessions = new client.Gauge({
name: 'active_sessions',
help: 'Current active user sessions',
registers: [register],
});
// Histogram — bucketed durations, quantiles computed at query time
const duration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'Request duration in seconds',
labelNames: ['method', 'route'],
buckets: [0.1, 0.3, 0.5, 1, 3, 5],
registers: [register],
});
requests.inc({ method: 'GET', route: '/api', status: '200' });
activeSessions.set(42);
const stop = duration.startTimer({ method: 'GET', route: '/api' });
// ... handle request ...
stop();
PromQL Essentials
PromQL is a functional query language for selecting and aggregating time series. A few patterns cover most day-to-day needs.
Selecting series
# All series for this metric
http_requests_total
# Filter by labels (= exact, != negate, =~ regex)
http_requests_total{status="200", method="GET"}
http_requests_total{status=~"5.."}
# A range vector — the last 5 minutes of samples
http_requests_total[5m]
The functions you will use constantly
# rate() — per-second average over a window; the go-to for counters
rate(http_requests_total[5m])
# Aggregate across series with sum/avg/max ... by (label)
sum(rate(http_requests_total[5m])) by (route)
# Error rate as a percentage
sum(rate(http_requests_total{status=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m])) * 100
# 95th percentile latency from a histogram
histogram_quantile(0.95,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
# CPU utilization per instance (from node_exporter)
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
⚠️ Always rate() a counter before graphing it
A raw counter only ever climbs, so its graph is a meaningless up-and-to-the-right line. rate() converts it into the per-second change — the thing you actually care about — and correctly handles counter resets on restart.
Standing Up the Stack
Here is a complete, runnable Docker Compose file bringing up Prometheus, Grafana, Alertmanager, and node_exporter together.
# docker-compose.yml
services:
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- ./prometheus/rules:/etc/prometheus/rules
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=15d'
ports: ["9090:9090"]
restart: unless-stopped
alertmanager:
image: prom/alertmanager:latest
volumes:
- ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml
ports: ["9093:9093"]
restart: unless-stopped
node-exporter:
image: prom/node-exporter:latest
ports: ["9100:9100"]
restart: unless-stopped
grafana:
image: grafana/grafana:latest
depends_on: [prometheus]
ports: ["3000:3000"]
volumes:
- grafana-storage:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
environment:
- GF_SECURITY_ADMIN_PASSWORD__FILE=/run/secrets/grafana_admin
restart: unless-stopped
volumes:
grafana-storage:
The Prometheus config tells it what to scrape and where Alertmanager lives:
# prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
rule_files:
- 'rules/*.yml'
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'node'
static_configs:
- targets: ['node-exporter:9100']
- job_name: 'web-app'
static_configs:
- targets: ['web-app:3000']
Rather than clicking through Grafana's UI, provision the data source as code so the stack is reproducible:
# grafana/provisioning/datasources/prometheus.yml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
💡 In Kubernetes, use the Operator
For real clusters, the kube-prometheus-stack Helm chart installs Prometheus, Grafana, Alertmanager, and dozens of ready-made dashboards, and lets you define scrape targets declaratively with ServiceMonitor resources. Docker Compose is perfect for learning; the Operator is how it is run in production.
Building Dashboards
A Grafana dashboard is a grid of panels, each backed by a query. Panels come in types — time series, stat, gauge, bar, table, heatmap, logs. The art is choosing panels that answer a question at a glance.
A great first dashboard is a RED dashboard for your service — one row, three panels, the whole health of the service in one look:
# Panel 1 — Rate (requests/sec by route), a "time series" panel
sum(rate(http_requests_total[5m])) by (route)
# Panel 2 — Errors (% of requests failing), a "stat" panel with red threshold
sum(rate(http_requests_total{status=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m])) * 100
# Panel 3 — Duration (p50/p95/p99), a "time series" panel with three queries
histogram_quantile(0.50, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
✅ Make dashboards reusable with variables
Add a template variable so one dashboard serves every instance or route. Define a query variable $route with label_values(http_requests_total, route), then reference {route="$route"} in each panel. A dropdown at the top now filters the whole board.
Do not build everything from scratch — Grafana's dashboard library has thousands of community dashboards (the Node Exporter Full dashboard, ID 1860, is a classic) you can import by ID and adapt.
Alerting with Alertmanager
Prometheus evaluates alert rules and, when one fires, hands it to Alertmanager, whose job is to make alerts humane: deduplicate identical alerts, group related ones, silence during maintenance, and route by severity to the right channel.
Recall the alert rules from the monitoring lesson — they live in the rules/ directory Prometheus loads. Alertmanager then decides who gets paged:
# alertmanager/alertmanager.yml
global:
resolve_timeout: 5m
route:
group_by: ['alertname', 'job']
group_wait: 30s # wait to batch related alerts
group_interval: 5m
repeat_interval: 4h # don't re-page every minute
receiver: 'team-slack' # default destination
routes:
- matchers: [severity="critical"]
receiver: 'pagerduty' # wake someone up
- matchers: [severity="warning"]
receiver: 'team-slack' # just notify
receivers:
- name: 'team-slack'
slack_configs:
- api_url: ''
channel: '#alerts'
title: '{{ .CommonAnnotations.summary }}'
- name: 'pagerduty'
pagerduty_configs:
- routing_key: ''
⚠️ Route by severity, not by volume
Send only critical, wake-someone-up alerts to PagerDuty; route warnings to a Slack channel people check during the day. Mixing them trains the team to ignore the pager — the exact failure mode good alerting is meant to prevent.
Hands-on Exercise
🏋️ Run the stack & build a RED dashboard
Objective: Bring up the full monitoring stack, scrape an instrumented service, and visualize its health.
Instructions:
- Create the directory layout and the four config files from the "Standing Up the Stack" section, then run
docker compose up -d. - Add the instrumented Express app (from the monitoring lesson) as a
web-appservice in Compose and confirm it exposes/metrics. - Open Prometheus at
http://localhost:9090/targetsand verify all targets are UP. - Open Grafana at
http://localhost:3000, confirm the Prometheus data source is provisioned, and build a RED dashboard with the three queries shown earlier. - Generate traffic against the app and watch the panels move in real time.
💡 Hint
If a target shows DOWN, the usual cause is the service name / port in scrape_configs not matching the Compose service. Prometheus resolves web-app:3000 by the Compose service name, so they must match exactly and be on the same network.
✅ Example solution
A verifying PromQL query you can paste into Grafana's Explore view once traffic is flowing:
# Should show a non-zero, moving line per route
sum(rate(http_requests_total{job="web-app"}[1m])) by (route)
Your finished dashboard: Panel 1 (Rate) shows per-route request rate rising as you send traffic; Panel 2 (Errors) sits near 0% until you hit an error route; Panel 3 (Duration) plots p50/p95/p99 latency. That single row is the health of the service at a glance — exactly what an on-call engineer opens first.
Production Best Practices
| ✅ Do | ❌ Avoid |
|---|---|
| Provision data sources & dashboards as code | Hand-clicking config that dies with the container |
| Set a sensible TSDB retention window | Storing raw metrics forever on local disk |
| Prefer histograms; keep cardinality bounded | Summaries you later try to aggregate |
| Enable auth & TLS; use secrets for passwords | Shipping admin/admin to production |
| Monitor the monitoring stack itself | Assuming Prometheus never runs out of disk |
Scaling beyond one Prometheus
A single Prometheus is not horizontally scalable and is bounded by one machine's disk and RAM. When you outgrow it, reach for:
- Functional sharding — several Prometheus servers, each owning part of the fleet.
- Federation — a higher-level Prometheus scraping aggregated series from lower ones for a global view.
- Thanos, Cortex, or Grafana Mimir — systems that add long-term object-storage, a global query view, and high availability on top of Prometheus.
Summary & Quiz
🎉 Key Takeaways
- Prometheus pull-scrapes
/metricsendpoints into a time-series database; Grafana visualizes it. - Use a counter for totals, a gauge for snapshots, and a histogram for distributions (prefer it over summaries).
- PromQL:
rate()counters, aggregateby (label), and usehistogram_quantile()for percentiles. - Stand the stack up with Docker Compose and provision data sources as code for reproducibility.
- Alertmanager dedupes, groups, and routes alerts by severity — critical to a pager, warnings to chat.
🎯 Quick Quiz
Question 1: Prometheus uses a "pull" model. What does that mean?
Question 2: You want p95 latency aggregated across ten instances of a service. Which metric type should you have used?
Question 3: What is Alertmanager's primary job, distinct from Prometheus?
📚 Further Reading
- Prometheus Documentation
- Grafana Documentation
- PromQL Querying Basics
- kube-prometheus — the production Kubernetes stack
🚀 What's Next?
You have now covered the full observability triad — metrics, logs, and dashboards with alerting. Next comes the weekend project, where you will pull everything from this DevOps & Deployment module together into one deployed, monitored application.
🎉 Module milestone!
You can instrument, log, visualize, and alert on a production system. Time to put it all together.