What You'll Build
In this exercise you will build a complete Service Health Dashboard for the CricketPulse scoring service using the four-row hierarchy, add a parameterised $endpoint variable, push a deployment annotation, and share the dashboard via snapshot. By the end you will have a functional production-grade dashboard that could be used during a real incident, with all the structural patterns from Lessons 9, 10, and 11 applied in a single working artefact.
Prerequisites
- Lessons 8 stack running (cricketpulse service and Prometheus) — docker compose ps should show both Up
- Lessons 9-11 completed — understand data sources, panel types, variables, and annotations
- Grafana basics — you should be able to create a panel and configure a query
- Pushgateway available (add to docker-compose.yml in Step 4)
Setup — Add Grafana to the Stack
Add Grafana to the existing docker-compose.yml and create the provisioning directory structure. Anonymous auth is enabled for this exercise to avoid login friction — in production, configure LDAP, OAuth, or Grafana's built-in user management.
# Prerequisites: Lesson 8 stack must be running
cd cricketpulse-metrics
docker compose ps # verify cricketpulse and prometheus are running
# Add Grafana to docker-compose.yml
# (append to the services section)
cat >> docker-compose.yml << 'YAML'
grafana:
image: grafana/grafana:latest
ports:
- '3000:3000'
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning
YAML
mkdir -p grafana/provisioning/datasources grafana/provisioning/dashboards
docker compose up -d grafana
# Grafana UI: http://localhost:3000
Step 1 — Provision the Prometheus Datasource
Create the datasource provisioning YAML so Grafana automatically connects to Prometheus on startup. Provisioning ensures the datasource survives container restarts without manual reconfiguration.
# grafana/provisioning/datasources/datasources.yml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
uid: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
jsonData:
timeInterval: '15s'
# After saving, restart Grafana to pick up the provisioned datasource:
docker compose restart grafana
# Verify datasource via API:
curl http://localhost:3000/api/datasources | python3 -m json.tool
Step 2 — Build the Four-Row Dashboard
Create a new dashboard in the Grafana UI and add panels following the four-row hierarchy. Build the Status row first with three Stat panels, then the Trends row with two Time Series panels, then the Breakdown row with a Table panel. Use the PromQL expressions in the code block as a reference.
# Dashboard creation steps (Grafana UI walkthrough)
# 1. Click + -> New Dashboard -> Add visualization
# ROW 1: STATUS (Stat panels)
# Panel A: Error Rate
# Type: Stat
# Query: sum(rate(cricketpulse_http_requests_total{status_code=~'5..'}[5m]))
# / sum(rate(cricketpulse_http_requests_total[5m]))
# Unit: Percent (0.0-1.0)
# Thresholds: green=0, yellow=0.001, red=0.01
# Panel B: p99 Latency
# Type: Stat
# Query: histogram_quantile(0.99,
# sum(rate(cricketpulse_http_request_duration_seconds_bucket[5m])) by (le)
# )
# Unit: seconds (s)
# Thresholds: green=0, yellow=0.5, red=1.0
# Panel C: Request Rate
# Type: Stat
# Query: sum(rate(cricketpulse_http_requests_total[5m]))
# Unit: requests/sec (reqps)
# ROW 2: TRENDS (Time Series)
# Panel D: Request Rate Over Time
# Type: Time Series
# Query: sum(rate(cricketpulse_http_requests_total[5m])) by (endpoint)
# Legend: {{endpoint}}
# Panel E: Error Rate Over Time
# Type: Time Series
# Query: sum(rate(cricketpulse_http_requests_total{status_code=~'5..'}[5m])) by (endpoint)
# / sum(rate(cricketpulse_http_requests_total[5m])) by (endpoint)
# Unit: Percent (0.0-1.0)
# ROW 3: BREAKDOWN (Table)
# Panel F: Endpoint Request Rates
# Type: Table
# Query: sum(rate(cricketpulse_http_requests_total[5m])) by (endpoint, status_code)
# Instant: true (show current value, not time series)
Step 3 — Add the $endpoint Variable
Add a query variable that populates from the Prometheus label values and update the Trends row panels to filter by the variable selection. Test the variable by selecting individual endpoints and verifying that panels filter correctly.
# Add $endpoint variable to the dashboard
# Dashboard Settings -> Variables -> Add variable
#
# Name: endpoint
# Type: Query
# Datasource: Prometheus
# Query: label_values(cricketpulse_http_requests_total, endpoint)
# Multi-value: true
# Include All: true (regex: .*)
# Refresh: On time range change
#
# Update Panel D query to use the variable:
# sum(rate(cricketpulse_http_requests_total{endpoint=~'$endpoint'}[5m])) by (endpoint)
#
# Update Panel E query:
# sum(rate(cricketpulse_http_requests_total{endpoint=~'$endpoint',status_code=~'5..'}[5m])) by (endpoint)
# / sum(rate(cricketpulse_http_requests_total{endpoint=~'$endpoint'}[5m])) by (endpoint)
Step 4 — Push a Deployment Annotation
Add Pushgateway to the docker-compose stack, create the deployment annotation shell script, and configure a Grafana annotation query to display deployment events as vertical lines on the Time Series panels. Push a test annotation and verify it appears on the dashboard.
# push_deployment.sh — run this after each deployment
#!/bin/bash
VERSION=${1:-v1.0.0}
PUSHGATEWAY=http://localhost:9091
# Add Pushgateway to docker-compose.yml if not already present
# pushgateway:
# image: prom/pushgateway:latest
# ports:
# - '9091:9091'
cat <<EOF | curl --silent --data-binary @- \
${PUSHGATEWAY}/metrics/job/deployments/instance/cricketpulse
# HELP deployment_timestamp Unix timestamp of deployment
# TYPE deployment_timestamp gauge
deployment_timestamp{service="cricketpulse",version="${VERSION}"} $(date +%s)
EOF
echo "Deployment annotation pushed for cricketpulse ${VERSION}"
# In Grafana: Dashboard Settings -> Annotations -> Add annotation query
# Datasource: Prometheus
# Query: changes(deployment_timestamp{service='cricketpulse'}[1m]) > 0
# Step: 60s
# Title field: version
# Use value as timestamp: false
# Verify dashboard panels return data
# 1. Generate traffic
python3 generate_traffic.py
# 2. Check each panel in Grafana (inspect -> Query -> Run)
# All panels should show non-zero values after 5 min of traffic
# 3. Test the $endpoint variable
# - Select 'live_score' only -> panels filter to live_score
# - Select 'All' -> panels show all endpoints
# 4. Push a fake deployment annotation
bash push_deployment.sh v2.0.0
# Verify: a vertical annotation line appears on Time Series panels
# 5. Generate a share URL
# Dashboard -> Share -> Link -> Copy link with current time range
# Paste the URL into an incognito window — should show the same view
# 6. Create a snapshot for this exercise
# Share -> Snapshot -> Publish
# Copy the snapshot URL — this is what you would share in an incident report
Warning: The GF_AUTH_ANONYMOUS_ENABLED=true configuration is for development only. Never deploy Grafana with anonymous admin access in production. Use GF_SECURITY_ADMIN_PASSWORD for local development and configure OIDC or SAML for production authentication. An unprotected Grafana instance exposes all your metric data and allows anyone to create alert silences or modify dashboards.
Extension challenge: Export the dashboard JSON (Dashboard Settings -> JSON Model -> Copy to clipboard) and save it to grafana/dashboards/cricketpulse-health.json. Add a dashboard provisioning configuration to grafana/provisioning/dashboards/dashboards.yml so the dashboard is automatically loaded on Grafana startup. Verify by destroying and recreating the Grafana container — the dashboard should reappear automatically.
- Provisioned datasources survive container restarts — never configure production datasources only through the UI.
- Build the Status row first — it is the most-viewed element during incidents and should be verified first.
- Test the $endpoint variable by selecting individual endpoints and verifying panel filtering works correctly.
- Deployment annotations require Pushgateway — Prometheus cannot accept pushed metrics without it.
- Always verify annotations appear on the Time Series panels before using the dashboard in production.
- Export and version-control the dashboard JSON to enable GitOps and disaster recovery.