docs: user-oriented charttpl README rewrite (#22049)
Ilya Mashchenko committed
Mar 26, 2026 at 18:38 UTC
421c60bb0d35ac1f741b31b3f4b96972ca82e9ba
1 file changed
+998
-155
src/go/plugin/framework/charttpl/README.md
+998
-155
@@ -1,193 +1,1036 @@
1
-# charttpl
1
+# Chart Template Format
2
3
-`charttpl` defines the chart-template DSL used by `ModuleV2` collectors.
4
-It is the input schema consumed by `chartengine`.
3
+## Overview
4
6
-**Audience**: `ModuleV2` collector authors and framework contributors.
5
+A **chart template** defines _how a collector's metrics are organized into charts_ in the Netdata dashboard.
6
8
-**See also**: [metrix](/src/go/pkg/metrix/README.md) (metrics storage and read API),
9
-[chartengine](/src/go/plugin/framework/chartengine/README.md) (compile + plan).
7
+> [!NOTE]
8
+> Chart templates are declarative YAML files. You describe **what** to chart, and the engine handles creating, updating, and removing chart instances at runtime.
9
11
-## Purpose
10
+It tells the chart engine:
11
13
-| Package | Role |
14
-|---------------|------------------------------------------------------------------------------|
15
-| `charttpl` | Parse + default + semantic validation of chart-template YAML |
16
-| `chartengine` | Compile template into immutable program and build create/update/remove plans |
12
+- which **metrics** to include
13
+- how to **group** them into charts and families
14
+- how to **name** and **scale** dimensions
15
+- how to create **per-instance charts** (e.g., one chart per host, per disk, per user)
16
18
-## Processing Pipeline
17
+Each collector has a single `charts.yaml` file that describes all its charts.
18
20
-1. **Decode** — Strict YAML decode (`yaml.UnmarshalStrict`). Unknown fields fail decoding.
21
-2. **Defaults** — `version` defaults to `v1` (top-level only); chart `type` defaults to `line`; inheritable `group.chart_defaults` are applied recursively using nearest-group-wins replace semantics.
22
-3. **Semantic validation** — Field-level checks with path-aware errors (metric scoping, selectors, dimension rules, lifecycle bounds).
19
+> [!TIP]
20
+> Groups can be nested to **any depth**. Family paths, context namespaces, and metric scopes compose automatically as you nest — no need to repeat prefixes. See [groups](#4-groups) for the full composition rules and examples.
21
24
-## Syntax (v1)
22
+### How Chart Templates Work
23
26
-### Top-level fields
24
+When a collector runs, the chart engine:
25
28
-| Field | Type | Required | Description |
29
-|---------------------|--------|----------|---------------------------------------------------------------|
30
-| `version` | string | no | Must be `v1` (defaults to `v1`) |
31
-| `context_namespace` | string | no | Prefix for chart context path (see context composition below) |
32
-| `engine` | object | no | Template-level chartengine policy |
33
-| `groups` | array | yes | Recursive chart groups |
26
+1. Reads the collector's `charts.yaml` file.
27
+2. Compiles it into an immutable program (validates, resolves defaults, infers algorithms).
28
+3. On each collection cycle, matches incoming metrics against dimension selectors.
29
+4. Creates chart instances dynamically based on instance identity labels.
30
+5. Updates dimension values every cycle; removes stale instances based on lifecycle policy.
31
35
-### `groups[]`
32
+**Template Lifecycle**
33
37
-| Field | Type | Required | Description |
38
-|---------------------|---------------|----------|--------------------------------------------------------------------------------------|
39
-| `family` | string | yes | Family segment used for chart family composition |
40
-| `context_namespace` | string | no | Context segment appended to inherited context namespace |
41
-| `metrics` | array[string] | no | Metrics available for dimension selectors in this group (inherited by nested groups) |
42
-| `chart_defaults` | object | no | Inheritable chart fields for descendant charts in this group subtree |
43
-| `charts` | array | no | Chart definitions |
44
-| `groups` | array | no | Nested groups |
34
+```text
35
+ charts.yaml
36
+ |
37
+ v
38
+ ┌─────────────────────┐
39
+ │ Decode & Validate │ strict YAML parse + semantic checks
40
+ └──────────┬──────────┘
41
+ v
42
+ ┌─────────────────────┐
43
+ │ Compile (engine) │ selector parsing, algorithm inference,
44
+ │ │ context/family/ID composition
45
+ └──────────┬──────────┘
46
+ v
47
+ ┌─────────────────────┐
48
+ │ Runtime (per cycle)│ match series → create/update/remove
49
+ │ │ charts and dimensions
50
+ └─────────────────────┘
51
+```
52
+
53
+### Example: Complete Chart Template
54
+
55
+The example below shows a single template that covers all common metric kinds:
56
+gauge, counter, histogram, summary, and stateset. Inline comments explain each field.
57
+
58
+```yaml
59
+version: v1 # schema version (only "v1" supported)
60
+context_namespace: myapp # prefix for all chart contexts → myapp.<group context>.<chart context>
61
+
62
+groups:
63
+ # ── Gauge: point-in-time values (algorithm: absolute) ──────────────
64
+ - family: Resources
65
+ metrics: # metrics visible to dimension selectors in this group
66
+ - memory_used_bytes
67
+ - memory_total_bytes
68
+ - cpu_usage_percent
69
+ charts:
70
+ - id: memory_usage
71
+ title: Memory Usage
72
+ context: memory_usage # final context: myapp.memory_usage
73
+ units: bytes
74
+ type: stacked # line (default), area, stacked, heatmap
75
+ algorithm: absolute # absolute = raw value; incremental = rate (value - prev) / interval
76
+ instances:
77
+ by_labels: [host] # one chart per unique "host" label value
78
+ dimensions:
79
+ - selector: memory_used_bytes
80
+ name: used # static dimension name
81
+ - selector: memory_total_bytes
82
+ name: total
83
+ options:
84
+ hidden: true # collected but not drawn (useful for % calculations)
85
+
86
+ - id: cpu_usage
87
+ title: CPU Usage
88
+ context: cpu_usage
89
+ units: percentage
90
+ instances:
91
+ by_labels: [host]
92
+ dimensions:
93
+ - selector: cpu_usage_percent
94
+ name: used
95
+ options:
96
+ divisor: 100 # raw value in basis points → divide by 100 for percent
97
+ float: true # use floating-point precision
98
+
99
+ # ── Counter: monotonically increasing values (algorithm: incremental) ──
100
+ - family: Traffic
101
+ metrics:
102
+ - http_requests_total
103
+ - bytes_received
104
+ - bytes_sent
105
+ charts:
106
+ - id: http_requests
107
+ title: HTTP Requests
108
+ context: http_requests
109
+ units: requests/s
110
+ algorithm: incremental # engine computes rate: (current - previous) / interval
111
+ instances:
112
+ by_labels: [host]
113
+ dimensions:
114
+ - selector: http_requests_total
115
+ name_from_label: method # dynamic name: each unique label value becomes a dimension
116
+ # e.g., method="GET" → dim "GET", method="POST" → dim "POST"
117
+
118
+ - id: bandwidth
119
+ title: Network Bandwidth
120
+ context: bandwidth
121
+ units: kilobits/s
122
+ type: area
123
+ algorithm: incremental
124
+ instances:
125
+ by_labels: [host]
126
+ dimensions:
127
+ - selector: bytes_received
128
+ name: in
129
+ options:
130
+ multiplier: 8 # bytes → bits
131
+ divisor: 1000 # bits → kilobits
132
+ - selector: bytes_sent
133
+ name: out
134
+ options:
135
+ multiplier: -8 # negative = drawn below zero line (bidirectional chart)
136
+ divisor: 1000
137
+
138
+ # ── Histogram: bucketed distribution (flattened into _bucket, _count, _sum) ──
139
+ - family: Latency
140
+ metrics:
141
+ - request_duration_seconds_bucket
142
+ - request_duration_seconds_count
143
+ - request_duration_seconds_sum
144
+ charts:
145
+ - id: request_duration_buckets
146
+ title: Request Duration Buckets
147
+ context: request_duration_buckets
148
+ units: observations/s
149
+ type: stacked
150
+ algorithm: incremental # histogram buckets are counters
151
+ instances:
152
+ by_labels: [host]
153
+ dimensions:
154
+ - selector: request_duration_seconds_bucket
155
+ # no name, no name_from_label → engine infers dimension names
156
+ # from the "le" (less-than-or-equal) label automatically:
157
+ # le="0.005" → dim "0.005", le="0.01" → dim "0.01", etc.
158
+
159
+ - id: request_rate
160
+ title: Request Rate
161
+ context: request_rate
162
+ units: requests/s
163
+ algorithm: incremental
164
+ instances:
165
+ by_labels: [host]
166
+ dimensions:
167
+ - selector: request_duration_seconds_count
168
+ name: requests
169
+
170
+ # ── Summary: quantile distribution (flattened into quantile values, _count, _sum) ──
171
+ - family: Response Time
172
+ metrics:
173
+ - response_time_seconds
174
+ - response_time_seconds_count
175
+ - response_time_seconds_sum
176
+ charts:
177
+ - id: response_time_quantiles
178
+ title: Response Time Quantiles
179
+ context: response_time_quantiles
180
+ units: seconds
181
+ algorithm: absolute # quantile values are gauges, not counters
182
+ instances:
183
+ by_labels: [host]
184
+ dimensions:
185
+ - selector: response_time_seconds
186
+ # no name, no name_from_label → engine infers dimension names
187
+ # from the "quantile" label automatically:
188
+ # quantile="0.5" → dim "0.5", quantile="0.99" → dim "0.99", etc.
189
+ options:
190
+ float: true
191
+
192
+ # ── StateSet: named boolean states (exactly one active at a time) ──
193
+ - family: Health
194
+ metrics:
195
+ - service_status
196
+ charts:
197
+ - id: service_health
198
+ title: Service Health Status
199
+ context: service_health
200
+ units: state
201
+ instances:
202
+ by_labels: [host]
203
+ dimensions:
204
+ - selector: service_status
205
+ # no name, no name_from_label → engine infers dimension names
206
+ # from the metric-name label (service_status=<value>) automatically:
207
+ # service_status="ready" → dim "ready", service_status="degraded" → dim "degraded", etc.
208
+```
209
+
210
+**What this template produces** — if the collector reports metrics for 2 hosts (`host="web-1"`, `host="web-2"`), the engine creates **2 instances of every chart** (one per host). The histogram bucket chart gets one dimension per `le` boundary, the summary chart gets one per quantile, and the stateset chart gets one per state — all named automatically by the engine.
211
+
212
+## Template Structure
213
+
214
+Every `charts.yaml` follows this structure:
215
+
216
+```yaml
217
+version: <schema version>
218
+context_namespace: <context prefix>
219
+engine: <engine policy>
220
+groups:
221
+ - family: <family name>
222
+ context_namespace: <context segment>
223
+ metrics: <available metrics>
224
+ chart_defaults: <inheritable defaults>
225
+ charts: <chart definitions>
226
+ groups: <nested groups>
227
+```
228
+
229
+| Section | Purpose |
230
+|-----------------------------------------------|----------------------------------------------------|
231
+| [**version**](#1-version) | Schema version (must be `v1`). |
232
+| [**context_namespace**](#2-context_namespace) | Top-level prefix for chart context paths. |
233
+| [**engine**](#3-engine) | Engine-level policy (selectors, autogeneration). |
234
+| [**groups**](#4-groups) | Recursive chart groups — the core of the template. |
235
+
236
+---
237
46
-**Context composition**: The final chart context is built by joining all context parts with `.`:
47
-`<top context_namespace>.<group context_namespace>...<chart.context>`.
48
-For example, with top-level `context_namespace: netdata.go.plugin`, group `context_namespace: mysql`,
49
-and chart `context: queries`, the resulting context is `netdata.go.plugin.mysql.queries`.
238
+## Field Reference
239
+
240
+### 1. version
241
+
242
+Schema version. Currently only `v1` is supported. Defaults to `v1` if omitted.
243
+
244
+```yaml
245
+version: v1
246
+```
247
+
248
+### 2. context_namespace
249
+
250
+Top-level prefix for all chart context paths in the template. Combined with group-level `context_namespace` and chart `context` to form the final context.
251
+
252
+```yaml
253
+context_namespace: mysql
254
+```
255
+
256
+**Context composition** — the final chart context is built by joining all context parts with `.`:
257
+
258
+```
259
+<top context_namespace>.<group context_namespace>...<chart.context>
260
+```
261
51
-### `chart_defaults`
262
+For example:
263
53
-`chart_defaults` applies only to descendant charts of the current group. Supported fields are:
264
+| Level | Value |
265
+|-------------------------------|---------------------|
266
+| Top-level `context_namespace` | `mysql` |
267
+| Group `context_namespace` | _(empty)_ |
268
+| Chart `context` | `queries` |
269
+| **Resulting context** | **`mysql.queries`** |
270
+
271
+### 3. engine
272
+
273
+Template-level policy that controls metric filtering and autogeneration.
274
+
275
+```yaml
276
+engine:
277
+ selector:
278
+ allow: ["cpu_*", "memory_*"]
279
+ deny: ["cpu_guest_*"]
280
+ autogen:
281
+ enabled: true
282
+ expire_after_success_cycles: 50
283
+```
284
+
285
+| Field | Type | Default | Description |
286
+|---------------------------------------|---------------|-------------|----------------------------------------------------------------------------------------|
287
+| `selector.allow` | array[string] | _(empty)_ | Include only metrics matching these patterns (simple patterns: `*` and `?` wildcards). |
288
+| `selector.deny` | array[string] | _(empty)_ | Exclude metrics matching these patterns (simple patterns: `*` and `?` wildcards). |
289
+| `autogen.enabled` | bool | `false` | Create charts for metrics not matched by any template dimension. |
290
+| `autogen.max_type_id_len` | int | `0` (=1200) | Max full `type.id` length. Must be `0` or `>= 4`. |
291
+| `autogen.expire_after_success_cycles` | uint64 | `0` | Remove autogenerated charts not seen for N successful cycles (`0` = never). |
292
+
293
+**When to use autogen**: For collectors like Nagios plugins where the set of metrics is unpredictable and user-defined. The engine creates a chart for every unmatched metric automatically.
294
+
295
+**Example: Nagios collector with autogeneration**
296
+
297
+```yaml
298
+version: v1
299
+context_namespace: nagios
300
+engine:
301
+ autogen:
302
+ enabled: true
303
+ expire_after_success_cycles: 50
304
+groups:
305
+ - family: Job
306
+ context_namespace: job
307
+ groups:
308
+ - family: Execution
309
+ metrics:
310
+ - nagios.job.execution_state
311
+ - nagios.job.execution_duration
312
+ charts:
313
+ - id: job_execution_state
314
+ title: Job Execution State
315
+ context: execution_state
316
+ units: state
317
+ instances:
318
+ by_labels: [nagios_job]
319
+ dimensions:
320
+ - selector: nagios.job.execution_state
321
+ - id: job_execution_duration
322
+ title: Execution Duration
323
+ context: execution_duration
324
+ units: seconds
325
+ instances:
326
+ by_labels: [nagios_job]
327
+ dimensions:
328
+ - selector: nagios.job.execution_duration
329
+ name: duration
330
+ options:
331
+ float: true
332
+```
333
+
334
+Explicitly defined charts (like `execution_state`) use the template. Any _other_ metrics the Nagios plugin emits get auto-charted by the engine.
335
+
336
+### 4. groups
337
+
338
+Groups organize charts into a hierarchy that can be nested to **any depth**. Each group defines a **family** segment, can declare **metrics** in scope, and contains **charts** and/or nested **groups**.
339
+
340
+Nesting serves three purposes:
341
+
342
+1. **Family composition** — each level's `family` is joined with `/`, producing Netdata's hierarchical family structure automatically (the UI renders `/`-separated families as navigable levels).
343
+2. **Context composition** — each level's `context_namespace` is joined with `.`, so you write short context leaves instead of long prefixed strings.
344
+3. **Metric scoping** — metrics declared in a group are inherited by all descendants, so you declare once at the appropriate level.
345
+
346
+```yaml
347
+groups:
348
+ - family: <family name>
349
+ context_namespace: <optional context segment>
350
+ metrics:
351
+ - <metric_name>
352
+ chart_defaults:
353
+ label_promotion: [<label>, ...]
354
+ instances:
355
+ by_labels: [<label>, ...]
356
+ charts:
357
+ - <chart definition>
358
+ groups:
359
+ - <nested group>
360
+```
361
+
362
+| Field | Type | Required | Description |
363
+|---------------------|---------------|----------|-------------------------------------------------------------------------------------|
364
+| `family` | string | **yes** | Family segment. Groups compose the chart family hierarchy. |
365
+| `context_namespace` | string | no | Context segment appended to inherited context namespace. |
366
+| `metrics` | array[string] | no | Metrics visible to dimension selectors in this group and descendants. |
367
+| `chart_defaults` | object | no | Inheritable defaults for descendant charts (see [chart_defaults](#chart_defaults)). |
368
+| `charts` | array | no | Chart definitions (see [charts](#5-charts)). |
369
+| `groups` | array | no | Nested groups (recursive). |
370
+
371
+**Family composition** — group families compose hierarchically. The final chart family is built by joining all group `family` segments and the chart's own `family` (if set) with `/`:
372
+
373
+| Level | Family value |
374
+|----------------------|-----------------------------------------|
375
+| Root group | `Storage Engine` |
376
+| Nested group | `InnoDB` |
377
+| Nested group | `Buffer Pool` |
378
+| **Resulting family** | **`Storage Engine/InnoDB/Buffer Pool`** |
379
+
380
+Here is a real-world nesting example showing how family and context compose at each level:
381
+
382
+```yaml
383
+# context_namespace: mysql (set at top level)
384
+groups: # family context
385
+ - family: Storage Engine # Storage Engine (inherited)
386
+ groups:
387
+ - family: InnoDB # Storage Engine/InnoDB (inherited)
388
+ groups:
389
+ - family: Buffer Pool # Storage Engine/InnoDB/Buffer Pool
390
+ charts:
391
+ - context: pages # → mysql.pages
392
+ - family: I/O # Storage Engine/InnoDB/I/O
393
+ charts:
394
+ - context: bandwidth # → mysql.bandwidth
395
+ - family: MyISAM # Storage Engine/MyISAM
396
+ charts:
397
+ - context: key_blocks # → mysql.key_blocks
398
+```
399
+
400
+Without nesting, you would repeat `Storage Engine/InnoDB/` in every chart's family and `mysql.` in every context. Nesting eliminates that repetition and makes the structure self-documenting.
401
+
402
+> [!WARNING]
403
+> Dimensions can only reference metrics declared in their group or any ancestor group. Referencing a metric not in scope produces a validation error.
404
+
405
+**Metric scoping** — this prevents accidental cross-references and keeps templates self-documenting:
406
+
407
+```yaml
408
+groups:
409
+ - family: Database
410
+ metrics:
411
+ - queries_total # visible to all charts in this group and nested groups
412
+ groups:
413
+ - family: Cache
414
+ metrics:
415
+ - cache_hits # visible only in this group and its descendants
416
+ charts:
417
+ - title: Cache Performance
418
+ context: cache
419
+ units: hits/s
420
+ dimensions:
421
+ - selector: cache_hits # OK — declared in this group
422
+ name: hits
423
+ - selector: queries_total # OK — inherited from parent group
424
+ name: queries
425
+```
426
+
427
+#### chart_defaults
428
+
429
+Inheritable chart configuration applied to all descendant charts in the group subtree. Useful when many charts share the same instance identity or label promotion policy.
430
431
| Field | Type | Description |
432
|-------------------|---------------|------------------------------------------|
57
-| `label_promotion` | array[string] | Default chart label promotion policy |
58
-| `instances` | object | Default chart instance identity policy |
59
-
60
-Inheritance rules:
61
-
62
-- nearest group default wins
63
-- chart-local field overrides inherited default
64
-- list/object fields replace the inherited field wholesale
65
-- no deep merge or append semantics
66
-
67
-### `charts[]`
68
-
69
-| Field | Type | Required | Description |
70
-|-------------------|---------------|----------|----------------------------------------------------------------------------------|
71
-| `id` | string | no | Base chart ID template (if omitted, derived from `context`) |
72
-| `title` | string | yes | Chart title |
73
-| `family` | string | no | Optional chart-level family leaf |
74
-| `context` | string | yes | Chart context leaf |
75
-| `units` | string | yes | Chart units |
76
-| `algorithm` | string | no | `absolute` or `incremental` |
77
-| `type` | string | no | `line`, `area`, `stacked`, `heatmap` (defaults to `line`) |
78
-| `priority` | int | no | Chart priority |
79
-| `label_promotion` | array[string] | no | Labels to promote as chart labels (visible in chart metadata, used for grouping) |
80
-| `instances` | object | no | Instance identity policy |
81
-| `lifecycle` | object | no | Instance/dimension cap and expiry policy |
82
-| `dimensions` | array | yes | Dimension selectors and naming |
83
-
84
-### `instances.by_labels`
85
-
86
-Instance identity determines how series are grouped into chart instances.
87
-When multiple series share the same instance identity labels, they appear as dimensions on the same chart.
88
-
89
-| Token | Meaning |
90
-|--------------|-------------------------------------------------|
91
-| `label_key` | Include explicit label key in instance identity |
92
-| `*` | Include all labels |
93
-| `!label_key` | Exclude label key |
94
-
95
-### `lifecycle`
96
-
97
-| Field | Type | Default | Description |
98
-|----------------------------------|------|----------------|---------------------------------------------------------|
99
-| `max_instances` | int | `0` | Best-effort cap (`0` = disabled) |
100
-| `expire_after_cycles` | int | engine default | Expire chart instances not seen for N successful cycles |
101
-| `dimensions.max_dims` | int | `0` | Best-effort dimension cap (`0` = disabled) |
102
-| `dimensions.expire_after_cycles` | int | `0` | Expire dimensions not seen for N successful cycles |
103
-
104
-### `dimensions[]`
105
-
106
-| Field | Type | Required | Description |
107
-|----------------------|--------|----------|----------------------------------------------------------------|
108
-| `selector` | string | yes | Metric selector expression (must include explicit metric name) |
109
-| `name` | string | no | Static dimension name |
110
-| `name_from_label` | string | no | Dynamic dimension name sourced from one label |
111
-| `options.multiplier` | int | no | DIM multiplier (`0` means default `1`) |
112
-| `options.divisor` | int | no | DIM divisor (`0` means default `1`) |
113
-| `options.hidden` | bool | no | Mark dimension hidden |
114
-| `options.float` | bool | no | Emit `type=float` and use `SETFLOAT` updates |
115
-
116
-**Selector syntax**: A selector takes the form `metric_name` or `metric_name{label=value, ...}`.
117
-The metric name prefix is required; label-only selectors like `{label=value}` are rejected.
118
-
119
-### `engine` (template-level policy)
120
-
121
-| Field | Type | Description |
122
-|---------------------------------------|---------------|---------------------------------------------------------------------------|
123
-| `selector.allow` | array[string] | Global include selectors |
124
-| `selector.deny` | array[string] | Global exclude selectors |
125
-| `autogen.enabled` | bool | Enable unmatched-series autogen fallback |
126
-| `autogen.max_type_id_len` | int | Max full `type.id` length (`0` = default; must be `0` or `>= 4` when set) |
127
-| `autogen.expire_after_success_cycles` | uint64 | Autogen lifecycle expiry |
128
-
129
-## Minimal Example
433
+| `label_promotion` | array[string] | Default labels to promote on all charts. |
434
+| `instances` | object | Default instance identity policy. |
435
+
436
+> [!NOTE]
437
+> **Inheritance rules**: nearest group default wins (child overrides parent), chart-local field overrides inherited default, and list/object fields replace the inherited field wholesale — there is no deep merge or append.
438
+
439
+**Example: Azure Monitor — all charts share the same instance identity**
440
+
441
+```yaml
442
+groups:
443
+ - family: Azure Key Vault
444
+ context_namespace: key_vault
445
+ chart_defaults:
446
+ label_promotion: [resource_name, resource_group, region]
447
+ instances:
448
+ by_labels: [resource_uid]
449
+ charts:
450
+ # Every chart below inherits instances and label_promotion
451
+ # without repeating them.
452
+ - id: availability
453
+ title: Azure Key Vault Availability
454
+ context: availability
455
+ units: percentage
456
+ dimensions:
457
+ - selector: key_vault.availability_average
458
+ name: average
459
+ - id: api_latency
460
+ title: Azure Key Vault API Latency
461
+ context: api_latency
462
+ units: milliseconds
463
+ dimensions:
464
+ - selector: key_vault.service_api_latency_average
465
+ name: average
466
+```
467
+
468
+Without `chart_defaults`, you would need to repeat `instances` and `label_promotion` on every chart.
469
+
470
+### 5. charts
471
+
472
+A chart defines a single visualization in the Netdata dashboard.
473
+
474
+```yaml
475
+charts:
476
+ - id: <chart ID>
477
+ title: <chart title>
478
+ family: <optional family leaf>
479
+ context: <chart context>
480
+ units: <units string>
481
+ algorithm: <absolute|incremental>
482
+ type: <line|area|stacked|heatmap>
483
+ priority: <int>
484
+ label_promotion: [<label>, ...]
485
+ instances:
486
+ by_labels: [<label>, ...]
487
+ lifecycle:
488
+ max_instances: <int>
489
+ expire_after_cycles: <int>
490
+ dimensions:
491
+ max_dims: <int>
492
+ expire_after_cycles: <int>
493
+ dimensions:
494
+ - <dimension definition>
495
+```
496
+
497
+| Field | Type | Required | Default | Description |
498
+|-------------------|---------------|----------|------------------------|------------------------------------------------------------------------------|
499
+| `id` | string | no | derived from `context` | Base chart ID. If omitted, derived by replacing `.` with `_` in `context`. |
500
+| `title` | string | **yes** | | Chart title shown in the dashboard. |
501
+| `family` | string | no | | Optional chart-level family leaf, appended to the group family. |
502
+| `context` | string | **yes** | | Chart context leaf. Combined with context namespaces. |
503
+| `units` | string | **yes** | | Chart units (e.g., `queries/s`, `bytes`, `percentage`). |
504
+| `algorithm` | string | no | inferred from metrics | `absolute` or `incremental`. If omitted, inferred from metric suffixes. |
505
+| `type` | string | no | `line` | `line`, `area`, `stacked`, or `heatmap`. |
506
+| `priority` | int | no | `70000` | Chart ordering priority in the dashboard (`0` = use engine default `70000`). |
507
+| `label_promotion` | array[string] | no | from `chart_defaults` | Labels to promote as chart labels (for filtering/grouping in UI). |
508
+| `instances` | object | no | from `chart_defaults` | Instance identity policy (see [instances](#instances)). |
509
+| `lifecycle` | object | no | | Instance/dimension cap and expiry (see [lifecycle](#lifecycle)). |
510
+| `dimensions` | array | **yes** | | At least one dimension required (see [dimensions](#6-dimensions)). |
511
+
512
+> [!TIP]
513
+> When `algorithm` is omitted, the engine infers it from metric name suffixes. You only need to set it explicitly when the suffix doesn't match the intended behavior (e.g., a gauge metric named `*_total`).
514
+
515
+| Suffix | Inferred algorithm |
516
+|-------------------------------------------|--------------------|
517
+| `*_total`, `*_count`, `*_sum`, `*_bucket` | `incremental` |
518
+| Everything else | `absolute` |
519
+
520
+> [!WARNING]
521
+> If a chart's dimensions mix counter-like metrics (e.g., `requests_total`) with gauge-like metrics (e.g., `temperature`) and `algorithm` is omitted, the engine fails with a compile error: _"algorithm inference is ambiguous for mixed metric kinds; set algorithm explicitly"_. Set `algorithm` on the chart to resolve this.
522
+
523
+**Example: MySQL queries — incremental counters displayed as rates**
524
+
525
+```yaml
526
+charts:
527
+ - id: queries
528
+ title: Queries
529
+ context: queries
530
+ units: queries/s
531
+ algorithm: incremental
532
+ dimensions:
533
+ - selector: queries
534
+ name: queries
535
+ - selector: questions
536
+ name: questions
537
+ - selector: slow_queries
538
+ name: slow_queries
539
+```
540
+
541
+**Example: MySQL bandwidth — bidirectional area chart with unit conversion**
542
+
543
+```yaml
544
+charts:
545
+ - id: net
546
+ title: Bandwidth
547
+ context: net
548
+ units: kilobits/s
549
+ type: area
550
+ algorithm: incremental
551
+ dimensions:
552
+ - selector: bytes_received
553
+ name: in
554
+ options:
555
+ multiplier: 8
556
+ divisor: 1000
557
+ - selector: bytes_sent
558
+ name: out
559
+ options:
560
+ multiplier: -8 # negative = below zero line
561
+ divisor: 1000
562
+```
563
+
564
+#### instances
565
+
566
+Instance identity determines how series are grouped into chart instances. When multiple series share the same instance identity label values, they appear as dimensions on the same chart instance.
567
+
568
+> [!TIP]
569
+> Without `instances`, there is one chart instance (all matching series land on the same chart). With `instances`, the engine creates one chart instance per unique combination of the specified label values.
570
+
571
+```yaml
572
+instances:
573
+ by_labels: [host]
574
+```
575
+
576
+| Token | Meaning |
577
+|--------------|---------------------------------------------------------------|
578
+| `label_key` | Include this label in instance identity. |
579
+| `*` | Include all labels. |
580
+| `!label_key` | Exclude this label (use with `*` to include all _except_...). |
581
+
582
+**Example: One chart per host**
583
+
584
+```yaml
585
+instances:
586
+ by_labels: [host]
587
+```
588
+
589
+If the collector reports metrics for hosts `server-1`, `server-2`, `server-3`, the engine creates 3 separate chart instances — each showing only that host's dimensions.
590
+
591
+**Example: One chart per unique (job, instance) combination**
592
+
593
+```yaml
594
+instances:
595
+ by_labels: [nagios_job, perfdata_value]
596
+```
597
+
598
+**Example: All labels except one**
599
+
600
+```yaml
601
+instances:
602
+ by_labels: ["*", "!_collect_job"]
603
+```
604
+
605
+#### lifecycle
606
+
607
+Controls cardinality limits and expiry for chart instances and dimensions.
608
+
609
+| Field | Type | Default | Description |
610
+|----------------------------------|------|----------------|--------------------------------------------------------------------------------------|
611
+| `max_instances` | int | `0` (disabled) | Best-effort cap on chart instances per template. Active instances are never evicted. |
612
+| `expire_after_cycles` | int | `5` | Remove chart instances not seen for N successful collection cycles. |
613
+| `dimensions.max_dims` | int | `0` (disabled) | Best-effort cap on dimensions per chart instance. |
614
+| `dimensions.expire_after_cycles` | int | `0` (disabled) | Remove dimensions not seen for N successful collection cycles. |
615
+
616
+**How lifecycle caps work**:
617
+
618
+- Caps are **best-effort** — instances/dimensions actively seen in the current cycle are never evicted.
619
+- Oldest inactive entries are evicted first (by last-seen time).
620
+- Expiry counters only advance on **successful** collection cycles.
621
+
622
+### 6. dimensions
623
+
624
+A dimension binds a metric from the collector's metric store to a line on the chart.
625
+
626
+```yaml
627
+dimensions:
628
+ - selector: <metric selector>
629
+ name: <static name>
630
+ name_from_label: <label key>
631
+ options:
632
+ multiplier: <int>
633
+ divisor: <int>
634
+ hidden: <bool>
635
+ float: <bool>
636
+```
637
+
638
+| Field | Type | Required | Default | Description |
639
+|----------------------|--------|----------|---------|------------------------------------------------------------------|
640
+| `selector` | string | **yes** | | Metric selector expression (see [selectors](#selectors) below). |
641
+| `name` | string | no | | Static dimension name shown in the chart. |
642
+| `name_from_label` | string | no | | Dynamic name: use the value of this label as the dimension name. |
643
+| `options.multiplier` | int | no | `1` | Multiply the raw value by this factor. |
644
+| `options.divisor` | int | no | `1` | Divide the raw value by this factor. |
645
+| `options.hidden` | bool | no | `false` | Hide this dimension in the chart (still collected). |
646
+| `options.float` | bool | no | `false` | Use floating-point precision for this dimension. |
647
+
648
+> [!IMPORTANT]
649
+> There are three ways to name a dimension — pick **exactly one**:
650
+> - `name` — static name you choose (e.g., `name: read`).
651
+> - `name_from_label` — dynamic name from a label value (e.g., `name_from_label: method` → dimensions "GET", "POST", ...).
652
+> - **Omit both** — the engine infers the name automatically for histogram buckets (`le`), summary quantiles (`quantile`), and statesets.
653
+>
654
+> `name` and `name_from_label` are mutually exclusive. Duplicate static `name` values within the same chart are rejected.
655
+
656
+#### selectors
657
+
658
+A selector specifies which metric(s) a dimension should match.
659
+
660
+**Syntax:**
661
+
662
+```
663
+metric_name
664
+metric_name{label_key=label_value, ...}
665
+```
666
+
667
+- The metric name prefix is **required** — label-only selectors like `{label=value}` are rejected.
668
+- The metric must be declared in the current group's `metrics` list (or inherited from an ancestor group).
669
+- Label filters narrow which series match. Without labels, all series of that metric match.
670
+
671
+**Examples:**
672
+
673
+```yaml
674
+# Match all series of the "queries" metric
675
+- selector: queries
676
+
677
+# Match only series where method="GET"
678
+- selector: http_requests_total{method="GET"}
679
+
680
+# Match a specific histogram bucket
681
+- selector: request_duration_seconds_bucket{le="0.5"}
682
+```
683
+
684
+#### Common dimension patterns
685
+
686
+**Unit conversion** — convert bytes to kilobits per second:
687
+
688
+```yaml
689
+dimensions:
690
+ - selector: bytes_received
691
+ name: in
692
+ options:
693
+ multiplier: 8
694
+ divisor: 1000
695
+```
696
+
697
+**Bidirectional charts** — use a negative multiplier to display below zero:
698
+
699
+```yaml
700
+dimensions:
701
+ - selector: bytes_received
702
+ name: in
703
+ options:
704
+ multiplier: 8
705
+ divisor: 1000
706
+ - selector: bytes_sent
707
+ name: out
708
+ options:
709
+ multiplier: -8
710
+ divisor: 1000
711
+```
712
+
713
+**Float precision** — for ratios or small decimal values:
714
+
715
+```yaml
716
+dimensions:
717
+ - selector: efficiency_ratio
718
+ name: efficiency
719
+ options:
720
+ float: true
721
+```
722
+
723
+**Dynamic naming from labels** — each unique label value becomes a separate dimension:
724
+
725
+```yaml
726
+dimensions:
727
+ - selector: http_requests_total
728
+ name_from_label: method
729
+```
730
+
731
+If the metric has series with `method="GET"`, `method="POST"`, etc., each becomes its own dimension on the chart.
732
+
733
+## Examples
734
+
735
+### Simple: static metrics, no instances
736
+
737
+A collector that monitors a single MySQL server. Each chart has a fixed set of dimensions.
738
739
```yaml
740
version: v1
133
-context_namespace: netdata.go.plugin.example
741
+context_namespace: mysql
742
groups:
135
- - family: HTTP
743
+ - family: Queries
744
+ groups:
745
+ - family: Statistics
746
+ metrics:
747
+ - queries
748
+ - questions
749
+ - slow_queries
750
+ - com_delete
751
+ - com_insert
752
+ - com_select
753
+ - com_update
754
+ charts:
755
+ - id: queries
756
+ title: Queries
757
+ context: queries
758
+ units: queries/s
759
+ algorithm: incremental
760
+ dimensions:
761
+ - selector: queries
762
+ name: queries
763
+ - selector: questions
764
+ name: questions
765
+ - selector: slow_queries
766
+ name: slow_queries
767
+ - id: queries_type
768
+ title: Queries By Type
769
+ context: queries_type
770
+ units: queries/s
771
+ type: stacked
772
+ algorithm: incremental
773
+ dimensions:
774
+ - selector: com_delete
775
+ name: delete
776
+ - selector: com_insert
777
+ name: insert
778
+ - selector: com_select
779
+ name: select
780
+ - selector: com_update
781
+ name: update
782
+```
783
+
784
+### Per-instance: one chart per host
785
+
786
+A ping collector that monitors multiple hosts. Each host gets its own set of charts.
787
+
788
+```yaml
789
+version: v1
790
+context_namespace: ping
791
+groups:
792
+ - family: latency
793
metrics:
137
- - app.requests_total
138
- - app.latency_seconds_bucket
794
+ - min_rtt
795
+ - max_rtt
796
+ - avg_rtt
797
charts:
140
- - id: requests
141
- title: Requests
142
- context: requests
798
+ - id: host_rtt
799
+ title: Ping round-trip time
800
+ context: host_rtt
801
+ units: milliseconds
802
+ type: area
803
+ instances:
804
+ by_labels: [host]
805
+ dimensions:
806
+ - selector: min_rtt
807
+ name: min
808
+ options:
809
+ divisor: 1000
810
+ - selector: max_rtt
811
+ name: max
812
+ options:
813
+ divisor: 1000
814
+ - selector: avg_rtt
815
+ name: avg
816
+ options:
817
+ divisor: 1000
818
+```
819
+
820
+### Per-instance with multiple labels
821
+
822
+MySQL replication monitoring creates one chart per replication connection.
823
+
824
+```yaml
825
+groups:
826
+ - family: Replication
827
+ groups:
828
+ - family: Slave Status
829
+ metrics:
830
+ - seconds_behind_master
831
+ - slave_io_running
832
+ - slave_sql_running
833
+ charts:
834
+ - id: slave_behind
835
+ title: Slave Behind Seconds
836
+ context: slave_behind
837
+ units: seconds
838
+ instances:
839
+ by_labels: [connection]
840
+ dimensions:
841
+ - selector: seconds_behind_master
842
+ name: seconds
843
+ - id: slave_thread_running
844
+ title: I/O / SQL Thread Running State
845
+ context: slave_status
846
+ units: boolean
847
+ instances:
848
+ by_labels: [connection]
849
+ dimensions:
850
+ - selector: slave_io_running
851
+ name: io_running
852
+ - selector: slave_sql_running
853
+ name: sql_running
854
+```
855
+
856
+### Deeply nested groups
857
+
858
+MySQL's InnoDB storage engine metrics organized in a deep hierarchy.
859
+
860
+```yaml
861
+groups:
862
+ - family: Storage Engine
863
+ groups:
864
+ - family: InnoDB
865
+ groups:
866
+ - family: Buffer Pool
867
+ metrics:
868
+ - innodb_buffer_pool_pages_data
869
+ - innodb_buffer_pool_pages_dirty
870
+ - innodb_buffer_pool_pages_free
871
+ - innodb_buffer_pool_pages_misc
872
+ - innodb_buffer_pool_pages_total
873
+ charts:
874
+ - id: innodb_buffer_pool_pages
875
+ title: InnoDB Buffer Pool Pages
876
+ context: innodb_buffer_pool_pages
877
+ units: pages
878
+ dimensions:
879
+ - selector: innodb_buffer_pool_pages_data
880
+ name: data
881
+ - selector: innodb_buffer_pool_pages_dirty
882
+ name: dirty
883
+ options:
884
+ multiplier: -1
885
+ - selector: innodb_buffer_pool_pages_free
886
+ name: free
887
+ - selector: innodb_buffer_pool_pages_misc
888
+ name: misc
889
+ options:
890
+ multiplier: -1
891
+ - selector: innodb_buffer_pool_pages_total
892
+ name: total
893
+ - family: I/O
894
+ metrics:
895
+ - innodb_data_read
896
+ - innodb_data_written
897
+ charts:
898
+ - id: innodb_io
899
+ title: InnoDB I/O Bandwidth
900
+ context: innodb_io
901
+ units: KiB/s
902
+ type: area
903
+ algorithm: incremental
904
+ dimensions:
905
+ - selector: innodb_data_read
906
+ name: read
907
+ options:
908
+ divisor: 1024
909
+ - selector: innodb_data_written
910
+ name: write
911
+ options:
912
+ divisor: 1024
913
+```
914
+
915
+The resulting chart families are `Storage Engine/InnoDB/Buffer Pool` and `Storage Engine/InnoDB/I/O`.
916
+
917
+### chart_defaults: reducing repetition
918
+
919
+When monitoring a cloud resource that has many charts, all sharing the same instance identity.
920
+
921
+```yaml
922
+groups:
923
+ - family: Azure PostgreSQL
924
+ context_namespace: postgres_flexible
925
+ chart_defaults:
926
+ label_promotion: [resource_name, resource_group, region]
927
+ instances:
928
+ by_labels: [resource_uid]
929
+ charts:
930
+ - title: CPU Percent
931
+ context: cpu_percent
932
+ units: percentage
933
+ dimensions:
934
+ - selector: postgres_flexible.cpu_percent_average
935
+ name: average
936
+ - title: Memory Percent
937
+ context: memory_percent
938
+ units: percentage
939
+ dimensions:
940
+ - selector: postgres_flexible.memory_percent_average
941
+ name: average
942
+ - title: Storage Percent
943
+ context: storage_percent
944
+ units: percentage
945
+ dimensions:
946
+ - selector: postgres_flexible.storage_percent_average
947
+ name: average
948
+```
949
+
950
+All three charts inherit `instances` and `label_promotion` from `chart_defaults` — no repetition needed.
951
+
952
+### Autogeneration: handling unpredictable metrics
953
+
954
+For collectors where the metric set is user-defined or discovered at runtime, use autogen to catch metrics that don't match any explicit chart template.
955
+
956
+```yaml
957
+version: v1
958
+context_namespace: prometheus_scraper
959
+engine:
960
+ autogen:
961
+ enabled: true
962
+ expire_after_success_cycles: 30
963
+ selector:
964
+ deny: ["go_*", "promhttp_*"] # exclude internal Go/Prometheus metrics
965
+groups:
966
+ - family: Application
967
+ metrics:
968
+ - app_http_requests_total
969
+ - app_http_response_time_seconds
970
+ charts:
971
+ - id: app_requests
972
+ title: Application HTTP Requests
973
+ context: http_requests
974
units: requests/s
975
+ algorithm: incremental
976
+ instances:
977
+ by_labels: [instance]
978
dimensions:
145
- - selector: app.requests_total
146
- name: requests
147
- - id: latency
148
- title: Request latency
149
- context: latency
979
+ - selector: app_http_requests_total
980
+ name_from_label: status_code
981
+ - id: app_response_time
982
+ title: Application Response Time
983
+ context: response_time
984
units: seconds
985
+ instances:
986
+ by_labels: [instance]
987
dimensions:
152
- - selector: app.latency_seconds_bucket{le="0.5"}
153
- name: le_0_5
988
+ - selector: app_http_response_time_seconds
989
+ name: p99
990
+ options:
991
+ float: true
992
```
993
994
+The explicitly defined charts handle `app_http_requests_total` and `app_http_response_time_seconds`. Any _other_ application metrics the scraper discovers are automatically charted by the engine, and removed after 30 cycles of inactivity. The `selector.deny` filter excludes noisy internal metrics from autogeneration.
995
+
996
## Validation Rules
997
998
+> [!CAUTION]
999
+> Unknown YAML fields cause an immediate decode error (strict unmarshal). Double-check field names for typos — a misspelled field like `demensions` will be caught at parse time with an unmarshal error, not at runtime with a descriptive message pointing to the affected chart.
1000
+
1001
All rules below produce semantic validation errors unless noted:
1002
160
-- `version` must be `v1`
161
-- `groups[]` must be non-empty
162
-- `group.family` must not be empty or whitespace-only
163
-- `group.metrics[]` entries must not be empty or whitespace-only; no duplicates within same group
164
-- `group.chart_defaults.instances.by_labels` follows the same validation rules as `chart.instances.by_labels`
165
-- `chart.title`, `chart.context`, `chart.units` must be non-empty
166
-- `dimension.selector` must include explicit metric name (prefix before `{`)
167
-- Selector metric must be visible in current group metric scope
168
-- `name` and `name_from_label` are mutually exclusive
169
-- `name` and `name_from_label` must not be whitespace-only
170
-- Duplicate dimension `name` values within the same chart are rejected
171
-- `instances.by_labels` must contain at least one token when `instances` is set
172
-- `instances.by_labels` exclude token must include label key (e.g., `!key`, not bare `!`)
173
-- `instances.by_labels` tokens must not be duplicated
174
-- Lifecycle numeric fields must be `>= 0`
175
-- `engine.autogen.max_type_id_len` must be `0` or `>= 4`
176
-- Unknown YAML field — decode error (strict unmarshal)
177
-
178
-## Current Phase-1 Constraints
179
-
180
-- **Placeholders in `chart.id` and `dimension.name`** — Parsed by template parser but intentionally rejected in phase-1 syntax.
181
-- **Runtime inferred dimension naming** — Allowed only for inferable selectors (histogram bucket / summary quantile / stateset-like sources) when both `name` and `name_from_label` are omitted.
182
-- **Selector parse stage** — Full selector parsing happens in the chartengine compile stage, not here.
1003
+| Rule | Error type |
1004
+|-----------------------------------------------------------------------------------------|---------------------------------|
1005
+| `version` must be `v1` | semantic |
1006
+| `groups[]` must be non-empty | semantic |
1007
+| `group.family` must not be empty or whitespace-only | semantic |
1008
+| `group.metrics[]` entries must not be empty; no duplicates within same group | semantic |
1009
+| `chart.title`, `chart.context`, `chart.units` must be non-empty | semantic |
1010
+| `chart.algorithm` must be `absolute` or `incremental` (when specified) | semantic |
1011
+| `chart.type` must be `line`, `area`, `stacked`, or `heatmap` (when specified) | semantic |
1012
+| `dimension.selector` must include explicit metric name (prefix before `{`) | semantic |
1013
+| Selector metric must be visible in current group metric scope | semantic |
1014
+| `name` and `name_from_label` are mutually exclusive | semantic |
1015
+| `name` and `name_from_label` must not be whitespace-only | semantic |
1016
+| Duplicate dimension `name` values within the same chart are rejected | semantic |
1017
+| `instances.by_labels` must contain at least one token when `instances` is set | semantic |
1018
+| `instances.by_labels` exclude token must include label key (e.g., `!key`, not bare `!`) | semantic |
1019
+| `instances.by_labels` tokens must not be duplicated | semantic |
1020
+| Lifecycle numeric fields must be `>= 0` | semantic |
1021
+| `engine.autogen.max_type_id_len` must be `0` or `>= 4` | semantic |
1022
+| Unknown YAML fields | decode error (strict unmarshal) |
1023
1024
## Compiler-Derived Behavior
1025
186
-The following behaviors are applied by `chartengine` during compilation, not by `charttpl`.
187
-They affect how you write templates:
1026
+> [!NOTE]
1027
+> These behaviors are applied by `chartengine` during compilation, not by the template parser. You don't need to configure them — they happen automatically, but knowing about them helps you write simpler templates.
1028
189
-| Input | Derived behavior |
190
-|-----------------------------------------|-------------------------------------------------------------------------------------------|
191
-| Missing `chart.id` | `id` derived from `context` (`.` replaced with `_`) |
192
-| Missing `chart.algorithm` | Inferred from metric suffixes (`*_total`, `*_count`, `*_sum`, `*_bucket` => counter-like) |
193
-| Group family hierarchy + `chart.family` | Composed into slash-separated chart family |
1029
+| Input | Derived behavior |
1030
+|-----------------------------------------|------------------------------------------------------------------------------------------|
1031
+| Missing `chart.id` | `id` derived from `context` (`.` replaced with `_`). |
1032
+| Missing `chart.algorithm` | Inferred from metric suffixes (`*_total`, `*_count`, `*_sum`, `*_bucket` = incremental). |
1033
+| `chart.priority = 0` | Treated as `70000` (engine default). |
1034
+| Group family hierarchy + `chart.family` | Composed into `/`-separated chart family. |
1035
+| `options.multiplier = 0` | Treated as `1`. |
1036
+| `options.divisor = 0` | Treated as `1`. |