feat: add grafana dashboarding and promql agent skills
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
---
|
||||
name: dashboarding
|
||||
license: Apache-2.0
|
||||
description: Build, modify, and ship Grafana dashboards as JSON via the HTTP API — panel types (timeseries / stat / gauge / table / heatmap / logs / traces / node-graph), `gridPos` 24-column layout, units, thresholds, template + datasource + chained variables, transformations (`organize` / `calculateField` / `filterByValue`), panel + dashboard links with `${__field.labels.x}` / `${__from}`, and Loki/Prometheus annotations. Use when scripting dashboard creation, writing the dashboard JSON for a new service, adding a `$job` dropdown variable, computing an "Error %" column with a transformation, overlaying deploys as annotations, or pushing a dashboard via `POST /api/dashboards/db` — even when the user says "create a dashboard for this metric", "add a service dropdown", "show errors as percentage", "overlay our deploys", or "export the dashboard JSON" without naming the API or schema. After every API push, verify with the returned `version` plus a GET on the dashboard UID.
|
||||
---
|
||||
|
||||
# Grafana Dashboard Authoring
|
||||
|
||||
> **Docs**: https://grafana.com/docs/grafana/latest/dashboards/
|
||||
|
||||
Dashboards are JSON. Author once, push via API, share by `uid`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Grafana stack (OSS, Enterprise, or Cloud) reachable from your machine
|
||||
- API token with `dashboards:write` (`Authorization: Bearer <token>`)
|
||||
- `jq` for inspecting responses
|
||||
- The JSON-schema cheat sheet in [`references/json-schema.md`](references/json-schema.md)
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### 1. Push a new dashboard via the API + verify
|
||||
|
||||
```bash
|
||||
# 1. Build the payload — wrap the dashboard JSON, set folder, mark overwrite
|
||||
cat > /tmp/dash.json <<'JSON'
|
||||
{
|
||||
"dashboard": {
|
||||
"uid": "demo-svc-v1",
|
||||
"title": "Demo Service",
|
||||
"schemaVersion": 41,
|
||||
"tags": ["demo"],
|
||||
"time": { "from": "now-1h", "to": "now" },
|
||||
"templating": { "list": [] },
|
||||
"panels": [{
|
||||
"id": 1, "type": "timeseries", "title": "Request Rate",
|
||||
"gridPos": { "x": 0, "y": 0, "w": 24, "h": 8 },
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"targets": [{
|
||||
"expr": "sum(rate(http_requests_total[5m])) by (status_code)",
|
||||
"legendFormat": "{{status_code}}", "refId": "A"
|
||||
}],
|
||||
"fieldConfig": { "defaults": { "unit": "reqps" }, "overrides": [] }
|
||||
}]
|
||||
},
|
||||
"folderUid": "",
|
||||
"overwrite": true,
|
||||
"message": "initial push"
|
||||
}
|
||||
JSON
|
||||
|
||||
# 2. Validate the JSON BEFORE you send it (catches trailing-comma typos)
|
||||
jq empty /tmp/dash.json && echo "json ok"
|
||||
|
||||
# 3. POST
|
||||
RESP=$(curl -s -X POST -H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$GRAFANA/api/dashboards/db" -d @/tmp/dash.json)
|
||||
echo "$RESP" | jq '{status, uid, url, version}'
|
||||
# Expect: status="success", url="/d/demo-svc-v1/...", version=1 (incremented on each push)
|
||||
|
||||
# 4. Verify the round-trip — read it back and confirm one panel + the expected title
|
||||
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||
"$GRAFANA/api/dashboards/uid/demo-svc-v1" \
|
||||
| jq '{title: .dashboard.title, panels: (.dashboard.panels | length)}'
|
||||
# Expect: {"title":"Demo Service","panels":1}
|
||||
|
||||
# 5. Open the dashboard in a browser — confirm the panel renders with data.
|
||||
```
|
||||
|
||||
### 2. Add a `$job` template variable to an existing dashboard
|
||||
|
||||
```bash
|
||||
# 1. Fetch existing dashboard
|
||||
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||
"$GRAFANA/api/dashboards/uid/demo-svc-v1" > /tmp/dash.json
|
||||
|
||||
# 2. Edit templating.list — append:
|
||||
# { "name":"job", "type":"query",
|
||||
# "datasource":{"type":"prometheus","uid":"prometheus"},
|
||||
# "query":{"query":"label_values(up, job)","refId":"A"},
|
||||
# "refresh":2, "includeAll":true, "multi":true, "label":"Service" }
|
||||
# (Use jq, an editor, or the Grafana UI — schema in references/json-schema.md.)
|
||||
|
||||
# 3. Update the panel expr to use the variable: rate(http_requests_total{job=~"$job"}[5m])
|
||||
|
||||
# 4. POST it back with overwrite: true. Verify the variable appears in the UI dropdown.
|
||||
```
|
||||
|
||||
### 3. Compute an "Error %" column with a transformation
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "calculateField",
|
||||
"options": {
|
||||
"alias": "Error %", "mode": "reduceRow",
|
||||
"reduce": { "reducer": "last" },
|
||||
"binary": { "left": "errors", "right": "total", "operator": "/" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add this to the panel's `transformations: []`. Verify in the UI panel inspector — the new field should appear and update with the variable selection.
|
||||
|
||||
Full schema (panels, units, all transformations, annotations, links): [`references/json-schema.md`](references/json-schema.md).
|
||||
|
||||
## API reference
|
||||
|
||||
```bash
|
||||
# Get
|
||||
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||
"$GRAFANA/api/dashboards/uid/<uid>" | jq '.dashboard'
|
||||
|
||||
# Search
|
||||
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||
"$GRAFANA/api/search?query=kubernetes&type=dash-db" | jq '.[] | {uid,title,folderTitle}'
|
||||
|
||||
# Create folder
|
||||
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" "$GRAFANA/api/folders" \
|
||||
-d '{"uid":"platform-team","title":"Platform Team"}'
|
||||
```
|
||||
|
||||
For dashboards embedded in app plugins, use `@grafana/scenes` (skill `grafana-o11y:grafana-scenes`).
|
||||
|
||||
## Resources
|
||||
|
||||
- [Dashboard JSON model](https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/view-dashboard-json-model/)
|
||||
- [HTTP API — dashboards](https://grafana.com/docs/grafana/latest/developers/http_api/dashboard/)
|
||||
- [Panel types](https://grafana.com/docs/grafana/latest/panels-visualizations/)
|
||||
- [Variables](https://grafana.com/docs/grafana/latest/dashboards/variables/)
|
||||
- [Transformations](https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/transform-data/)
|
||||
@@ -0,0 +1,157 @@
|
||||
# Dashboard + panel JSON schema
|
||||
|
||||
## Dashboard root
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "My Dashboard",
|
||||
"uid": "my-dashboard-v1",
|
||||
"tags": ["service", "production"],
|
||||
"time": { "from": "now-1h", "to": "now" },
|
||||
"refresh": "30s",
|
||||
"timezone": "browser",
|
||||
"schemaVersion": 41,
|
||||
"templating": { "list": [] },
|
||||
"annotations": { "list": [] },
|
||||
"panels": []
|
||||
}
|
||||
```
|
||||
|
||||
- `uid` — stable identifier; keep short
|
||||
- `schemaVersion` — `41` for Grafana 11+
|
||||
- `time.from` / `to` — relative (`now-1h`) or absolute ISO
|
||||
- `refresh` — `"30s"`, `"1m"`, `"5m"`, `""` (off)
|
||||
|
||||
## Panel
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"type": "timeseries",
|
||||
"title": "Request Rate",
|
||||
"gridPos": { "x": 0, "y": 0, "w": 12, "h": 8 },
|
||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||
"targets": [{
|
||||
"expr": "sum(rate(http_requests_total{job=\"$job\"}[5m])) by (status_code)",
|
||||
"legendFormat": "{{status_code}}",
|
||||
"refId": "A"
|
||||
}],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "reqps",
|
||||
"thresholds": { "mode": "absolute", "steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "yellow", "value": 1000 },
|
||||
{ "color": "red", "value": 5000 }
|
||||
]}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"legend": { "calcs": ["mean","max","last"], "displayMode": "table", "placement": "bottom" },
|
||||
"tooltip": { "mode": "multi", "sort": "desc" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`gridPos`: 24-column grid. Widths: full=24, half=12, third=8, quarter=6. Height 1 unit ≈ 30 px.
|
||||
|
||||
## Panel types
|
||||
|
||||
| Panel | Use case |
|
||||
|---|---|
|
||||
| **Time series** | Any metric over time |
|
||||
| **Stat** | Single value + sparkline |
|
||||
| **Gauge** | % or value against min/max |
|
||||
| **Bar gauge** | Side-by-side comparison |
|
||||
| **Table** | Multi-column data |
|
||||
| **Heatmap** | Distribution over time |
|
||||
| **Logs** | Loki log streams |
|
||||
| **Traces** | Tempo trace search |
|
||||
| **Text** | Markdown docs |
|
||||
| **Candlestick** | OHLC / min-max-avg |
|
||||
| **Node graph** | Service dependency graph |
|
||||
|
||||
## Useful units
|
||||
|
||||
```
|
||||
reqps requests/sec
|
||||
ops ops/sec
|
||||
Bps bytes/sec
|
||||
percentunit 0.0-1.0 as %
|
||||
bytes bytes (auto-scales)
|
||||
decbytes decimal bytes (1 KB = 1000 B)
|
||||
ms / s milliseconds / seconds
|
||||
dtdurationms 1h 2m 3s
|
||||
short compact (1.2k, 3.4M)
|
||||
none raw number
|
||||
```
|
||||
|
||||
## Template variables
|
||||
|
||||
```json
|
||||
{ "name":"job", "type":"query", "datasource":{"type":"prometheus","uid":"prometheus"},
|
||||
"query":{"query":"label_values(up, job)","refId":"A"},
|
||||
"refresh":2, "includeAll":true, "multi":true, "label":"Service" }
|
||||
|
||||
{ "name":"cluster", "type":"constant", "query":"production", "label":"Cluster" }
|
||||
|
||||
{ "name":"datasource", "type":"datasource", "pluginId":"prometheus",
|
||||
"includeAll":false, "label":"Prometheus" }
|
||||
```
|
||||
|
||||
Multi-value variables expand to a regex OR: `$job=["api","worker"]` → `job=~"api|worker"`.
|
||||
Chained: `label_values(kube_pod_info{namespace=\"$namespace\"}, pod)`.
|
||||
|
||||
## Transformations
|
||||
|
||||
```json
|
||||
"transformations": [
|
||||
{ "id": "merge", "options": {} },
|
||||
{ "id": "organize", "options": {
|
||||
"renameByName": { "Value #A": "Request Rate", "Value #B": "Error Rate" },
|
||||
"excludeByName": { "Time": true }
|
||||
}},
|
||||
{ "id": "calculateField", "options": {
|
||||
"alias": "Error %", "mode": "reduceRow",
|
||||
"reduce": { "reducer": "last" },
|
||||
"binary": { "left": "errors", "right": "total", "operator": "/" }
|
||||
}},
|
||||
{ "id": "filterByValue", "options": {
|
||||
"filters": [{ "fieldName":"Error %", "config":{ "id":"greater", "options":{ "value":0.01 }}}],
|
||||
"type": "include", "match": "any"
|
||||
}}
|
||||
]
|
||||
```
|
||||
|
||||
Common IDs: `merge`, `organize`, `rename`, `calculateField`, `filterByValue`, `groupBy`, `sortBy`, `limit`, `labelsToFields`, `seriesToRows`, `partitionByValues`.
|
||||
|
||||
## Links & annotations
|
||||
|
||||
```json
|
||||
"links": [
|
||||
{ "title":"Go to details", "url":"/d/details?var-service=${__field.labels.service}", "targetBlank":false },
|
||||
{ "title":"Runbook", "url":"https://wiki.example.com/runbook/${job}", "icon":"external link",
|
||||
"targetBlank":true, "type":"link" }
|
||||
]
|
||||
```
|
||||
|
||||
Built-in vars: `${__value.raw}`, `${__field.labels.job}`, `${__url.params}`, `${__from}` / `${__to}` (Unix ms).
|
||||
|
||||
Loki annotation:
|
||||
|
||||
```json
|
||||
{ "datasource":{"type":"loki","uid":"loki"},
|
||||
"expr":"{job=\"deployments\"} |= \"deployed\"",
|
||||
"name":"Deployments", "iconColor":"blue",
|
||||
"titleFormat":"{{service}} deployed", "textFormat":"{{version}} by {{author}}" }
|
||||
```
|
||||
|
||||
Prometheus annotation:
|
||||
|
||||
```json
|
||||
{ "datasource":{"type":"prometheus","uid":"prometheus"},
|
||||
"expr":"changes(kube_deployment_status_observed_generation{namespace=\"production\"}[5m]) > 0",
|
||||
"step":"60s", "name":"Deployments", "iconColor":"blue",
|
||||
"titleFormat":"Deploy: {{deployment}}" }
|
||||
```
|
||||
Reference in New Issue
Block a user