Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions docs/design/curated-pack-315-kubernetes-cadvisor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Curated Pack — Grafana 315 "Kubernetes cluster monitoring (via Prometheus)"

> Design + living discoveries for the 315 curated pack. Follows the general
> Curation Playbook in `curated-dashboard-packs-plan.md`. First of two stacked
> Kubernetes packs (315 cAdvisor, then 6417 kube-state-metrics).

- Source: community **"Kubernetes cluster monitoring (via Prometheus)"**,
<https://grafana.com/grafana/dashboards/315-kubernetes-cluster-monitoring-via-prometheus/>
- gnetId **315**, latest revision **3**.
- canonical sha256 (rev 3) = `6fb5e045bc6d860f0f22ce7e145f4da04d2e25fd4fbf9fda29355cef6d63aeae`.
- Datasource: Prometheus — curated-pack eligible.
- Schema: **v12 (old `rows[]` layout)**. 21 panels across 13 rows.
- Metric family: **cAdvisor** (`container_*`) + **machine_** (`machine_cpu_cores`,
`machine_memory_bytes`). NOT kube-state-metrics.
- Variables: `$Node` = `label_values(kubernetes_io_hostname)`.

## Goal

Ship a curated pack so 315 renders in Kibana against a modern cAdvisor scrape
ingested in the Elastic `prometheus_native` layout (`metrics.* + labels.*`),
bridging the pre-1.16 cAdvisor label conventions the dashboard was authored
against, and degrading honestly where the source series no longer exist.

## The core problem: a pre-1.16 cAdvisor dashboard

Unlike 12485 (an old dashboard mapped onto a *modern exporter the rig runs*),
315's PromQL is written against label/runtime conventions that modern Kubernetes
and the Elastic cAdvisor integration no longer emit:

| Dashboard convention | Modern reality | Pack handling |
|---|---|---|
| `pod_name` / `container_name` grouping | cAdvisor is `pod` / `container` | `label_rewrites` |
| `kubernetes_io_hostname=~"^$Node$"` | relabel gone; container series carry no node label | `ignored_labels` (drop matcher); `$Node` control is inert → dropped |
| `image!=""`, `name=~"^k8s_.*"` | not emitted | `ignored_labels` (drop matcher) so the good panels are not filtered to empty |
| `name!~"^k8s_.*"` (docker), `rkt_container_name` | obsolete runtimes | **dropped as honest gaps** via curated `query_overrides` with `approximation_note` |
| `systemd_service_name` | pre-labelmap convention | NOT ignored → panel degrades to an honest empty (never a fake single aggregate) |
| `machine_cpu_cores`, `machine_memory_bytes`, `container_fs_*`, `id="/"` | present only on a full cAdvisor + machine scrape | cluster-KPI panels are `APPROXIMATE`; render when that telemetry exists |

## Engine vs pack split

The general pipeline already handles rate() counters, gauge sum, the
unary-minus **butterfly** net-I/O panels, `rows[]` → Kibana sections, singlestat
reducers, and control synthesis. The pack carries only:

- **`metric_kinds`** — cAdvisor counters (`container_cpu_usage_seconds_total`,
`container_network_{receive,transmit}_bytes_total`) vs gauges
(`container_memory_working_set_bytes`, `container_fs_*`, `machine_*`).
- **`label_rewrites`** — `pod_name`→`labels.pod`, `container_name`→`labels.container`,
`id`→`labels.id`.
- **`ignored_labels`** — `kubernetes_io_hostname`, `image`, `name`. These appear
only as filters on the good container panels; dropping them lets those panels
resolve instead of filtering to empty. `systemd_service_name` /
`rkt_container_name` are deliberately NOT ignored so their panels degrade to
an honest empty rather than collapse into one misleading aggregate line.
- **`query_overrides`** — the three multi-runtime panels (Containers CPU / memory
/ network) keep only the k8s pod/container series and disclose the dropped
docker/rkt runtimes via `approximation_note` (status capped at
`migrated_with_warnings`, never a clean `migrated`); Pods CPU/memory keep the
Lens breakdown on `labels.pod`; Pods/All-processes network name Received/Sent
(native butterfly otherwise labels transmit `Value B`); All-processes panels
group by `labels.id` excluding the root cgroup.

