Command Palette

Search for a command to run...

Hectal
Case 5.4·Production ObservabilitySEV3

“During the outage, three engineers opened three different 'ShopLite' dashboards. Two were broken, and the third used the old metric names.”

case name: 214 dashboards and none of them right

Service
grafana
Impact
15 minutes of the incident spent finding and fixing dashboards
Detected by
Incident review
Time to resolve
A sprint of cleanup

Skills you'll use on this case

dashboard design (RED, SLO, saturation)Grafana provisioningdashboards as JSON in GitTerraform's Grafana provider

01 The investigation

  1. Incident

    DEAD END

    'ShopLite Overview', 'ShopLite Overview (copy)', 'ShopLite NEW', 'priya-test-shoplite'…

    Dashboards created by clicking accumulate like console-created infrastructure did in the Terraform course: no owner, no review, and nothing notices when the metrics they query are renamed.

  2. Review

    RESOLVED

    One provisioned, version-controlled dashboard per service; the rest archived

Root cause

Dashboards were created and edited by hand in the UI, with no source control, ownership, or review. Copies drifted, queries broke silently after metric renames, and nobody knew which one was authoritative.

02 The concepts behind it

A dashboard answers a question, top to bottom

A service dashboard should read like triage. Row 1: are we meeting our SLOs (SLI, budget remaining, burn rate)? Row 2: RED per route (rate, error ratio, p50/p99). Row 3: saturation (CPU vs limit, memory vs limit, event-loop lag, DB pool). Row 4: dependencies (payments latency and errors, DB query time). Each panel links onward: to logs filtered to the service, to traces via exemplars, and to the runbook.

Use template VARIABLES ($service, $route) so one dashboard serves every service with the same RED metrics, which is the payoff of consistent metric names since Case 0.1.

Dashboards as code

Treat dashboards like any other configuration: JSON (or Jsonnet/Grafonnet, or the Terraform grafana provider) in Git, reviewed in pull requests, deployed automatically. Grafana's file PROVISIONING loads dashboards from a directory at startup; UI edits then can't silently diverge, and a changed metric name shows up in code review alongside the change that renamed it.

03 The fix

  1. 01Provision dashboards from files

    A provider tells Grafana to load every JSON file in a folder. Export a dashboard you like from the UI (Share → Export → 'Export for sharing externally' off), save it in the repo, and delete the UI copy.

    obs-lab/grafana/provisioning/dashboards/dashboards.ymlwhole fileyaml
    apiVersion: 1
    providers:
      - name: services
        folder: Services
        type: file
        allowUiUpdates: false        # the file is the source of truth
        options:
          path: /etc/grafana/provisioning/dashboards/json
  2. 02The ShopLite service dashboard (abbreviated)

    A service variable driven by the job label, and SLO, RED, and saturation rows using the queries from this course. Put the full JSON in dashboards/json/service.json.

    obs-lab/grafana/provisioning/dashboards/json/service.jsonwhole filejson
    {
      "title": "Service overview",
      "uid": "service-overview",
      "templating": {
        "list": [{
          "name": "service", "type": "query", "datasource": { "uid": "prometheus" },
          "query": "label_values(http_request_duration_seconds_count, job)"
        }]
      },
      "panels": [
        { "type": "row", "title": "SLO" },
        { "type": "stat", "title": "Error budget remaining (28d)",
          "targets": [{ "expr": "1 - (sum(increase(http_request_duration_seconds_count{job=\"$service\", status=~\"5..\"}[28d])) / sum(increase(http_request_duration_seconds_count{job=\"$service\"}[28d]))) / 0.005" }] },
        { "type": "row", "title": "RED" },
        { "type": "timeseries", "title": "Requests/s by route",
          "targets": [{ "expr": "sum by (route) (rate(http_request_duration_seconds_count{job=\"$service\"}[$__rate_interval]))" }] },
        { "type": "timeseries", "title": "Error ratio by route",
          "targets": [{ "expr": "sum by (route) (rate(http_request_duration_seconds_count{job=\"$service\", status=~\"5..\"}[$__rate_interval])) / sum by (route) (rate(http_request_duration_seconds_count{job=\"$service\"}[$__rate_interval]))" }] },
        { "type": "timeseries", "title": "p99 latency by route",
          "targets": [{ "expr": "histogram_quantile(0.99, sum by (le, route) (rate(http_request_duration_seconds_bucket{job=\"$service\"}[$__rate_interval])))" }] },
        { "type": "row", "title": "Saturation" },
        { "type": "timeseries", "title": "Event-loop lag p99",
          "targets": [{ "expr": "nodejs_eventloop_lag_p99_seconds{job=\"$service\"}" }] }
      ]
    }
  3. 03Or manage Grafana with Terraform

    If your team already uses Terraform (as ShopLite does), the grafana/grafana provider manages dashboards, folders, data sources, and alert rules alongside the infrastructure they observe, reviewed in the same pull requests.

    infra/grafana.tfwhole filehcl
    terraform {
      required_providers {
        grafana = { source = "grafana/grafana", version = "~> 4.0" }
      }
    }
    
    provider "grafana" {
      url  = "http://localhost:3001"
      auth = var.grafana_token
    }
    
    resource "grafana_folder" "services" {
      title = "Services"
    }
    
    resource "grafana_dashboard" "service_overview" {
      folder      = grafana_folder.services.uid
      config_json = file("${path.module}/dashboards/service.json")
    }

04 Make sure it never surprises you again

  1. 01Close the case — tear the lab down

    That's the course. -v also removes the volumes (Prometheus, Loki, Tempo, and Mimir data). Keep the obs-lab/ folder: everything you've built is in its files, and docker compose up -d brings it all back.

    terminal
    $ cd obs-lab && docker compose down -v
    ── expected output ──
    ✔ Container obs-lab-k6-1 Removed
    ✔ Container shoplite Removed
    ...
    ✔ Network obs-lab_default Removed

05 Your turn: write the query

Run each one against your lab before revealing the answer. Share your own version in the comments; there's usually more than one correct query.

01

Add a panel query to the service dashboard showing how many restarts each service had in the selected time range.

02

How would you make a dashboard link from a latency spike directly to example traces?

06 Interview questions from this case

01

What makes a good service dashboard?

02

Why manage dashboards as code?

0/4 · 0%