Skip to content

Identify hosts in multi-node panels - #63

Open
kapantzak wants to merge 5 commits into
masterfrom
fix-ticket-835
Open

Identify hosts in multi-node panels#63
kapantzak wants to merge 5 commits into
masterfrom
fix-ticket-835

Conversation

@kapantzak

@kapantzak kapantzak commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Problem

A panel covering several nodes could not show which host each series belonged to. Reported in support ticket 835: disk usage of / across 13 hosts produced 13 indistinguishable series.

The series name came from result.labels, which carries machine-readable ids, and the only translation was a machine-GUID lookup against summary.nodes:

// src/datasource.ts (before)
fields: response.data.result.labels.map((id: string, i: number) => {
  const node = response.data.summary.nodes.find((n: any) => n.mg === id);
  return { name: node?.nm || id, ... };
})

That lookup only matches when grouping by node (where the id is the machine GUID). Grouping by a label makes the id the label value, so every host came back as /. On top of that, Grouping by was single-select, so "per node and per mount point" was not expressible, and no Grafana field labels were emitted, so nothing could name a series by its host.

What the API already provides

Verified against netdata/cloud-charts-service (internal/model/data.go, internal/aggregation/data_v2_metadata.go) and a recorded response fixture in that repo:

Field Content
result.labels ["time", ...ids] — machine-readable
view.dimensions.names human-readable names, index-aligned with the above minus "time"
view.dimensions.labels { "<key>": [[values], ...] }, same alignment — only when the request sends the group-by-labels option

aggregations.metrics[].group_by accepts combined values (["node","label"]) alongside group_by_label.

Changes

Naming and labels

  • Name series from view.dimensions.names instead of the raw ids (src/shared/utils/series.ts).
  • Attach Field.labels from view.dimensions.labels plus the node identity.
  • Send the group-by-labels option, without which those labels are absent.
  • Recover the node when the grouping does not encode it: with a single node in scope every series belongs to it; with several the series is a genuine cross-node aggregate and stays unlabelled. summary.nodes is the only valid source — agents names whichever agent served the query, which is routinely a different node.

Grouping

  • Grouping by is now a multi-select mapping to group_by / group_by_label (src/shared/utils/grouping.ts), so one query can split per node and per label.
  • The single-string groupBy of already saved dashboards keeps working (normalizeGroupBy) — no dashboard migration needed.

Legend

  • New Legend field in the query editor (src/shared/utils/legend.ts): {{node}}, any other label key, and {{name}} for the name the query returned. Renders into config.displayNameFromDS, so a panel-level Display name still overrides it. Empty leaves naming to Grafana, preserving its cross-query disambiguation.
  • Documented in src/README.md.

Unrelated defects fixed on the way

  • The chart-data event was published on one global topic that every query editor row subscribed to, so with multiple rows each editor's Dimensions and Filter option lists came from whichever query answered last. Now scoped per query (CHART_DATA.<refId>).
  • Bumped the patch version to 3.0.5. Grafana keys plugin asset caching on the version; every build shipping as 3.0.4 left browsers serving a stale module.js after a deploy, making it impossible to tell whether a change had landed. %VERSION% in plugin.json is substituted from package.json at build time, so the one bump also renames the CI artifact to netdata-datasource-3.0.5.zip.
  • Stopped tracking .eslintcache and added it to .gitignore. It was swept into the toolkit migration (b441723) by accident; eslint keys it on absolute paths, so the committed copy — whose entries point at a different working copy entirely — could never be a cache hit for anyone, while yarn lint --cache rewrote it on every run and dirtied the tree. Untracked rather than deleted, so local lint caching keeps working. CI runs the build, not lint, so it is unaffected.

Implementation note

Netdata joins the group-by parts of an id with , (and @ for instance@machine_guid) without escaping, so ids cannot be split back apart reliably. Label values are therefore read straight from view.dimensions.labels, and the node is found by matching any separated part against a known machine GUID.

Result

Verified against two real captured responses (single node per query, grouped by dimension — the shape that previously produced identical legends):

