Grafana Stack Operational Guide
Concise operational pointers for deep Grafana / Prometheus / Mimir / Loki / Tempo / Alertmanager troubleshooting.
Assumes you already know what dashboards, metrics, logs, and traces are, and can write a basic PromQL/LogQL query. This skill covers the operational layer — the parts models tend to gloss over: cardinality cost, rule semantics, alert lifecycle, query-language footguns, provisioning UID stability, and the cross-component contract.
When to use
Load when the question is about:
- Cardinality bombs (Prometheus/Mimir/Loki) and how to detect them
- PromQL correctness around
rate/irate/increase/histogram_quantile/vector matching
- Recording-rule vs alerting-rule semantics, eval intervals, naming
- Alert lifecycle (Inactive→Pending→Firing→Recovering),
for:, keep_firing_for:
- Alertmanager grouping (
group_wait / group_interval / repeat_interval) and inhibition
- LogQL filter ordering and structured metadata in Loki 3.x
- Dashboard / datasource provisioning and UID stability
- Variable scoping (
$__interval vs $__rate_interval, formatting modifiers)
- Exemplars wiring metrics → traces, TraceQL service-graph metrics
- Native histograms (v2.40+ experimental, v3.8+ stable)
Do NOT load for: panel construction, picking a panel type, "how do I plot X", first-time install, basic PromQL syntax tutorials.
Cardinality cost model
The single biggest operational footgun across Prometheus, Mimir, and Loki. Every unique label-set is a stored time-series; cost = series_count × samples_per_series.
- Per-metric guideline: keep cardinality below 10 per metric; investigate alternatives above 100. The vast majority of metrics should have no labels.
- Common bombs (do NOT label by these):
user_id, email, request_id/trace_id, status text, raw URL paths, K8s pod names with random suffixes, container IDs.
- Detect top metrics by series count:
topk(20, count by (__name__)({__name__=~".+"}))
- Detect top labels for a hot metric:
topk(20, count by (label_name)(metric_name))
- TSDB internals to watch:
prometheus_tsdb_head_series (current active), prometheus_tsdb_symbol_table_size_bytes (label string interning), scrape_series_added per target.
- Mimir per-tenant limits:
max_global_series_per_user (active series across ingesters; flag -ingester.max-global-series-per-user), max_global_series_per_metric, ingestion_rate, ingestion_burst_size. Excess is rejected as 4xx client error (data discarded, not retried). Per-ingester hard cut-off ≈ 2.5M series default.
- Loki cardinality: each unique stream-label combo = one log stream. Multiplicative — 3 status × 5 actions × 3 endpoints = 45 streams. Static labels only (
app, namespace, env); push high-cardinality fields to structured metadata (Loki 3.0+, schema v13, chunk format v4) — queryable as {job="x"} | pod="..." without index cost.
PromQL gotchas
rate() minimum window: needs ≥ 4 samples in the range vector for stability; with default 15s scrape, that's a 1-minute floor — but you almost always want longer (5m+). Counter resets are handled inside rate/increase.
$__rate_interval formula (Grafana 7.2+): max($__interval + scrape_interval, 4 * scrape_interval). Always ≥ 4× scrape. Use this in dashboards, not $__interval — $__interval alone can drop below the 4× floor on zoom-in and produce empty graphs or undercounted rates.
irate(): only the last two samples; great for short bursts/debugging, unstable for long ranges or alerts because it's noisy and non-monotonic.
increase() = rate() × seconds_in_range. Same minimum-window requirement. Prefer rate() in recording rules.
histogram_quantile() (classic histograms):
- Must group by the
le label: histogram_quantile(0.99, sum by (le, job)(rate(http_request_duration_seconds_bucket[5m]))).
le values are string-encoded floats — "0.5" and "0.50" are different label values and won't aggregate.
- Returns
NaN if too few buckets; +Inf for φ > 1, -Inf for φ < 0.
- Quantile error margin = bucket width. A 95th percentile of 220 ms with buckets at 0.2 / 0.3 estimates as ~295 ms — silent 75 ms error.
- Buckets must align across instances or aggregation silently drops series.
- Native histograms (v2.40 experimental via
--enable-feature=native-histograms; v3.8+ stable; v3.9 flag becomes no-op): exponential bucketing, single composite sample, ad-hoc quantile, aggregation across instances. Prefer these — switches scrape exposition to protobuf.
quantile_over_time(φ, scalar[5m]) vs histogram_quantile(φ, …_bucket): completely different. The former aggregates raw scalars across time; the latter interpolates over a histogram's bucket boundaries.
@ modifier (introduced 2.25 experimental, stable in 2.33): pin evaluation timestamp — metric @ 1609746000 or @ start() / @ end(). Required to make topk() stable across a range query (otherwise re-evaluates per step and series flicker in/out).
offset 1w: shift evaluation back; combine with @ to compare current-vs-historical at fixed boundaries.
topk / bottomk: evaluated independently per timestamp — series enter and leave the result set across a range. Use @ to pin.
- Vector matching:
on(labels) restricts match keys; ignoring(labels) excludes. group_left(x) / group_right(x) for many-to-one / one-to-many — names the side with higher cardinality. Default is 1:1 and fails the query on cardinality mismatch.
sum without (instance) ≠ sum by (job): without drops named labels, keeps everything else (including new labels added later); by keeps only named labels. A new label appearing in scrape config will silently break dashboards built with without but not those built with by (and vice versa, depending on intent). Pick deliberately.
Recording rules
- Naming:
level:metric:operations. Level = remaining aggregation labels (e.g. job, instance_path); metric = base name unchanged (drop _total only when applying rate); operations = transformations newest-first. Example: job:http_requests:rate5m.
- Aggregation strategy: when computing ratios, aggregate numerator and denominator separately, then divide. Never average ratios.
- Group eval interval ≤ scrape interval, otherwise rules see stale samples. Default =
global.evaluation_interval.
- Group cannot finish before next cycle? The next eval is skipped and
rule_group_iterations_missed_total increments — silent gaps in recorded series.
for: does NOT exist in recording rules — alerting-only.
- Replacing a query in a recording rule that's also referenced by an alert: alerts must be updated together. Otherwise the alert continues using the old (cached) query against the now-different output series.
Alerting rules and state lifecycle
- Prometheus states: Inactive → Pending (during
for:) → Firing → (Inactive on resolve, after keep_firing_for: if set).
- Grafana-managed states add: NoData (query returned 0 series), Error (query/eval failure), Recovering (firing → normal during
keep_firing_for: window). NoData/Error are Grafana-only — Prometheus-managed alerts simply don't fire.
for: 5m is a smoothing window: condition must be true on every evaluation for 5 minutes before firing. An alert that flaps inside that window never fires — convenient for noise, dangerous if the underlying issue is genuinely intermittent.
keep_firing_for: (Prometheus 2.42, Feb 2023): keep firing for N after the condition clears. Defaults to 0 (off). Use to dampen flapping resolves; default behaviour deactivates immediately after first eval where condition not met.
labels: vs annotations:: labels are indexed in Prom, used for routing/dedup; annotations are unindexed, free-form (summary, description, runbook_url, dashboard_url). Don't put high-cardinality strings in labels — they multiply your alert series.
- Alert dedup by label set: identical
{alertname, ...labels} are the same alert. Adding a high-cardinality label (e.g. instance per pod) creates one alert per pod.
Alertmanager grouping and inhibition
Defaults (override per route):
group_wait: 30s — initial buffer for a new group, lets sibling alerts coalesce into the first notification.
group_interval: 5m — wait between notifications when new alerts join an already-notified group.
repeat_interval: 4h — re-notify the same alert after this if still firing.
group_by: [alertname, cluster] — alerts sharing these labels coalesce into one notification. Add too many labels → no grouping; too few → unrelated alerts merged.
- Inhibition rules: when a "source" alert fires, suppress matching "target" alerts. Classic use:
severity=critical on a node down inhibits severity=warning for individual pods on that node. Requires shared equal: labels.
- Silences: time-bounded label-matcher mute, separate from inhibition. Good for maintenance windows.
LogQL gotchas (Loki)
Filter ordering matters for performance — Loki applies in this order, so cheap filters first:
- Stream selector
{app="api", env="prod"} — only indexed filter, narrows reading from object storage.
- Line filters
|= "error" (substring, fastest), |~ "regex", !=, !~ — applied to raw line bytes before parsing.
- Parsers
| json, | logfmt, | pattern "<ip> - <user>", | regexp — pattern is much cheaper than regexp.
- Label filters on parsed labels
| status_code = 500.
- Metric extraction
| unwrap latency for log-derived metrics: sum(rate({...} | json | unwrap bytes [1m])).
- Anti-pattern:
{app=~".+"} |= "error" — full-tenant scan. Always pin at least one indexed label.
- Loki 2.x cardinality cost: putting
pod or request_id in stream labels exploded the index. Loki 3.0+ structured metadata is the fix — query as {app="x"} | pod="...", no index growth.
Dashboard variables
- Types: query, custom, interval, datasource, textbox, constant, filters, switch.
- Time variables:
$__from / $__to are epoch ms by default. ${__from:date:seconds} for Unix seconds, ${__from:date:YYYY-MM-DD} for custom.
$__interval: (to - from) / panel_resolution_pixels. Used for group by (time) style binning. Do NOT use in rate() — can fall under 4×scrape on zoom.
$__rate_interval: max($__interval + scrape_interval, 4 * scrape_interval). Always use this for rate/increase in PromQL.
- Multi-value formatting:
${var} (default per datasource) — Prometheus emits regex (a|b|c), requires =~ matcher in the query.
${var:csv} → a,b,c
${var:pipe} → a|b|c
${var:regex} → escaped for regex
${var:json} → JSON array
- Include All: defaults to a literal
.+ regex unless "Custom all value" overrides. Beware: .+ against =~ matcher matches every series, not nothing.
- Variable interpolation order: query variables refresh on time-range change only if "Refresh: On Time Range Change" is set; otherwise stale on dashboard load — cause of mysterious "missing" series.
Provisioning and UID stability
- Dashboard provisioning (
provisioning/dashboards/*.yaml):providers:
- name: 'team-x'
folder: 'Team X'
folderUid: 'team-x-folder' # pin
type: file
updateIntervalSeconds: 10
allowUiUpdates: false
options:
path: /var/lib/grafana/dashboards
foldersFromFilesStructure: true
- The
uid inside the dashboard JSON is the stable identity. If absent, Grafana generates a new one on every reprovision → external links break, alert links break, history splits.
allowUiUpdates: false keeps file as source of truth; UI edits are reverted on next sync.
- Datasource provisioning (
provisioning/datasources/*.yaml): always set uid: explicitly. Without it, the UID is randomly generated per instance — the same dashboard JSON imported into staging vs prod references different datasource UIDs and panels show "Datasource not found".datasources:
- name: Prometheus
uid: prom-prod # pin or pay later
type: prometheus
url: http://prom:9090
version: 1
prune: true removes provisioned datasources when files are deleted; version controls precedence across instances.
- Mixin pattern (
monitoring-mixin, kube-prometheus-stack, prometheus-operator helm chart): jsonnet libraries shipping dashboards + alerts as code. Lets you template datasource and cluster labels at render time.
Prometheus Operator / kube-prometheus-stack
CRDs you'll touch:
ServiceMonitor discovers via Service+EndpointSlice; PodMonitor discovers Pods directly (use when there's no Service).
Probe for blackbox-style external endpoints; ScrapeConfig for arbitrary configs that don't fit ServiceMonitor.
PrometheusRule holds recording + alerting rules; reconciled and hot-loaded — no Prometheus restart.
AlertmanagerConfig for routing/inhibition split per namespace.
- Resource selectors: empty
{} = match all; null/unset = match none. Wrong selector = silent zero-targets.
Exemplars and traces
- Exemplars link a metric data point to a trace ID. Requires
--enable-feature=exemplar-storage on Prometheus (fixed circular buffer in memory; persisted to WAL).
- Instrumentation must emit them (Go
prometheus/client_golang ObserveWithExemplar; Java io.prometheus.client.exemplars).
- Grafana Prometheus datasource: enable "Exemplars" in datasource settings, link to a Tempo datasource by UID.
- TraceQL (Tempo):
{ resource.service.name = "checkout" && duration > 1s && status = error }. Three attribute scopes: intrinsic (name, duration, status), span.* (per-span), resource.* (per-process). Wrong scope returns zero results without error.
- Service graph metrics (Tempo metrics-generator): emits
traces_service_graph_request_total, traces_service_graph_request_failed_total, traces_service_graph_request_server_seconds_* as Prometheus metrics. Cardinality = (caller × callee) pairs — bounded by service count, not trace count.
- Span metrics (similar generator): emits RED metrics from spans (
traces_spanmetrics_calls_total, traces_spanmetrics_latency_*). Cardinality watch: span.name and http.route should be normalized; raw URLs explode it.
Datasource considerations
Resolution / Min interval / Min step override $__interval and feed the step parameter to PromQL query_range. Smaller step = more points = potentially blown query timeout.
maxDataPoints in panel — Grafana downsamples by adjusting step. If your alert query uses a fixed range and the dashboard query uses dynamic step, results legitimately differ.
- Per-query timeout (datasource setting): default 60 s; long-range high-cardinality
histogram_quantile queries hit this.
Common pitfalls (consolidated)
sum without (instance) vs sum by (job) — drops named vs keeps named. New scrape labels silently flip results.
- Alert reused across envs via
for: — copying production thresholds to staging without adjusting for: causes noisy paging on lower-traffic environments.
- Recording-rule renamed series, alert still references old name → silent eval against absent metric (alert never fires; not the same as a "passing" alert).
- Dashboard imported via UI without UID pin → external bookmarks/email links break on reimport.
- Datasource UID drift between instances → dashboard panels say "Datasource not found"; pin in provisioning YAML.
irate() in alerts → flapping. Use rate() with a longer window.
- High-cardinality label in alert
labels: (e.g. per-pod) → 1 alert per pod, paging storm.
$__interval in rate(metric[$__interval]) on dashboards → empty graphs on zoom-in.
- Annotations are unindexed — don't expect to filter or route on annotation contents.
topk in a range query without @ end() → series flicker; viewer assumes stability that isn't there.
Authoritative references
Prometheus (prometheus.io/docs):
Grafana:
Loki:
Mimir:
Tempo:
Alertmanager:
prometheus-operator / kube-prometheus-stack:
Reliable authors: Tom Wilkie, Bryan Boreham, Cyril Tovena (Grafana Labs blog); Björn Rabenstein, Julius Volz, Richard Hartmann (Prometheus core).
Guardrails
Before recommending a non-trivial operational change (cardinality cap, for:/keep_firing_for:, scrape interval, $__rate_interval rollout, Mimir limit override):
- Quote the specific parameter / function name and its default.
- Cite the upstream doc (
prometheus.io/docs, grafana.com/docs/{loki,mimir,tempo}).
- Make the recommendation conditional on observed metrics —
prometheus_tsdb_head_series, cortex_limits_overrides, rule_group_iterations_missed_total, alert flap rate — never blanket-tune.
- Name a rollback: "if X spikes, revert by Y."
Tuning without measurement is worse than defaults. Cardinality, alert thresholds, and rate windows are environment-specific — what works for a 100-pod cluster will page non-stop on 10,000.
1---2name: grafana3description: Deep Grafana stack operational intuition — Prometheus/Mimir cardinality cost, PromQL/LogQL pitfalls, alert lifecycle, provisioning + UID stability, histogram math, Tempo TraceQL, Alertmanager grouping. Load when diagnosing cardinality bombs, alert flapping, provisioning at scale, quantile correctness, or trace-to-metric correlation. Skip for panel building, basic PromQL, install, or first-time dashboards. Triggers on: "cardinality bomb", "rate vs irate", "histogram_quantile", "recording rule", "keep_firing_for", "loki cardinality", "max_global_series_per_user", "traceql", "group_wait", "group_left", "native histograms".4---56# Grafana Stack Operational Guide78Concise operational pointers for deep Grafana / Prometheus / Mimir / Loki / Tempo / Alertmanager troubleshooting.910Assumes you already know what dashboards, metrics, logs, and traces are, and can write a basic PromQL/LogQL query. This skill covers the **operational layer** — the parts models tend to gloss over: cardinality cost, rule semantics, alert lifecycle, query-language footguns, provisioning UID stability, and the cross-component contract.1112## When to use1314Load when the question is about:15- Cardinality bombs (Prometheus/Mimir/Loki) and how to detect them16- PromQL correctness around `rate`/`irate`/`increase`/`histogram_quantile`/vector matching17- Recording-rule vs alerting-rule semantics, eval intervals, naming18- Alert lifecycle (Inactive→Pending→Firing→Recovering), `for:`, `keep_firing_for:`19- Alertmanager grouping (`group_wait` / `group_interval` / `repeat_interval`) and inhibition20- LogQL filter ordering and structured metadata in Loki 3.x21- Dashboard / datasource provisioning and UID stability22- Variable scoping (`$__interval` vs `$__rate_interval`, formatting modifiers)23- Exemplars wiring metrics → traces, TraceQL service-graph metrics24- Native histograms (v2.40+ experimental, v3.8+ stable)2526**Do NOT load** for: panel construction, picking a panel type, "how do I plot X", first-time install, basic PromQL syntax tutorials.2728## Cardinality cost model2930The **single biggest operational footgun** across Prometheus, Mimir, and Loki. Every unique label-set is a stored time-series; cost = series_count × samples_per_series.3132- **Per-metric guideline**: keep cardinality below 10 per metric; investigate alternatives above 100. The vast majority of metrics should have no labels.33- **Common bombs** (do NOT label by these): `user_id`, `email`, `request_id`/`trace_id`, status text, raw URL paths, K8s pod names with random suffixes, container IDs.34- **Detect top metrics by series count**:35 ```promql36 topk(20, count by (__name__)({__name__=~".+"}))37 ```38- **Detect top labels for a hot metric**:39 ```promql40 topk(20, count by (label_name)(metric_name))41 ```42- **TSDB internals to watch**: `prometheus_tsdb_head_series` (current active), `prometheus_tsdb_symbol_table_size_bytes` (label string interning), `scrape_series_added` per target.43- **Mimir per-tenant limits**: `max_global_series_per_user` (active series across ingesters; flag `-ingester.max-global-series-per-user`), `max_global_series_per_metric`, `ingestion_rate`, `ingestion_burst_size`. Excess is **rejected as 4xx client error** (data discarded, not retried). Per-ingester hard cut-off ≈ 2.5M series default.44- **Loki cardinality**: each unique stream-label combo = one log stream. Multiplicative — 3 status × 5 actions × 3 endpoints = 45 streams. Static labels only (`app`, `namespace`, `env`); push high-cardinality fields to **structured metadata** (Loki 3.0+, schema v13, chunk format v4) — queryable as `{job="x"} | pod="..."` without index cost.4546## PromQL gotchas4748- **`rate()` minimum window**: needs ≥ 4 samples in the range vector for stability; with default 15s scrape, that's a 1-minute floor — but you almost always want longer (5m+). Counter resets are handled inside `rate`/`increase`.49- **`$__rate_interval` formula** (Grafana 7.2+): `max($__interval + scrape_interval, 4 * scrape_interval)`. Always ≥ 4× scrape. **Use this in dashboards, not `$__interval`** — `$__interval` alone can drop below the 4× floor on zoom-in and produce empty graphs or undercounted rates.50- **`irate()`**: only the last two samples; great for short bursts/debugging, **unstable for long ranges or alerts** because it's noisy and non-monotonic.51- **`increase()` = `rate() × seconds_in_range`**. Same minimum-window requirement. Prefer `rate()` in recording rules.52- **`histogram_quantile()` (classic histograms)**:53 - Must group by the `le` label: `histogram_quantile(0.99, sum by (le, job)(rate(http_request_duration_seconds_bucket[5m])))`.54 - `le` values are **string-encoded floats** — `"0.5"` and `"0.50"` are different label values and won't aggregate.55 - Returns `NaN` if too few buckets; `+Inf` for φ > 1, `-Inf` for φ < 0.56 - Quantile error margin = bucket width. A 95th percentile of 220 ms with buckets at 0.2 / 0.3 estimates as ~295 ms — silent 75 ms error.57 - Buckets must align across instances or aggregation silently drops series.58- **Native histograms** (v2.40 experimental via `--enable-feature=native-histograms`; v3.8+ stable; v3.9 flag becomes no-op): exponential bucketing, single composite sample, ad-hoc quantile, aggregation across instances. **Prefer these** — switches scrape exposition to protobuf.59- **`quantile_over_time(φ, scalar[5m])`** vs **`histogram_quantile(φ, …_bucket)`**: completely different. The former aggregates raw scalars across time; the latter interpolates over a histogram's bucket boundaries.60- **`@` modifier** (introduced 2.25 experimental, stable in 2.33): pin evaluation timestamp — `metric @ 1609746000` or `@ start()` / `@ end()`. Required to make `topk()` stable across a range query (otherwise re-evaluates per step and series flicker in/out).61- **`offset 1w`**: shift evaluation back; combine with `@` to compare current-vs-historical at fixed boundaries.62- **`topk` / `bottomk`**: evaluated independently per timestamp — series enter and leave the result set across a range. Use `@` to pin.63- **Vector matching**: `on(labels)` restricts match keys; `ignoring(labels)` excludes. `group_left(x)` / `group_right(x)` for many-to-one / one-to-many — names the side with higher cardinality. Default is 1:1 and **fails the query** on cardinality mismatch.64- **`sum without (instance)` ≠ `sum by (job)`**: `without` drops named labels, **keeps everything else** (including new labels added later); `by` keeps **only** named labels. A new label appearing in scrape config will silently break dashboards built with `without` but not those built with `by` (and vice versa, depending on intent). Pick deliberately.6566## Recording rules6768- **Naming**: `level:metric:operations`. Level = remaining aggregation labels (e.g. `job`, `instance_path`); metric = base name unchanged (drop `_total` only when applying `rate`); operations = transformations newest-first. Example: `job:http_requests:rate5m`.69- **Aggregation strategy**: when computing ratios, aggregate numerator and denominator separately, then divide. Never average ratios.70- **Group eval interval ≤ scrape interval**, otherwise rules see stale samples. Default = `global.evaluation_interval`.71- **Group cannot finish before next cycle?** The next eval is **skipped** and `rule_group_iterations_missed_total` increments — silent gaps in recorded series.72- **`for:` does NOT exist in recording rules** — alerting-only.73- **Replacing a query in a recording rule that's also referenced by an alert**: alerts must be updated together. Otherwise the alert continues using the old (cached) query against the now-different output series.7475## Alerting rules and state lifecycle7677- **Prometheus states**: Inactive → Pending (during `for:`) → Firing → (Inactive on resolve, after `keep_firing_for:` if set).78- **Grafana-managed states** add: **NoData** (query returned 0 series), **Error** (query/eval failure), **Recovering** (firing → normal during `keep_firing_for:` window). NoData/Error are Grafana-only — Prometheus-managed alerts simply don't fire.79- **`for: 5m`** is a smoothing window: condition must be true on **every evaluation** for 5 minutes before firing. An alert that flaps inside that window **never fires** — convenient for noise, dangerous if the underlying issue is genuinely intermittent.80- **`keep_firing_for:`** (Prometheus 2.42, Feb 2023): keep firing for N after the condition clears. Defaults to 0 (off). Use to dampen flapping resolves; default behaviour deactivates immediately after first eval where condition not met.81- **`labels:` vs `annotations:`**: labels are **indexed** in Prom, used for routing/dedup; annotations are **unindexed**, free-form (`summary`, `description`, `runbook_url`, `dashboard_url`). Don't put high-cardinality strings in labels — they multiply your alert series.82- **Alert dedup by label set**: identical `{alertname, ...labels}` are the same alert. Adding a high-cardinality label (e.g. `instance` per pod) creates one alert per pod.8384## Alertmanager grouping and inhibition8586Defaults (override per route):87- **`group_wait: 30s`** — initial buffer for a new group, lets sibling alerts coalesce into the first notification.88- **`group_interval: 5m`** — wait between notifications when **new** alerts join an already-notified group.89- **`repeat_interval: 4h`** — re-notify the same alert after this if still firing.90- **`group_by: [alertname, cluster]`** — alerts sharing these labels coalesce into one notification. Add too many labels → no grouping; too few → unrelated alerts merged.91- **Inhibition rules**: when a "source" alert fires, suppress matching "target" alerts. Classic use: `severity=critical` on a node down inhibits `severity=warning` for individual pods on that node. Requires shared `equal:` labels.92- **Silences**: time-bounded label-matcher mute, separate from inhibition. Good for maintenance windows.9394## LogQL gotchas (Loki)9596Filter ordering matters for performance — Loki applies in this order, so cheap filters first:97981. **Stream selector** `{app="api", env="prod"}` — only indexed filter, narrows reading from object storage.992. **Line filters** `|= "error"` (substring, fastest), `|~ "regex"`, `!=`, `!~` — applied to raw line bytes before parsing.1003. **Parsers** `| json`, `| logfmt`, `| pattern "<ip> - <user>"`, `| regexp` — `pattern` is much cheaper than `regexp`.1014. **Label filters on parsed labels** `| status_code = 500`.1025. **Metric extraction** `| unwrap latency` for log-derived metrics: `sum(rate({...} | json | unwrap bytes [1m]))`.103104- **Anti-pattern**: `{app=~".+"} |= "error"` — full-tenant scan. Always pin at least one indexed label.105- **Loki 2.x cardinality cost**: putting `pod` or `request_id` in stream labels exploded the index. **Loki 3.0+ structured metadata** is the fix — query as `{app="x"} | pod="..."`, no index growth.106107## Dashboard variables108109- **Types**: query, custom, interval, datasource, textbox, constant, filters, switch.110- **Time variables**: `$__from` / `$__to` are **epoch ms** by default. `${__from:date:seconds}` for Unix seconds, `${__from:date:YYYY-MM-DD}` for custom.111- **`$__interval`**: `(to - from) / panel_resolution_pixels`. Used for `group by (time)` style binning. **Do NOT use in `rate()`** — can fall under 4×scrape on zoom.112- **`$__rate_interval`**: `max($__interval + scrape_interval, 4 * scrape_interval)`. **Always use this for `rate`/`increase`** in PromQL.113- **Multi-value formatting**:114 - `${var}` (default per datasource) — Prometheus emits regex `(a|b|c)`, requires `=~` matcher in the query.115 - `${var:csv}` → `a,b,c`116 - `${var:pipe}` → `a|b|c`117 - `${var:regex}` → escaped for regex118 - `${var:json}` → JSON array119- **Include All**: defaults to a literal `.+` regex unless "Custom all value" overrides. Beware: `.+` against `=~` matcher matches **every series**, not nothing.120- **Variable interpolation order**: query variables refresh on time-range change only if "Refresh: On Time Range Change" is set; otherwise stale on dashboard load — cause of mysterious "missing" series.121122## Provisioning and UID stability123124- **Dashboard provisioning** (`provisioning/dashboards/*.yaml`):125 ```yaml126 providers:127 - name: 'team-x'128 folder: 'Team X'129 folderUid: 'team-x-folder' # pin130 type: file131 updateIntervalSeconds: 10132 allowUiUpdates: false133 options:134 path: /var/lib/grafana/dashboards135 foldersFromFilesStructure: true136 ```137 - **The `uid` inside the dashboard JSON is the stable identity.** If absent, Grafana generates a new one on every reprovision → external links break, alert links break, history splits.138 - `allowUiUpdates: false` keeps file as source of truth; UI edits are reverted on next sync.139- **Datasource provisioning** (`provisioning/datasources/*.yaml`): **always set `uid:`** explicitly. Without it, the UID is randomly generated per instance — the same dashboard JSON imported into staging vs prod references different datasource UIDs and panels show "Datasource not found".140 ```yaml141 datasources:142 - name: Prometheus143 uid: prom-prod # pin or pay later144 type: prometheus145 url: http://prom:9090146 version: 1147 ```148 `prune: true` removes provisioned datasources when files are deleted; `version` controls precedence across instances.149- **Mixin pattern** (`monitoring-mixin`, kube-prometheus-stack, prometheus-operator helm chart): jsonnet libraries shipping dashboards + alerts as code. Lets you template `datasource` and `cluster` labels at render time.150151## Prometheus Operator / kube-prometheus-stack152153CRDs you'll touch:154- **`ServiceMonitor`** discovers via Service+EndpointSlice; **`PodMonitor`** discovers Pods directly (use when there's no Service).155- **`Probe`** for blackbox-style external endpoints; **`ScrapeConfig`** for arbitrary configs that don't fit ServiceMonitor.156- **`PrometheusRule`** holds recording + alerting rules; reconciled and **hot-loaded — no Prometheus restart**.157- **`AlertmanagerConfig`** for routing/inhibition split per namespace.158- Resource selectors: empty `{}` = match all; `null`/unset = match none. Wrong selector = silent zero-targets.159160## Exemplars and traces161162- **Exemplars** link a metric data point to a trace ID. Requires `--enable-feature=exemplar-storage` on Prometheus (fixed circular buffer in memory; persisted to WAL).163- Instrumentation must emit them (Go `prometheus/client_golang` `ObserveWithExemplar`; Java `io.prometheus.client.exemplars`).164- Grafana Prometheus datasource: enable "Exemplars" in datasource settings, link to a Tempo datasource by UID.165- **TraceQL** (Tempo): `{ resource.service.name = "checkout" && duration > 1s && status = error }`. Three attribute scopes: **intrinsic** (`name`, `duration`, `status`), **`span.*`** (per-span), **`resource.*`** (per-process). Wrong scope returns zero results without error.166- **Service graph metrics** (Tempo metrics-generator): emits `traces_service_graph_request_total`, `traces_service_graph_request_failed_total`, `traces_service_graph_request_server_seconds_*` as Prometheus metrics. Cardinality = (caller × callee) pairs — bounded by service count, not trace count.167- **Span metrics** (similar generator): emits RED metrics from spans (`traces_spanmetrics_calls_total`, `traces_spanmetrics_latency_*`). Cardinality watch: `span.name` and `http.route` should be normalized; raw URLs explode it.168169## Datasource considerations170171- **`Resolution` / `Min interval` / `Min step`** override `$__interval` and feed the `step` parameter to PromQL `query_range`. Smaller step = more points = potentially blown query timeout.172- **`maxDataPoints`** in panel — Grafana downsamples by adjusting `step`. If your alert query uses a fixed range and the dashboard query uses dynamic step, results legitimately differ.173- **Per-query timeout** (datasource setting): default 60 s; long-range high-cardinality `histogram_quantile` queries hit this.174175## Common pitfalls (consolidated)176177- `sum without (instance)` vs `sum by (job)` — drops named vs keeps named. New scrape labels silently flip results.178- Alert reused across envs via `for:` — copying production thresholds to staging without adjusting `for:` causes noisy paging on lower-traffic environments.179- Recording-rule renamed series, alert still references old name → silent eval against absent metric (alert never fires; not the same as a "passing" alert).180- Dashboard imported via UI without UID pin → external bookmarks/email links break on reimport.181- Datasource UID drift between instances → dashboard panels say "Datasource not found"; pin in provisioning YAML.182- `irate()` in alerts → flapping. Use `rate()` with a longer window.183- High-cardinality label in alert `labels:` (e.g. per-pod) → 1 alert per pod, paging storm.184- `$__interval` in `rate(metric[$__interval])` on dashboards → empty graphs on zoom-in.185- Annotations are **unindexed** — don't expect to filter or route on annotation contents.186- `topk` in a range query without `@ end()` → series flicker; viewer assumes stability that isn't there.187188## Authoritative references189190**Prometheus** (`prometheus.io/docs`):191- [Querying functions](https://prometheus.io/docs/prometheus/latest/querying/functions/)192- [Querying basics — `@` and `offset`](https://prometheus.io/docs/prometheus/latest/querying/basics/)193- [Operators / vector matching](https://prometheus.io/docs/prometheus/latest/querying/operators/)194- [Recording rules](https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/)195- [Alerting rules](https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/)196- [Histograms and quantiles](https://prometheus.io/docs/practices/histograms/)197- [Instrumentation best practices](https://prometheus.io/docs/practices/instrumentation/)198- [Naming](https://prometheus.io/docs/practices/naming/)199- [Recording rule patterns (`level:metric:operations`)](https://prometheus.io/docs/practices/rules/)200- [Feature flags (exemplar-storage, native-histograms)](https://prometheus.io/docs/prometheus/latest/feature_flags/)201- [`@` modifier introduction blog](https://prometheus.io/blog/2021/02/18/introducing-the-@-modifier/)202203**Grafana**:204- [Template variables (`$__interval`, `$__rate_interval`)](https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/)205- [Prometheus template variables](https://grafana.com/docs/grafana/latest/datasources/prometheus/template-variables/)206- [`$__rate_interval` formula](https://grafana.com/blog/new-in-grafana-7-2-rate-interval-for-prometheus-rate-queries-that-just-work/)207- [Provisioning](https://grafana.com/docs/grafana/latest/administration/provisioning/)208- [Alert rule state and health](https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/state-and-health/)209210**Loki**:211- [LogQL](https://grafana.com/docs/loki/latest/query/)212- [Cardinality](https://grafana.com/docs/loki/latest/get-started/labels/cardinality/)213- [Structured metadata (3.0+)](https://grafana.com/docs/loki/latest/get-started/labels/structured-metadata/)214215**Mimir**:216- [Runbooks (limits, cardinality)](https://grafana.com/docs/mimir/latest/manage/mimir-runbooks/)217- [Configuration parameters](https://grafana.com/docs/mimir/latest/references/configuration-parameters/)218219**Tempo**:220- [TraceQL](https://grafana.com/docs/tempo/latest/traceql/)221222**Alertmanager**:223- [Configuration / routing](https://prometheus.io/docs/alerting/latest/configuration/)224- [Robust Perception: `group_wait` / `group_interval` / `repeat_interval`](https://www.robustperception.io/whats-the-difference-between-group_interval-group_wait-and-repeat_interval/)225226**prometheus-operator / kube-prometheus-stack**:227- [Operator design and CRDs](https://prometheus-operator.dev/docs/getting-started/design/)228229**Reliable authors**: Tom Wilkie, Bryan Boreham, Cyril Tovena (Grafana Labs blog); Björn Rabenstein, Julius Volz, Richard Hartmann (Prometheus core).230231## Guardrails232233Before recommending a non-trivial operational change (cardinality cap, `for:`/`keep_firing_for:`, scrape interval, `$__rate_interval` rollout, Mimir limit override):2342351. Quote the specific parameter / function name and its default.2362. Cite the upstream doc (`prometheus.io/docs`, `grafana.com/docs/{loki,mimir,tempo}`).2373. Make the recommendation conditional on observed metrics — `prometheus_tsdb_head_series`, `cortex_limits_overrides`, `rule_group_iterations_missed_total`, alert flap rate — never blanket-tune.2384. Name a rollback: "if X spikes, revert by Y."239240**Tuning without measurement is worse than defaults.** Cardinality, alert thresholds, and rate windows are environment-specific — what works for a 100-pod cluster will page non-stop on 10,000.