grafana-best-practice — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited grafana-best-practice (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Use this skill to make Grafana dashboards readable, correctly aggregated, navigable, and safely deployable. Treat the rendered dashboard, Grafana API state, datasource query results, and container health as the source of truth.
uid, title, tags, templating.list, links, panels.provisioning/dashboards/*.yaml, especially folder, provider path, and allowUiUpdates./api/search, /api/folders, /api/dashboards/uid/<uid>, /api/ds/query, /api/health.symbol, reason, side in a trading app), container_name for containers, and host-level metrics that carry none of these.environment, project, url, or raw Prometheus family names.defaults.displayName (it overrides every column); columns renamed via the organize transform.union() queries — every filter() pushed inside each from() (predicate pushdown), never after union().collapsed: true) holds its child panels in the row's own panels[], but an expanded row (collapsed: false) keeps them as TOP-LEVEL siblings after the row object — not nested. A programmatic patch that assumes nesting silently skips every panel under an expanded row; index panels by walking both top-level entries and each row's panels[].display processor run client-side, so the API answers a different question than "what does the reader see". Open the deployed panel (/d-solo/<uid>/?panelId=N isolates one) and read its cells out of the DOM. Do this whenever you touched units, transforms, displayName, value mappings, overrides, or anything that returns more than one frame.
These fail SILENTLY — the panel renders, the query returns, the alert stays green, and the result is wrong. You will not know to go read the reference file, so they live here. Each points at the file with the worked example.
host, interface name). It is a latent datasource outage, not a tuning nit — see below.
They read the index and report the old shape after a collector change. Query real points.
labels + displayName. A bare set() that is not in the group key is not a label. → flux.md
schema collision: column "_value" is both of type string and float. Pivot first, group after. (For an all-numeric pivot the opposite holds: group first, or the rowKey tags stay frame labels and pollute every column header.) → flux.md, panels.md
fn: last.built with map() before spread() vanishes. Map after, or make it a group key. → flux.md
union() isnot pushed down: each branch full-scans the whole bucket into memory and can wedge a shared host. → flux.md
datasource.** Filter by measurement, keep the range tight, validate with count() first.
header. Rename in the organize transform. → panels.md
leaves a string alone only when no unit is set (getDisplayProcessor: else if (!unit && field.type === string) unit = 'string'). With one set it runs Number(value), and JS parses 0x… as a HEX LITERAL — an address column rendered 2.84027890695473e+22 Sept. Set units per column via overrides. → panels.md
processor both run in the BROWSER, so the API returns pre-transform, pre-format data. Every server-side check can be green while the page is wrong — read the rendered DOM. → deployment.md
errors, never shows NoData. Verify any schema change under such a rule against live data before shipping. → alerting.md
range(start: -15m) + last()), notthe dashboard picker. → performance.md
collector bind mount, app) ship by different mechanisms. → deployment.md
This is a collector-config decision (telegraf.conf tags vs included_keys), not a dashboard one, but it is the single thing most likely to take the datasource down — so audit it whenever you touch a dashboard that filters or groups by a status-like dimension.
series = distinct tag-sets × field keys. A tag whose value changes for a fixed entity over time — a "churning tag" — multiplies cardinality by every value it has ever taken inside the retention window. Status flags, a "best route"/"cheapest provider" pick, a container name on a shared host, a network interface name, a request id: all churn.
Three properties make this worse than an ordinary sizing mistake:
shard data but never reclaims series entries, so cardinality only ever goes up until someone rebuilds the index.
container can sit for weeks pegged at 95-100% of its memory cap and look perfectly healthy, then fail to boot forever after the first restart — a deploy, a host reboot, one heavy query. Cold-start capacity is not exercised by uptime; nothing warns you.
claiming "20-25k series worst case" sat above a measurement holding 166,773 — 7× off, and that staleness is why nobody looked. Another: six route-venue tags added on the belief that "cardinality is bounded by asset × a small venue-pair" minted 897,212 dead series, 79% of the whole bucket, and the resulting boot loop was a full outage.
Rules:
container_name for containers). Put churning values in included_keys as string fields and pivot them back in at query time.
deploying. Against a stopped instance: influxd inspect report-db --db-path <data>/<bucket-id> --rollup m. Every cardinality comment must carry a measured number and its date.
filters/groups on it, every alert rule, and the downsample task's string guard all move in one commit. A partial change breaks silently, not loudly.
schema.tagValues() / schema.fieldKeys() to tell you what is being writtenNOW. They read the index, which still lists every tag value and field key whose series exists in an overlapping shard, so right after a collector change they report the OLD shape for the new window — a -3m query can return 30 days of dead values. To answer "what is actually landing", query real points: range(start: -90s) |> filter(...) |> keep(columns: ["<tag>"]) |> group() |> distinct(column: "<tag>").
Prefer a named Grafana folder when permissions are known to be handled. For provisioned Grafana OSS dashboards with anonymous access, verify folder permissions explicitly because named folders can 403 even when dashboards exist.
Safe folder checklist:
provisioning/dashboards/default.yaml uses the intended folder, for example folder: Production./api/folders shows the folder./api/search?type=dash-db&tag=<base-tag> shows every dashboard with the expected folderTitle.200 for both /d/<uid>/... and /dashboards.Use one stable base tag for dashboard links and discovery, then add semantic tags. Keep tags low-cardinality and queryable.
Recommended tag shape — one stable base tag (the service slug) plus semantic axes. The values below are an illustrative set; substitute your own facets:
<service> # stable base tag, e.g. `myservice`
area:<facet> # e.g. area:overview | area:app | area:risk | area:infra
audience:<role> # e.g. audience:operator | audience:oncall
plane:<data-plane> # e.g. plane:prometheus | plane:app | plane:hostKeep dashboard dropdown links on the stable base tag (the service slug, e.g. myservice) so adding semantic tags does not break navigation.
Use explicit labels that explain important defaults:
{
"name": "instance",
"label": "Instance (All by default)",
"multi": true,
"includeAll": true,
"allValue": ".*"
}For high-level fleet dashboards, defaulting instance to All is good only if every panel handles All safely. When All is selected:
Load the file that matches what you are doing:
collapse, ratio math, reading a string field back, dual-bucket union() and downsampling: references/flux.md
answer-first summary row, and stat / timeseries / bar / table panel rules: references/panels.md
lookbacks, cheaper producer-side fields, and how to prove a performance fix: references/performance.md
errors, noDataState choice, Telegram chat_id / supergroup / topic message_thread_id, parse-mode escaping, message-template composition (group-label-in-header DRY, no-text-color emphasis, resolved-only context), live rule-health and real delivery verification: references/alerting.md
permissions, Grafana major-version bumps, the minimum post-deploy verification curls, compose + Telegraf monitoring-stack gotchas, and the InfluxDB OOM boot-loop recovery runbook: references/deployment.md
When reviewing or finishing work, report evidence, not just intent:
measurement/field renamed, a bucket purged), extract EVERY panel target from the deployed dashboards, substitute the Grafana variables, and execute them all against the datasource — not just the ones you edited. It is cheap, it is scriptable, and it is the only thing that catches a panel you did not realise depended on the old shape. Report the tally (e.g. "70 queries: 69 returned data, 0 errors, 1 empty") and explain every empty one; an empty result is only acceptable once you have shown the query is sound by loosening its filter to a value that does exist.
influxd inspect report-db, plus proof the new shape is what is actually landing (a distinct() over real points from the last 90s — NOT schema.tagValues()).
operators removed, bounded time windows added, old/new query result or timing comparison, and live dashboard JSON proof after deploy.
cards render, use range-correct semantics, and are not blank when expected data exists.
actual DOM text of the affected panel, quoted. "The datasource returns the right value" is not evidence that the panel shows it; those are different layers and only the second one is what the reader gets. A before/after pair is best: it is the only thing that distinguishes a fixed panel from one whose bug you never saw.
/api/.../rules all ok) and a real delivery test, when alerting changed.If a validation failure appears, fix the dashboard and rerun the relevant static, API, and datasource checks before declaring completion.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.