ObservabilityXXVIII · Grafana VariablesGrafanaVariables
Common Variable Patterns
What you'll learn
- Lay out a canonical $cluster, $namespace, $service variable chain for a Kubernetes-flavoured production dashboard
- Configure the includeAll, multi, allValue, and refresh fields so that the chain works for both the picker and the URL
- Write a regex exclusion that filters out control-plane and infra namespaces without breaking the All-value
- Recognise the failure shape of a regex that excludes a label the All-value expands to
- Apply the canonical layout to a Loki datasource and a Tempo datasource without losing the chain
Prerequisites
Verified against Prometheus 2.55.x · Alertmanager 0.28.x · node_exporter 1.8.x · blackbox_exporter 0.26.x · Grafana 11.x · Loki 3.x · Tempo current · OpenTelemetry Collector 0.110.x · Grafana Alloy current · Docker Engine 28.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-13
The on-call engineer opens a Kubernetes service dashboard
and chooses a namespace. The dropdown lists 137 of them. 89
are kube-system, kube-public, istio-system,
linkerd, monitoring, and the rest are control-plane
namespaces that should not be in the service filter at all.
The dashboard takes eleven seconds to load. The first panel
says “No data” because the regex matched the literal
kube-system|kube-public|istio-system|... against the
service label and produced a 1,400-character regex.
This is what the canonical variable layout exists to prevent. A well-shaped layout does three things: it filters the value list at the source, it orders the chain from broadest to narrowest, and it treats the All-value as a first-class citizen that the regex must include.
What it is
The canonical variable layout for a Kubernetes-flavoured production dashboard is a chain of three query variables, each rooted in the same Prometheus metric and ordered from broadest to narrowest:
+-------------------+ +--------------------+ +-------------------+
| $cluster |--> | $namespace |-->| $service |
| label_values | | label_values | | label_values |
| (up, cluster) | | (up, namespace) | | (up, service) |
+-------------------+ +--------------------+ +-------------------+
Each variable is filtered on the previous variable’s current
selection through =~. Each has multi: true and
includeAll: true so the viewer can fan out or collapse the
filter. Each has refresh: 1 so the chain resolves on
dashboard load.
The canonical extensions for log and trace signals are the same chain against a different data source:
+-------------------+ +--------------------+ +-------------------+
| $cluster |--> | $namespace |-->| $service |
| Loki: | | Loki: | | Loki: |
| label_values | | label_values | | label_values |
| (... | cluster) | | (... | namespace) | | (... | service) |
+-------------------+ +--------------------+ +-------------------+
The shape is the same. The label extraction differs: Loki
variables use label_values against a Loki query; Tempo
variables use label_values against a Tempo query.
Why a sysadmin cares
Three operational pains map directly to the canonical layout:
- Predictable navigation. A viewer who knows the layout can predict the chain: cluster narrows to namespace narrows to service. The dropdowns behave the same way on every dashboard that follows the pattern.
- Bounded value lists. A
queryvariable that does not filter the value set at the source produces a dropdown that scrolls forever. The canonical layout uses aregexfield on the variable to exclude control namespaces at the source. - Survivable All-value. The All-value is the most-used
selection in production. A regex exclusion that
accidentally excludes the All-value’s expansion turns
“no filter” into “no data”. The canonical layout pairs
includeAll: truewithallValue: ".*"and writes the regex so it matches what All expands to.
The wrong shape shows up as a dropdown that takes minutes to load and a panel that returns nothing when the viewer expects “everything”.
The All-value and the regex exclusion
The All-value expands to allValue. When allValue: ".*",
the data source receives cluster=~".*". The exclusion
regex on a downstream variable must permit .* to pass
through, or the chain breaks at the All selection.
The failure shape is subtle: a regex like
cluster=~"(prod|staging).*" excludes every label value
that does not start with prod or staging. When the
viewer selects All on the parent variable, cluster
expands to .*, which does not start with prod or
staging, and the downstream variable’s value list is
empty.
The fix is either to use =~".*" (no exclusion) for the
chain or to expand the exclusion regex to match .* as
well: cluster=~"(prod|staging|.*)". The latter is
redundant but documents the intent.
How it works
The canonical layout has three moving parts:
+-------------------+
| $cluster |
| |
| query: label_values(up{job="kube-state"}, cluster)
| regex: /^(prod|staging)$/
+-------------------+
|
v
+-------------------+
| $namespace |
| |
| query: label_values(up{job="kube-state", cluster=~"$cluster"},
| namespace)
| regex: /^(kube-.*|istio-.*|.*)$/
+-------------------+
|
v
+-------------------+
| $service |
| |
| query: label_values(up{job="kube-state",
| cluster=~"$cluster",
| namespace=~"$namespace"},
| service)
| regex: /^(kube-state|node-exporter)$/
+-------------------+
The regex field on each variable is a client-side filter
applied after the data source returns the value list. The
regex is not PromQL; it is Grafana’s own value-list
filter, written as a JavaScript regex with optional
slashes.
Three details to notice:
- The chain expands the value set, then narrows. The
$clusterquery returns every cluster Prometheus has; the regex narrows toprodandstaging. The$namespacequery returns every namespace Prometheus has for the selected cluster(s); the regex permits the All-value (.*) and matches common control namespaces. - The regex must include the All expansion. When
allValue: ".*", the regex on the downstream variable must match.*or the chain breaks at All. The canonical pattern is(my-pattern|.*). - The regex is not a security boundary. The regex filters the value list; it does not filter the data the panels render. A viewer who picks a cluster outside the regex still sees panels that match that cluster; the regex only affects what is selectable.
How to configure it
The canonical templating.list for a Kubernetes dashboard:
{
"templating": {
"list": [
{
"name": "cluster",
"label": "Cluster",
"type": "query",
"datasource": { "type": "prometheus", "uid": "prom-prod" },
"query": "label_values(up{job=\"kube-state\"}, cluster)",
"regex": "/^(prod|staging)$/",
"refresh": 1,
"includeAll": true,
"allValue": ".*",
"multi": true,
"sort": 1,
"current": { "selected": true, "text": "All", "value": "$__all" }
},
{
"name": "namespace",
"label": "Namespace",
"type": "query",
"datasource": { "type": "prometheus", "uid": "prom-prod" },
"query": "label_values(up{job=\"kube-state\", cluster=~\"$cluster\"}, namespace)",
"regex": "/^(kube-system|kube-public|istio-system|.*)$/",
"refresh": 1,
"includeAll": true,
"allValue": ".*",
"multi": true,
"sort": 1,
"current": { "selected": true, "text": "All", "value": "$__all" }
},
{
"name": "service",
"label": "Service",
"type": "query",
"datasource": { "type": "prometheus", "uid": "prom-prod" },
"query": "label_values(up{job=\"kube-state\", cluster=~\"$cluster\", namespace=~\"$namespace\"}, service)",
"regex": "/^(kube-state|node-exporter|.*)$/",
"refresh": 1,
"includeAll": true,
"allValue": ".*",
"multi": true,
"sort": 1,
"current": { "selected": true, "text": "All", "value": "$__all" }
}
]
}
}
Three patterns to notice in this layout:
- The
regexfield has slashes. Grafana parses the regex as a JavaScript RegExp; the slashes are conventional, not required. The regex matches the entire value (^...$anchors) and is anchored to the start and end. - The regex permits
.*. The namespace regex lists control-plane namespaces explicitly and adds.*to cover All. Without.*, All breaks the chain. multi: trueplusincludeAll: true. The viewer can pick a single cluster, several clusters, or All. The data source receives a regex in either case; the panel query uses=~to match multi-value selections.
For Loki and Tempo, the chain is the same shape with a
different datasource field and a label-extraction query:
{
"name": "cluster",
"datasource": { "type": "loki", "uid": "loki-prod" },
"query": "label_values({job=\"kube-state\"}, cluster)"
}
{
"name": "service",
"datasource": { "type": "tempo", "uid": "tempo-prod" },
"query": "label_values({}, service)"
}
The regex, refresh, includeAll, and allValue fields are identical. The chain order is identical.
How to validate it
Three checks confirm the chain is correctly wired.
Severity: READ-ONLY.
# 1. The JSON declares the variables in chain order with
# the expected regex and allValue.
curl -s -u admin:$ADMIN \
https://grafana.example.com/api/dashboards/uid/k8s-svc \
| jq '.dashboard.templating.list[]
| {name, regex, allValue, includeAll, multi}'
{
"name": "cluster",
"regex": "/^(prod|staging)$/",
"allValue": ".*",
"includeAll": true,
"multi": true
}
{
"name": "namespace",
"regex": "/^(kube-system|kube-public|istio-system|.*)$/",
"allValue": ".*",
"includeAll": true,
"multi": true
}
{
"name": "service",
"regex": "/^(kube-state|node-exporter|.*)$/",
"allValue": ".*",
"includeAll": true,
"multi": true
}
# 2. Each variable's query returns a value set. The
# regex is not applied yet; the data source returns
# the full label list.
curl -G -s http://prometheus:9090/api/v1/query \
--data-urlencode 'query=label_values(up{job="kube-state"}, cluster)' \
--data-urlencode 'time='$(date +%s) \
| jq '.data.result | map(.metric.cluster)'
[
"prod",
"staging",
"dev"
]
# 3. The chain narrows at each step. With $cluster=prod,
# the namespace list is the namespaces in prod only.
curl -G -s http://prometheus:9090/api/v1/query \
--data-urlencode 'query=label_values(up{job="kube-state",cluster="prod"}, namespace)' \
--data-urlencode 'time='$(date +%s) \
| jq '.data.result | map(.metric.namespace) | length'
# 41
The final check confirms the chain narrows correctly. A chain that returns the same number of namespaces regardless of cluster has a broken query.
How it can fail
Five failure shapes appear repeatedly with the canonical layout:
- Regex excludes the All-value. The namespace regex
is
/^(kube-system|kube-public)$/with no.*cover. When the viewer selects All on$cluster, the namespace list is empty. Symptom: namespace dropdown is empty when cluster is All. - Chain order reversed. The
$servicevariable’s query references$namespacebefore$namespacehas resolved. The service query runs against all namespaces, every cluster. Symptom: dashboard load takes ten seconds; service list is unbounded. - Anchor regex too tight. A regex of
^prod$does not matchprod-1orprod-2. Symptom: cluster dropdown excludes multi-region variants. - Anchor regex too loose. A regex of
prod(no anchors) matchesprod,production,preprod. Symptom: cluster dropdown includes names that do not exist in the data source; the panel shows empty series for them. refresh: 2on a chain variable. Arefresh: 2on$clusterre-runs the variable query every time-range change. Symptom: dashboard load is slow on long ranges because the chain re-resolves.
How to troubleshoot it
The diagnostic order:
- Inspect the chain order. Topological-sort by
$references. The chain must run from broadest to narrowest. - Inspect the regex on each variable. The regex must
include
.*(or whateverallValueis) to survive the All selection. - Run the variable query directly. Take the
queryfield from each variable, paste it into Prometheus’s/graph, and confirm the value set. - Switch to a single cluster in the URL. Pin
?var-cluster=prodand confirm the namespace and service lists narrow. - Disable the regex temporarily. Set
regex: ""on the variable. The full label list from the data source appears. If the chain still works, the regex is the bug.
Security implications
- The regex is a value-list filter, not a security
boundary. The regex narrows what the viewer can pick;
it does not narrow what the panel can render. A viewer
who manually edits the URL to
var-cluster=anythingsees the panels filtered onanythingregardless of the regex. - The
queryfield runs against the data source with the data source’s permissions. A viewer’s selection propagates to the panel query; the data source receives a regex matcher, not a value list. - Multi-value selection can be unbounded. A
multi: truevariable withallValue: ".*"lets the viewer match every value the data source has. Against Prometheus, this is bounded by what the data source exposes. Against a non-paged query, this can be expensive.
Performance implications
- The chain is three round trips to the data source per dashboard load. The cost is amortised across every panel that uses the chain.
- The
regexfilter is client-side. A regex of 1,000 alternatives runs in the browser in microseconds. - A
refresh: 1chain re-runs on every dashboard load. Arefresh: 2chain re-runs on every time-range change. Either way, the cost is paid by the data source, not by Grafana.
Production guidance
- Default new dashboards to the canonical layout.
$cluster,$namespace,$servicein that order, withmulti: true,includeAll: true,allValue: ".*", andregexthat permits.*. - Document the regex in the dashboard description. The regex is the contract that controls what the dropdown shows; the viewer needs to know what is excluded.
- Use
refresh: 1unlessrefresh: 2is operationally required. The cost ofrefresh: 2is hidden in dashboard load latency. - Verify the chain order by inspecting the JSON. The chain must run from broadest to narrowest; a reversed chain is a slow dashboard.
Verification
You should now be able to answer:
- What is the canonical chain order for a Kubernetes dashboard, and why does order matter?
- Why does a regex exclusion that omits
.*break the All selection? - What does
multi: truecombined withincludeAll: trueplusallValue: ".*"give the viewer? - How do you test the chain in isolation?
- What does
refresh: 1versusrefresh: 2cost in terms of dashboard load latency?
Quiz
Knowledge check · 8 questions
Q1. What is the correct order for a Kubernetes-flavoured production dashboard variable chain?
Q2. Why does a regex like /^(kube-system|kube-public)$/ break the All selection on a downstream variable?
Q3. A regex on a Grafana variable filters what the panel queries can render, not just what the dropdown shows.
Q4. A dashboard uses $cluster = prod-eu, $namespace = kube-system, $service = coredns. Which chain is correctly ordered?
Q5. Name one thing the regex on a Grafana variable MUST contain to keep the All selection working when allValue is ".*".
Q6. Which of these are required for a canonical Kubernetes chain?
Q7. A Loki datasource variable for service wants to extract the service label from log streams. Which Grafana variable kind is correct?
Q8. Which regex best documents the intent to list the control-plane namespaces while still permitting the All pseudo-value?
Passing score: 75%. Answers are checked in this browser.