Legend Query A Query B
(empty) used used
{{node}} plaka-parent costa-desktop
{{node}} - {{mount_point}} plaka-parent - / costa-desktop - /

And the intended single-query path — Nodes = all, Grouping by = node + mount_point, Filter mount_point = / — names series by hostname directly.

Verification

yarn typecheck  → 0 errors
yarn lint       → 0 errors (13 pre-existing deprecation warnings)
yarn build      → ok
yarn test:ci    → 32 passed / 32 across 5 new suites

Notes for reviewers

  • Not yet exercised against a live space. The response contract is verified from the Cloud service source, a recorded fixture, and two real captured responses, but the built plugin has not been run against real data in Grafana yet.
  • Legend text changes for existing dashboards: raw GUIDs and bare label values become hostnames. Panel overrides keyed on the old strings will need re-pointing.
  • No CHANGELOG entry, deliberately. CHANGELOG.md holds only 2.0.0 and 1.0.12 — the whole 3.x line, including the current 3.0.4, was never recorded. A lone 3.0.5 entry would be inconsistent with how the file has actually been maintained; reviving it properly means covering 3.0.0–3.0.5 as separate work.
  • Pre-existing test failures, untouched by this branch: the 5 src/shared/hooks/useFetch*.test.ts suites fail with SyntaxError: Unexpected token 'export', a jest ESM-transform issue via marked@grafana/data. Left alone deliberately; it is why buildGrouping lives in its own dependency-free module rather than being exported from the hook.
  • Grouping by uses MultiSelect rather than Select isMulti, which is what typechecks correctly for a multi-value handler.
Screenshot 2026-09-11 at 15 42 19

Series were named from result.labels, which carries machine-readable ids,
with a node lookup that only applied when grouping by node. Grouping by a
label produced the same legend entry for every host (e.g. "/"), so a panel
covering several nodes could not be read.

- name series from view.dimensions.names, the human-readable counterpart
  that is index-aligned with result.labels
- attach Grafana field labels from view.dimensions.labels plus the node
  identity, so {{node}} legend overrides and transformations work
- request the group-by-labels option, without which those labels are absent
  from the response
- allow several groupings in one query, so a single query can separate
  series per node and per label at the same time
- keep accepting the single-string groupBy of already saved dashboards
- scope the chart-data event per query, so sibling editor rows stop
  overwriting each other's dimension and filter options
Grouping by dimension leaves the node out of the series id, so a query
pinned to a single node produced a legend with no way to tell which host
it came from - two such queries in one panel were identical.

Fall back to the only node in summary.nodes when the id carries none: with
one node in scope every series belongs to it, while several nodes mean the
series really is a cross-node aggregate and stays unlabelled.

summary.nodes is the only valid source - agents names whichever agent
served the query, which is routinely a different node.
The series labels were only reachable through Grafana's Display name field
option and its ${__field.labels.x} syntax, which is obscure enough that the
labels were effectively unusable.

Add a Legend template to the query editor, the control users expect from
other Grafana data sources: {{node}} and any other label key resolve per
series, {{name}} is the name the query returned, and an unknown token
resolves to an empty string. The rendered value goes to displayNameFromDS,
so a panel-level Display name still overrides it.

An empty template leaves the field unnamed, keeping Grafana's own naming
and its disambiguation of series that share a name across queries.
Grafana keys plugin asset caching on the version, so every build shipping
as 3.0.4 left browsers serving a stale module.js after a deploy, making it
impossible to tell whether a change had actually landed.
The file was swept into the toolkit migration by accident and never
maintained. eslint keys it on absolute paths, so the committed copy - whose
entries point at a different working copy entirely - can never be a cache
hit for anyone, while `yarn lint --cache` rewrites it on every run and
dirties the tree.

Untracked rather than deleted, so local lint caching keeps working. CI is
unaffected; it runs the build, not lint.
@kapantzak
kapantzak requested review from witalisoft and removed request for witalisoft September 11, 2026 12:34
@kapantzak
kapantzak marked this pull request as ready for review September 11, 2026 12:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant