Education › Site Reliability › Stage 2: Observability

Dashboards with Grafana

Dashboards that answer questions during an incident instead of decorating a wall.

Intermediate ~30 min read Module 6 of 16

Most dashboards are built once, in a quiet week, by someone adding every metric they can find. Then an incident happens, the on-call engineer opens forty panels of squiggly lines, and none of them answers the only question that matters: are users affected, and where is the problem? A good dashboard is a tool for making decisions under pressure. This module teaches a method for designing dashboards that answer questions, the Grafana features that make them work, and how to manage them as code.

After this module you can
  • Design a dashboard around the questions an on-call engineer needs answered, from top to bottom
  • Apply the RED and USE methods to decide which panels a service dashboard needs
  • Choose the right visualisation, unit, axis and threshold for each kind of data
  • Use variables, annotations and links to make one dashboard serve many services and speed up investigation
  • Manage data sources and dashboards as code with provisioning and version control

Start from questions, not metrics

Before adding a panel, write down who will look at the dashboard and what they need to decide. For an on-call engineer who has just been paged, the questions come in a fixed order.

  1. Is it really broken, and for whom? Are we meeting the SLO right now? How much error budget is left?
  2. What are users experiencing? Request rate, error rate and latency for the service as a whole.
  3. Where is it? Which route, which instance, which zone, which version, which dependency?
  4. Why? Is a resource saturated: CPU, memory, connection pool, queue, disk?
  5. What changed? Was there a deploy, a config push, a traffic spike, an autoscaling event?

Lay the dashboard out in the same order, top to bottom. The top row should be readable in five seconds and answer question one, so that someone glancing at it knows whether to worry. Detail goes lower down, in rows that can be collapsed. Keep one question per panel and give the panel a title that states it, such as "Error ratio by route" and not "Errors".

Build a small hierarchy instead of one giant board. An overview dashboard shows the health of every service on one screen, mostly as SLO status. Each service has its own dashboard, reached by a link from the overview. From there, links lead to the dashboards of its dependencies and resources: database, queue, nodes. An engineer should be able to move from "something is wrong" to "it is the connection pool on the orders database" in three or four clicks.

which service?dependencyresourcedata linktrace_idOverviewSLO status, all svcsServiceRED, breakdown, USEDatabaseconnections, locksNodesCPU, memory, diskLogssame filters, rangeTracesslow requests
A dashboard hierarchy instead of one giant board: an overview shows SLO health for every service, each service has its own dashboard, and links carry the responder from a symptom to the dependency or resource that explains it.
Tip

The real test of a dashboard is an incident. After each one, ask which panel showed the problem first, which panels misled you, and what you had to query by hand. Add the missing panel and delete the ones nobody looked at.

What goes on a service dashboard

The RED method gives you the core of any dashboard for a request-driven service: Rate, Errors, Duration. It is the user's view, and it should come first. The USE method covers every resource the service depends on: Utilisation, Saturation, Errors. It is the machine's view, and it explains the symptoms that RED reveals.

RowPanelsNotes
1. HealthSLO compliance, error budget remaining, current burn rateStat panels with thresholds; readable at a glance
2. REDRequest rate, error ratio, latency (p50, p95, p99)Whole service first, then broken down by route
3. BreakdownThe same three, by instance, zone or versionShows whether the problem is everywhere or in one place
4. DependenciesRate, errors and latency of calls this service makesDatabase, cache, downstream APIs
5. Resources (USE)CPU and throttling, memory against its limit, restarts, pool and queue usageCollapsed by default
6. ChangesDeploy annotations on every graph; replica countCorrelates symptoms with causes
Panel queries, using recording rules from the Prometheus module
promql
# Request rate by route
sum by (route) (rate(http_requests_total{job="$job"}[$__rate_interval]))

# Error ratio (a percentage when the unit is set to 'Percent (0.0-1.0)')
sum(rate(http_requests_total{job="$job", code=~"5.."}[$__rate_interval]))
/
sum(rate(http_requests_total{job="$job"}[$__rate_interval]))

# Latency percentiles: one query per percentile on the same panel
histogram_quantile(0.99, sum by (le) (
  rate(http_request_duration_seconds_bucket{job="$job"}[$__rate_interval])))

# CPU throttling: share of CPU periods in which the container was throttled
sum by (pod) (rate(container_cpu_cfs_throttled_periods_total{pod=~"$job-.*"}[$__rate_interval]))
/
sum by (pod) (rate(container_cpu_cfs_periods_total{pod=~"$job-.*"}[$__rate_interval]))

$__rate_interval is a Grafana variable designed for rate(). It picks a window that is always at least four scrape intervals and that grows as you zoom out, so graphs stay correct whether you look at the last fifteen minutes or the last thirty days. Use it in place of a hard-coded [5m] in dashboard queries. Alerting rules in Prometheus still need a fixed window.

Visualisation that does not mislead

DataPanelWhy
How something changes over timeTime seriesThe default; use it for rate, errors and latency
One current value against a thresholdStat or gaugeBudget remaining, replicas ready, days until a certificate expires
Distribution over timeHeatmapShows the whole latency distribution, including a second cluster of slow requests that percentiles hide
Ranked comparison across many itemsBar gauge or tableTop routes by error count, pods by memory
State over timeState timelineUp or down, or deployed version, per instance
Log lines next to graphsLogs panelContext without leaving the dashboard
  • Set the unit on every panel: seconds, bytes, percent, requests per second. Grafana then formats 0.0042 as 4.2 ms and picks sensible axis labels. A graph without a unit cannot be read under pressure.
  • Start ratio and count axes at zero, and fix the range of percentages to 0 to 100. An automatically scaled axis makes a change from 0.01% to 0.02% look like a catastrophe.
  • Draw the SLO threshold on the graph, as a line or a shaded region, so that "is this bad?" needs no mental arithmetic.
  • Use colour for meaning, not decoration. Reserve red for bad. Keep the same colour for the same series on every panel. Do not rely on red and green alone, since a meaningful share of people cannot tell them apart.
  • Limit the number of series. A panel with sixty lines conveys nothing. Show the top five with topk, or aggregate, and put the detail on a drill-down.
  • Prefer percentiles or a heatmap to averages for latency, and show p50 alongside p99, because the gap between them is itself informative.
  • Use a shared crosshair across panels, so that hovering over a spike in errors shows the same moment on the latency and CPU graphs.
Watch out

Be careful with stacked graphs. Stacking makes sense for parts of a whole, such as requests by status code. Stacking latency percentiles or unrelated series produces a picture in which the top line means nothing.

Dashboards as code

Dashboards edited by hand in the UI share the problems of infrastructure changed by hand in a console: no review, no history, no way to recreate them, and differences between environments that nobody can explain. A dashboard is a JSON document, and Grafana can load data sources and dashboards from files at start-up. This is provisioning.

provisioning/datasources/prometheus.yml
yaml
apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
provisioning/dashboards/default.yml
yaml
apiVersion: 1
providers:
  - name: services
    folder: Services
    type: file
    allowUiUpdates: false
    options:
      path: /var/lib/grafana/dashboards
  • Keep the dashboard JSON in Git, review changes in pull requests, and let the pipeline deliver them. Design in the UI if you like, then export the JSON and commit it.
  • With allowUiUpdates: false, changes made in the UI cannot be saved over the provisioned version, so Git remains the source of truth, which is the GitOps idea applied to dashboards.
  • On Kubernetes, the kube-prometheus-stack chart runs a sidecar that loads any ConfigMap carrying a specific label as a dashboard, so a service's dashboard can ship in the same Helm chart as the service.
  • For large estates, generate dashboards from code with Grafonnet, the Grafana Foundation SDK or Terraform's Grafana provider, so that every service gets the same standard layout.
  • Refer to data sources by a variable or a stable name, not by an internal ID, so that the same JSON works in every environment.

Manage sprawl deliberately. Use folders and tags, give each dashboard an owner, and delete what is unused. Grafana's usage statistics show which dashboards nobody has opened in months. Ten dashboards that people trust are worth more than three hundred that nobody can find their way around.

Dashboards are not alerts

A dashboard only works when someone is looking at it, and nobody watches graphs at three in the morning. Anything that needs a human response must be an alert. A dashboard is what the human opens after the alert, to understand and diagnose. If your team finds problems by noticing them on a wall-mounted screen, the alerting has a gap.

Grafana has its own alerting engine, which can evaluate queries against any data source and route notifications. Whether you use it or Prometheus with Alertmanager matters less than keeping the rules in version control and applying the principles of the next module: alert on symptoms that users feel, tie alerts to SLOs, and make every page actionable.

