Grafana

0/0 checks

1. Architecture & Data Sources

Grafana is a visualization layer that queries data sources and renders panels. It does not store metrics — it proxies queries to backends.

flowchart LR
    subgraph Sources
        PR["Prometheus<br/>metrics"]
        LK["Loki<br/>logs"]
        TP["Tempo<br/>traces"]
        PG["PostgreSQL<br/>relational"]
    end
    subgraph Grafana
        DS["Data Source<br/>plugins"] --> QE[Query Engine]
        QE --> PN[Panel Renderer]
    end
    Sources --> DS
    PN --> DB[Dashboard]
Data Source Query Language Best For
Prometheus PromQL Metrics, counters, gauges
Loki LogQL Log streams, structured logs
Tempo TraceQL Distributed traces, spans
PostgreSQL SQL Business data, audit logs

A panel doesn't talk to a backend directly — every query takes the same path from the panel editor to pixels on screen:

1. A panel needs data. Its query editor holds a query in the data source's own language — PromQL, LogQL, TraceQL, or SQL — plus a reference to which data source should run it.
2. The right plugin picks it up. Grafana routes the query to the matching Data Source plugin for that backend — it never talks to Prometheus, Loki, Tempo, or PostgreSQL directly itself.
3. The Query Engine executes it. The plugin hands the translated request to the Query Engine, which sends it over the network to the backend and waits on the response.
4. The Panel Renderer draws it. Returned rows or time series get converted into the panel's visual encoding — lines, bars, table cells, gauge needles — and painted onto the dashboard.

Does Grafana store the metrics, logs, and traces it displays?

2. Panel Types

Panel Use When
Time series Metrics over time (latency, RPS, CPU)
Stat Single current value with threshold color
Gauge Value within a min/max range (SLO burn)
Table Multi-dimensional comparison, top-N
Heatmap Latency distribution over time
Logs Log stream output from Loki

Picking the right panel:

  • Trending over time → Time series
  • "Is it OK right now?" → Stat or Gauge
  • "Which pods are slowest?" → Table
  • "Where are latency outliers?" → Heatmap
  • "What did the app log?" → Logs

You need to see which of 50 pods has the highest P99 latency right now. Why is a table a better fit here than a time series panel?

3. Variables

Variables make dashboards reusable across environments, clusters, and services.

Dashboard URL: /d/abc?var-cluster=prod&var-namespace=payments

Query variable — populated from a data source:

# Variable: cluster
# Query (Prometheus label_values):
label_values(kube_node_info, cluster)

Custom variable — static list:

name: env
values: dev,staging,prod

Interval variable — for $__interval in rate() calls:

name: interval
values: 1m,5m,10m,30m
auto: true

Use in panels:

rate(http_requests_total{cluster="$cluster", namespace="$namespace"}[$interval])
Populated from a data source. A label_values() query (or equivalent) asks the data source itself for the current list of values — new clusters or namespaces show up automatically as they appear, no dashboard edit required.
A static, hand-typed list. Values like dev,staging,prod are fixed in the variable definition. Simple and predictable, but adding a new environment means editing the dashboard.
A list of durations bound to $__interval. Panels reference it inside rate() calls, so one dashboard can be viewed at a coarse 30m resolution or a fine 1m resolution without editing a single query.

A dashboard hardcodes rate(http_requests_total[5m]) in every panel instead of using the interval variable. What's lost?

4. USE Method Dashboard

Per resource (CPU, memory, disk, network):

Row Metric PromQL sketch
Utilization % busy rate(node_cpu_seconds_total{mode!="idle"}[5m])
Saturation Run-queue / pressure node_pressure_cpu_waiting_seconds_total
Errors Hardware / kernel errors node_disk_io_time_seconds_total

Layout (4 rows × 3 panels):

[CPU Util]  [CPU Saturation]  [CPU Errors]
[Mem Util]  [Mem Saturation]  [OOM Kills ]
[Disk Util] [Disk Saturation] [Disk Errors]
[Net Util]  [Net Saturation]  [Net Errors ]

Each panel uses $node variable to filter by host.

In the USE method layout, the Memory row's third column is "OOM Kills" instead of a generic error metric. Why does that still count as the row's Errors column?

5. RED Method Dashboard

Per service/endpoint:

Panel Metric PromQL sketch
Rate Requests/sec sum(rate(http_requests_total[$interval])) by (service)
Errors Error rate % sum(rate(http_requests_total{status=~"5.."}[$interval])) / sum(rate(http_requests_total[$interval]))
Duration P50/P95/P99 latency histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[$interval])) by (le, service))

Layout:

[RPS - all services (time series)]
[Error Rate % (time series)]        [Top errors by service (table)]
[P50 latency] [P95 latency] [P99 latency]

In the RED method, which single panel type is used to show P50, P95, and P99 together, and via which PromQL function?

6. SLO Dashboard

SLO: 99.9% availability over 30-day rolling window
Error budget: 0.1% = ~43 minutes/month
Panel Formula
Availability % 1 - (errors / total) over 30d
Error budget remaining budget_total - errors_consumed
Burn rate (1h) error_rate_1h / (1 - SLO_target)
Burn rate (6h) same, 6h window
Budget exhaustion forecast linear projection

Burn rate thresholds (Google SRE):

Window Burn rate Action
1h > 14x Page immediately
6h > 6x Page
3d > 1x Ticket
Burn rate > 14x for a full hour. At that rate the entire 30-day error budget would be gone in about two days — page immediately.
Burn rate > 6x sustained over 6 hours. Slower-moving than the 1h check, but still fast enough to page.
Burn rate > 1x over 3 days — burning budget at roughly the SLO's own baseline rate. Worth a ticket to investigate, not an immediate page.
# 1-hour burn rate
(
  sum(rate(http_requests_total{status=~"5.."}[1h]))
  /
  sum(rate(http_requests_total[1h]))
) / (1 - 0.999)

A 99.9% availability SLO over a 30-day window gives roughly how much error budget, in minutes?

7. Provisioning Dashboards as Code

flowchart LR
    GH["Git repo<br/>dashboard JSON"] -->|CI push| CM[K8s ConfigMap]
    CM -->|volume mount| SC["Grafana sidecar<br/>container"]
    SC -->|watches /dashboards| GF["Grafana<br/>auto-reloads"]
1. Dashboard JSON lives in git. Dashboards are exported as JSON and committed to a repo, not clicked together directly in the Grafana UI.
2. CI pushes it into a ConfigMap. A pipeline step packages each dashboard JSON file as a key inside a Kubernetes ConfigMap carrying the label grafana_dashboard: "1".
3. The sidecar is watching for that label. Grafana's sidecar container watches every ConfigMap with that label — across the whole cluster if searchNamespace: ALL is set.
4. Grafana hot-reloads, no restart. The sidecar mounts the new file into the pod and Grafana picks up the change automatically.

ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: grafana-dashboards
  labels:
    grafana_dashboard: "1"   # sidecar watches this label
data:
  red-dashboard.json: |
    { "title": "RED Dashboard", ... }

Grafana Helm values:

grafana:
  sidecar:
    dashboards:
      enabled: true
      label: grafana_dashboard
      searchNamespace: ALL
  datasources:
    datasources.yaml:
      apiVersion: 1
      datasources:
        - name: Prometheus
          type: prometheus
          url: http://prometheus-server:9090
          isDefault: true
        - name: Loki
          type: loki
          url: http://loki:3100

Grafana sidecar watches all ConfigMaps with grafana_dashboard: "1" and hot-reloads dashboards without restart.

What ConfigMap label does the Grafana sidecar watch for to know a ConfigMap contains a dashboard to load?

8. Alerting

Grafana-native alerting (Grafana 9+) replaces the old panel-level alerts.

Components:

Component Role
Alert rule PromQL/LogQL condition with for: duration
Contact point Destination (Slack, PagerDuty, email, webhook)
Notification policy Routes alerts to contact points by labels
Silence Mutes matching alerts for a time range
A PromQL/LogQL condition plus a for: duration. The condition has to stay breached for the whole for: window before the rule actually fires — a single noisy sample doesn't page anyone.
The destination a firing alert gets sent to — Slack, PagerDuty, email, or a generic webhook.
The routing table. It matches a firing alert's labels (like severity = critical) and decides which contact point actually receives it.
A temporary mute for alerts matching given labels, for a fixed time range — for planned maintenance, without touching the underlying rule.

Contact point (Slack):

# provisioning/alerting/contact-points.yaml
apiVersion: 1
contactPoints:
  - orgId: 1
    name: slack-oncall
    receivers:
      - uid: slack-oncall-uid
        type: slack
        settings:
          url: 'https://hooks.slack.com/services/XXX/YYY/ZZZ'
          recipient: '#alerts'
          title: '{{ .CommonLabels.alertname }}'

Notification policy:

# provisioning/alerting/notification-policies.yaml
apiVersion: 1
policies:
  - orgId: 1
    receiver: slack-oncall
    group_by: [alertname, cluster]
    routes:
      - receiver: pagerduty-critical
        matchers:
          - severity = critical

Alert rule (provisioning):

apiVersion: 1
groups:
  - orgId: 1
    name: RED Alerts
    folder: SRE
    interval: 1m
    rules:
      - title: High Error Rate
        condition: C
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Error rate above 1% for {{ $labels.service }}"
        data:
          - refId: A
            datasourceUid: prometheus
            model:
              expr: sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
          - refId: C
            datasourceUid: __expr__
            model:
              type: threshold
              conditions:
                - evaluator: { params: [0.01], type: gt }
                  query: { params: [A] }

A rule evaluating true once doesn't fire immediately — the for: duration exists precisely to filter out single noisy samples:

flowchart LR
    EV["Rule evaluates<br/>every interval"] -->|condition breached| PD["Pending<br/>for-duration timer starts"]
    PD -->|condition clears early| OK[Normal]
    PD -->|still breached after<br/>full for-duration| FR[Firing]
    FR --> NP["Notification policy<br/>matches alert labels"]
    NP --> CP["Contact point<br/>Slack, PagerDuty, email"]
    FR -->|condition clears| OK
1. Normal. The rule evaluates its query on every interval tick and the condition isn't breached.
2. Pending. The condition breaches threshold. The for: timer starts — the alert doesn't fire yet, it just starts the clock.
3. Firing. The condition is still breached once the full for: duration has elapsed. Only now does the rule actually fire.
4. Routed and delivered. The firing alert's labels are matched against the notification policy tree, which hands it to the matching contact point.

An alert rule has for: 5m. The condition breaches, then clears again after 3 minutes. Does the alert fire?