No engine changes required — the pack reuses the APIs 12485 added.

## Fidelity

- **PERFECT** (render on any modern cAdvisor scrape after the label bridge):
Network I/O pressure, Pods CPU / memory / network (Received/Sent series names).
- **APPROXIMATE**: Containers CPU / memory / network (docker/rkt series dropped);
the cluster-KPI strip and Used/Total stats (require `machine_*` +
`container_fs_*` + the root-cgroup `id="/"` series); All-processes panels
(grouped by the cAdvisor cgroup `id`, excluding `id="/"`; the producer must
emit a per-container cgroup `id` or the legend is a single `(null)`).
- **GAP** (honest empty on modern data): System services CPU / memory
(`systemd_service_name` is a dead convention).

See `fidelity_manifest.yaml` for the per-panel table.

## Validation gates (UI testing)

Live validation uses the shared curated rig (`parity-rig/curated/…`) extended
with a synthetic cAdvisor exporter (`container_*` + `machine_*` + `container_fs_*`
with modern `pod`/`container` labels, a root-cgroup `id="/"` series, and a
per-container cgroup `id` so All-processes legends are not `(null)`) so the
per-pod panels and the cluster KPIs both have real data.

1. Migrate + upload to Kibana (`prometheus_native`, `--esql-index` = data view).
2. Render audit — 0 `render_error`; document `field_gap`/`data_gap` for the
`systemd`/all-processes gap panels in `fidelity_manifest.yaml`.
3. Side-by-side vs the provisioned Grafana 315.

## Task checklist

- [x] registry.yaml entry (315, rev 3, sha above)
- [x] pack.yaml + fidelity_manifest.yaml
- [x] offline fixture tests (`tests/test_curated_packs.py`)
- [x] rig: synthetic cAdvisor exporter (`machine_*`, `container_fs_*`, `id="/"`, per-container cgroup `id`)
- [x] live: migrate + upload + render audit + view-mode UI pass
- [x] docs: discoveries here + `docs/sources/grafana.md`

## Discoveries

- Offline translation confirms the label bridge: `Pods CPU usage` resolves to
`SUM(RATE(container_cpu_usage_seconds_total)) BY time_bucket, labels.pod`
with the `image`/`name`/`kubernetes_io_hostname` matchers stripped; the
Containers panels take the curated k8s-only ES|QL; `System services` uses an
impossible-filter override so Lens does not error on a missing
`systemd_service_name` column (honest empty).
- The cluster-KPI panels translate to a same-bucket ratio referencing
`machine_*` and the `id="/"` root cgroup — so live rendering needs those
series in the scrape (they are absent from a plain container-only ingest).
- Engine butterfly + a pod breakdown names transmit `Value B`; Pods network
I/O needs an explicit Received/Sent override (same class as Containers
network).
- All-processes panels group by cgroup `id`. A producer that only sets
`id="/"` on the root cgroup yields a single `(null)` legend; emit
`/kubepods/<pod>/<container>` on workload series.
23 changes: 23 additions & 0 deletions docs/sources/grafana.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,29 @@ target exporter runs the `stat_statements` + `postmaster` collectors and the
`pg_stat_statements` extension is installed; otherwise they degrade to an
honest field/data gap.

The Kubernetes cluster-monitoring (cAdvisor 315) pack bridges a pre-1.16
cAdvisor lineage: `label_rewrites` map `pod_name`/`container_name` to the modern
`labels.pod`/`labels.container`, and `ignored_labels` strip dead selector
matchers (`kubernetes_io_hostname`, `image`, `name`) so the per-pod/per-container
panels are not filtered to empty. Its `$Node` variable is dropped as inert —
modern cAdvisor container series carry no node label. Because the native PROMQL
DSL rewrites a grouped metric but leaves the Lens breakdown accessor bound to the
pre-rewrite label (a `by (pod_name)` panel then fails with "invalid column" once
`pod_name` becomes `labels.pod`), the pack emits explicit `query_overrides` for
the Pods CPU/memory panels so the ES|QL output column and the Lens breakdown stay
aligned on `labels.pod`, and for the Pods/All-processes network panels so the
transmit series is named `Sent` rather than the fused `Value B`. All-processes
panels group by the cAdvisor cgroup `id` (`labels.id`, excluding the root
cgroup). The obsolete container-runtime series are honest gaps
rather than fabricated aggregates: docker (`name!~"^k8s_"`) and rkt
(`rkt_container_name`) targets are dropped from the multi-runtime container
panels (disclosed as an approximation), and the systemd system-service panels
(`systemd_service_name`, a pre-labelmap relabel that no longer exists) degrade to
an honest empty via an impossible-filter override instead of erroring on a
non-existent breakdown column. The cluster-total KPI strip additionally needs the
node `machine_*` metrics and the root-cgroup (`id="/"`) + `container_fs_*` series
to populate.

Each pack is registered in `curated_packs/registry.yaml` with a
`gnet_revision` and `dashboard_sha256` — maintainer-verified provenance pins
recording the exact grafana.com revision the pack authors read, re-checkable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements.
# SPDX-License-Identifier: Elastic-2.0
#
# Fidelity manifest — Grafana dashboard 315 (Kubernetes cluster monitoring via Prometheus)
# https://grafana.com/grafana/dashboards/315-kubernetes-cluster-monitoring-via-prometheus/
#
# PERFECT = same information as Grafana on a modern cAdvisor scrape.
# APPROXIMATE = documented delta (dropped obsolete runtime series, id/device gaps).
# GAP = renders empty on modern data because the underlying series/label
# no longer exists (obsolete container runtimes, pre-1.16 labels).
#
# The dashboard is cAdvisor-only (container_* + machine_*). Panels marked
# "requires: machine_* / container_fs_*" render on real data only when the
# target cAdvisor scrape emits the node machine metrics and the root cgroup
# (id="/") filesystem series; otherwise they are an honest field/data gap.

schema_version: 1
gnet_id: 315
gnet_revision: 3
dashboard_title: "Kubernetes cluster monitoring (via Prometheus)"
maintainer: "community"

panels:
# --- Network I/O pressure -----------------------------------------------
- title: "Network I/O pressure"
fidelity: PERFECT
notes: "sum(rate(container_network_receive/transmit_bytes_total)) butterfly; engine emits Sent as the negated transmit rate."

# --- Total usage (cluster KPIs) -----------------------------------------
- title: "Cluster memory usage"
fidelity: APPROXIMATE
notes: "sum(container_memory_working_set_bytes{id=/})/sum(machine_memory_bytes)*100. requires: machine_memory_bytes + root-cgroup (id=/) series. Cross-metric ratio — PERFECT under native PROMQL."
- title: "Cluster CPU usage (1m avg)"
fidelity: APPROXIMATE
notes: "sum(rate(container_cpu_usage_seconds_total{id=/}))/sum(machine_cpu_cores)*100. requires: machine_cpu_cores + id=/ series."
- title: "Cluster filesystem usage"
fidelity: APPROXIMATE
notes: "container_fs_usage_bytes/container_fs_limit_bytes*100 with device=~/dev/[sv]d.. filter. requires: container_fs_* with a matching device label (the classic Grafana N/A when the device regex matches nothing)."
- title: "Used" # memory
fidelity: APPROXIMATE
notes: "sum(container_memory_working_set_bytes{id=/}). requires: root-cgroup id=/ series."
- title: "Total" # memory
fidelity: APPROXIMATE
notes: "sum(machine_memory_bytes). requires: machine_memory_bytes."
- title: "Used" # cpu (cores)
fidelity: APPROXIMATE
notes: "sum(rate(container_cpu_usage_seconds_total{id=/})). requires: id=/ series."
- title: "Total" # cpu (cores)
fidelity: APPROXIMATE
notes: "sum(machine_cpu_cores). requires: machine_cpu_cores."
- title: "Used" # filesystem
fidelity: APPROXIMATE
notes: "sum(container_fs_usage_bytes{device,id=/}). requires: container_fs_usage_bytes."
- title: "Total" # filesystem
fidelity: APPROXIMATE
notes: "sum(container_fs_limit_bytes{device,id=/}). requires: container_fs_limit_bytes."