Grafana is also not limited to metrics. The same dashboard can query logs in Loki or Elasticsearch, traces in Tempo or Jaeger, and SQL databases. Placing a log panel beneath the error graph, filtered by the same variables, saves a great deal of switching between tools during an incident.

Hands-on practice

Build a service dashboard that answers the five questions

  1. Run Grafana next to your Prometheus from the previous module, for example with Docker Compose. Add Prometheus as a data source through a provisioning file, not through the UI.
  2. Write down the five on-call questions for your instrumented service, then sketch the row layout on paper before touching Grafana.
  3. Build the RED row: request rate, error ratio and latency percentiles, using $__rate_interval. Set units, start axes at zero, and draw your SLO threshold on the error and latency panels.
  4. Add a job query variable and a multi-value route variable. Confirm that the same dashboard works for a second service by changing only the drop-down.
  5. Add a latency heatmap from the histogram buckets. Generate a mix of fast and slow requests, and compare what the heatmap shows with what the p99 line shows.
  6. Post a deploy annotation with the curl command, generate errors just after it, and confirm the vertical line appears on every panel.
  7. Export the dashboard JSON, commit it, and provision it from a file with allowUiUpdates: false. Try to save a change in the UI and observe what happens.
  8. Ask a colleague who does not know the service to look at the dashboard for ten seconds and tell you whether it is healthy. Revise the top row until they can.
Cheat sheet

Dashboards with Grafana — at a glance

Main things to focus on

  • Design from the on-call engineer's questions: is it broken, what do users see, where, why, what changed.
  • Top row readable in five seconds; detail below; one question per panel, stated in the title.
  • RED (rate, errors, duration) for every service first; USE (utilisation, saturation, errors) for every resource below.
  • Always set units, start ratio axes at zero, and draw the SLO threshold on the graph.
  • Percentiles or a heatmap for latency, never just an average. Few series per panel.
  • Use $__rate_interval in dashboard rate() queries, and =~ with multi-value variables.
  • Annotate every deploy. Link alerts to dashboards and dashboards to drill-downs.
  • Dashboards live in Git and are provisioned from files. A dashboard is not an alert.

Standard service layout

Row 1: HealthSLO compliance, budget remaining, burn rate
Row 2: REDRate, error ratio, latency p50/p95/p99
Row 3: BreakdownBy route, instance, zone, version
Row 4: DependenciesCalls to database, cache and downstream services
Row 5: Resources (USE)CPU, throttling, memory against limit, restarts, pools
Row 6: ChangesDeploy annotations, replica count

Grafana variables in queries

$__rate_intervalSafe window for rate(); at least 4 scrape intervals, grows with zoom
$__intervalTime between data points at the current zoom
$__rangeThe whole selected time range, e.g. for totals in a stat panel
label_values(METRIC, LABEL)Query variable: all values of a label
label_values(METRIC{job="$job"}, route)Chained variable, filtered by another
{route=~"$route"}Regex match, required for multi-value and All

Choosing a panel

Time seriesAnything that changes over time
Stat / GaugeOne current number with thresholds
HeatmapLatency distribution from histogram buckets
Bar gauge / TableRanked comparison across many items
State timelineDiscrete states per instance over time
LogsLog lines filtered by the same variables

As code

provisioning/datasources/*.ymlData sources loaded at start-up
provisioning/dashboards/*.ymlProviders that load dashboard JSON from a path
allowUiUpdates: falseUI edits cannot overwrite the provisioned dashboard
POST /api/annotationsCreate an annotation, e.g. from a deploy pipeline
Dashboard JSON in GitReviewed, versioned, reproducible
ConfigMap + sidecar labelShip a dashboard with the service's Helm chart

Common pitfalls

  • Building the dashboard from the list of available metrics instead of from the questions it must answer.
  • Graphing average latency, which looks healthy while a tenth of users wait ten seconds.
  • Leaving axes on automatic scaling, so that noise looks like an incident.
  • Cloning a dashboard for each service instead of using variables, leaving dozens of inconsistent copies.
  • Editing dashboards only in the UI, so they cannot be reviewed, recovered or reproduced.
  • Relying on someone noticing a graph instead of having an alert.
Quiz

Check your understanding

5 questions · 4 to pass · answers are explained as you go. Your best score is saved on this device only.

Progress and quiz scores are saved in this browser only. Back up or restore on the hub.

Was this lesson useful? Tell me what to improve →