# --- Per-pod workload (the core, renders on modern cAdvisor) -------------
- title: "Pods CPU usage (1m avg)"
fidelity: PERFECT
notes: "sum(rate(container_cpu_usage_seconds_total)) by pod. Curated ES|QL override groups by labels.pod directly: the native PROMQL path rewrites the metric but leaves the Lens breakdown accessor on the pre-rewrite `pod_name`, which fails as 'invalid column' after pod_name -> labels.pod. Explicit ES|QL keeps query output and breakdown aligned and excludes the root cgroup (id=/) series (no pod label)."
- title: "Pods memory usage"
fidelity: PERFECT
notes: "sum(container_memory_working_set_bytes) by pod. Same curated ES|QL breakdown-alignment override as Pods CPU."
- title: "Pods network I/O (1m avg)"
fidelity: PERFECT
notes: "receive positive, transmit negative, by pod. Curated ES|QL names the series Received/Sent; the native butterfly fusion otherwise labels transmit as Value B once a pod breakdown is present."

# --- Per-container workload ---------------------------------------------
- title: "Containers CPU usage (1m avg)"
fidelity: APPROXIMATE
notes: "Curated ES|QL keeps the k8s pod/container series (excludes the POD pause container); dropped the obsolete docker (name!~^k8s_) and rkt (rkt_container_name) targets."
- title: "Containers memory usage"
fidelity: APPROXIMATE
notes: "As above; per pod/container working set."
- title: "Containers network I/O (1m avg)"
fidelity: APPROXIMATE
notes: "As above; received/sent by pod. Docker/rkt series dropped."

# --- Obsolete-runtime and pre-1.16 panels (honest gaps) -----------------
- title: "System services CPU usage (1m avg)"
fidelity: GAP
notes: "systemd_service_name is a pre-labelmap cAdvisor convention absent on modern k8s. Curated ES|QL override filters on an impossible container value so the panel degrades to an honest empty (data_gap) instead of a Lens 'invalid column' error on the non-existent systemd_service_name breakdown."
- title: "System services memory usage"
fidelity: GAP
notes: "systemd_service_name absent on modern k8s; same impossible-filter override as System services CPU so the panel renders empty rather than erroring."
- title: "All processes CPU usage (1m avg)"
fidelity: APPROXIMATE
notes: "Groups by the cAdvisor cgroup `id` (excludes the root cgroup id=/). Curated ES|QL keeps the Lens breakdown on labels.id. Renders per-cgroup series when the scrape emits `id` on container metrics."
- title: "All processes memory usage"
fidelity: APPROXIMATE
notes: "As above; grouped by cgroup id."
- title: "All processes network I/O (1m avg)"
fidelity: APPROXIMATE
notes: "As above; grouped by cgroup id with Received/Sent series names (native butterfly would emit Value B)."

summary:
total_panels: 21
perfect: 4
approximate: 15
gap: 2
notes: >
The dashboard's per-pod/per-container workload panels (the operational core)
render on any modern cAdvisor scrape after the pre-1.16 label bridge. The
cluster-total KPI strip requires the node machine_* metrics and the root
cgroup (id="/") + container_fs_* series. The systemd and docker/rkt runtime
series are permanent gaps: those container runtimes no longer exist, so the
pack drops them rather than fabricate a single misleading aggregate.
Loading
Loading