| 1 | # chartengine |
| 2 | |
| 3 | `chartengine` compiles chart templates and builds deterministic chart plans (`create`, `update`, `remove`) from `metrix.Reader` snapshots. |
| 4 | |
| 5 | **Audience**: `ModuleV2` collector authors and framework contributors. |
| 6 | |
| 7 | **See also**: [charttpl](/src/go/plugin/framework/charttpl/README.md) (template DSL), |
| 8 | [metrix](/src/go/pkg/metrix/README.md) (metrics storage and read API). |
| 9 | |
| 10 | ## Purpose |
| 11 | |
| 12 | | Stage | Responsibility | |
| 13 | |-----------------------|----------------------------------------------------------| |
| 14 | | `charttpl` | Template decode/defaults/validation | |
| 15 | | `chartengine.Compile` | Build immutable program IR | |
| 16 | | `Engine.PreparePlan` | Prepare plan actions plus explicit commit/abort boundary | |
| 17 | | `chartemit.ApplyPlan` | Emit plan to Netdata wire protocol | |
| 18 | |
| 19 | ## Collector-Facing Contract |
| 20 | |
| 21 | For `ModuleV2` collectors, the runtime integration expects: |
| 22 | |
| 23 | | Requirement | Why it matters | |
| 24 | |---------------------------------------------------------------------------|---------------------------------------------------------------------| |
| 25 | | `MetricStore()` returns `metrix.CollectorStore` (cycle-managed) | Job runtime controls cycle boundaries and success/failure semantics | |
| 26 | | `ChartTemplateYAML()` returns valid `charttpl` YAML | Loaded once at autodetection/post-check | |
| 27 | | Collector writes metrics during `Collect()` only | Planner runs after a successful cycle commit | |
| 28 | | Metric names used in template selectors are present in group metric scope | Compile/validate consistency | |
| 29 | |
| 30 | ## Public API Surface |
| 31 | |
| 32 | | API | Purpose | |
| 33 | |-----------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |
| 34 | | `New(opts...)` | Create engine with policy/runtime options | |
| 35 | | `Load(spec, revision)` / `LoadYAML(data, revision)` | Compile and publish program revision | |
| 36 | | `PreparePlan(reader)` | Build deterministic action plan from reader snapshot and return an explicit attempt | |
| 37 | | `RuntimeStore()` | Access chartengine internal runtime metrics store | |
| 38 | | `WithEnginePolicy(...)` | Configure selector + autogen behavior | |
| 39 | | `WithRuntimeStore(...)` | Override/disable self-metrics store | |
| 40 | | `WithSeriesSelectionAllVisible()` | Process all visible series instead of filtering to latest successful collect cycle. Intended for runtime/internal stores that commit immediately (no cycle boundaries). | |
| 41 | | `WithEmitTypeIDBudgetPrefix(...)` | Set the effective type-id prefix used by autogen budget checks | |
| 42 | | `WithRuntimePlannerMode(...)` | Enable runtime planner mode with no-write-tick semantics, for jobs/tests that drive planning directly from runtime metrics instead of collect-cycle boundaries. | |
| 43 | |
| 44 | ## End-to-End Example (Single Flow) |
| 45 | |
| 46 | ```go |
| 47 | // 1) Collector writes metrics. |
| 48 | store := metrix.NewCollectorStore() |
| 49 | meter := store.Write().SnapshotMeter("app") |
| 50 | meter.Counter("requests_total").ObserveTotal(100) |
| 51 | |
| 52 | // 2) Engine loads chart template. |
| 53 | engine, err := chartengine.New( |
| 54 | chartengine.WithEnginePolicy(chartengine.EnginePolicy{ |
| 55 | Autogen: &chartengine.AutogenPolicy{Enabled: false}, |
| 56 | }), |
| 57 | ) |
| 58 | // handle err |
| 59 | |
| 60 | err = engine.LoadYAML([]byte(` |
| 61 | version: v1 |
| 62 | groups: |
| 63 | - family: App |
| 64 | metrics: [app.requests_total] |
| 65 | charts: |
| 66 | - id: requests |
| 67 | title: Requests |
| 68 | context: requests |
| 69 | units: requests/s |
| 70 | dimensions: |
| 71 | - selector: app.requests_total |
| 72 | name: requests |
| 73 | `), 1) |
| 74 | // handle err |
| 75 | |
| 76 | // 3) Prepare plan from flattened+raw reader, emit it, then commit. |
| 77 | // ReadFlatten() is included even for templates with static dimensions |
| 78 | // because it is required for inferred dimensions, structured-family autogen, |
| 79 | // and is the standard pattern. |
| 80 | attempt, err := engine.PreparePlan(store.Read(metrix.ReadRaw(), metrix.ReadFlatten())) |
| 81 | // handle err |
| 82 | plan := attempt.Plan() |
| 83 | defer attempt.Abort() |
| 84 | |
| 85 | err = chartemit.ApplyPlan(api, plan, chartemit.EmitEnv{ |
| 86 | TypeID: "plugin.job", |
| 87 | UpdateEvery: 1, |
| 88 | Plugin: "example", |
| 89 | Module: "example", |
| 90 | JobName: "example", |
| 91 | }) |
| 92 | // handle err |
| 93 | err = attempt.Commit() |
| 94 | // handle err |
| 95 | |
| 96 | ``` |
| 97 | |
| 98 | ## PreparePlan Lifecycle |
| 99 | |
| 100 | `PreparePlan` executes a deterministic phase pipeline. |
| 101 | Terms like "materialized state" and "route cache" are defined in the Engine State section below. |
| 102 | |
| 103 | | Phase | Summary | |
| 104 | |-----------------|------------------------------------------------------------------------------------------------| |
| 105 | | Prepare | Resolve program/index/cache/materialized state | |
| 106 | | Validate reader | Ensure flattened metadata is available when inferred dimensions are used | |
| 107 | | Scan | Iterate series, filter by success sequence and selector, route to chart/dimension accumulators | |
| 108 | | Cache retain | Prune route cache entries not seen in the latest successful sequence | |
| 109 | | Lifecycle caps | Enforce chart/dimension cap policy | |
| 110 | | Materialize | Emit create/update actions from accumulated state | |
| 111 | | Expiry | Emit removals for stale charts/dimensions | |
| 112 | | Sort | Deterministically sort inferred dimension output | |
| 113 | |
| 114 | ## Reader Requirements |
| 115 | |
| 116 | | Scenario | Required reader mode | |
| 117 | |--------------------------------------------------------------------------------|----------------------------------------------------------------------------| |
| 118 | | Static named dimensions only | `Read(...)` is sufficient (no flatten needed) | |
| 119 | | Inferred dimensions (`name` and `name_from_label` omitted) | Must use flattened reader metadata (`ReadFlatten`) | |
| 120 | | Structured autogen families (`Histogram`, `Summary`, `StateSet`, `MeasureSet`) | Must use flattened reader metadata (`ReadFlatten`) or they are not visible | |
| 121 | | Runtime/default `ModuleV2` path | `Read(ReadRaw(), ReadFlatten())` | |
| 122 | |
| 123 | If inferred dimensions are present without flattened reader metadata, `PreparePlan` returns an explicit error. |
| 124 | |
| 125 | ## Action Semantics |
| 126 | |
| 127 | | Action | Meaning | |
| 128 | |-------------------------|--------------------------------------------------------------------------------------------------------------------------------------| |
| 129 | | `CreateChartAction` | Materialize chart instance (with chart metadata and labels) | |
| 130 | | `CreateDimensionAction` | Materialize dimension for a chart | |
| 131 | | `UpdateChartAction` | Emit chart values for current cycle; unseen dims, and dims whose value is non-finite (NaN/Inf), become `IsEmpty=true` (gap, never 0) | |
| 132 | | `RemoveDimensionAction` | Obsolete one dimension | |
| 133 | | `RemoveChartAction` | Obsolete one chart | |
| 134 | |
| 135 | `chartemit` normalizes emitted action order by phase: |
| 136 | |
| 137 | 1. create chart/dimensions |
| 138 | 2. update values |
| 139 | 3. remove dimensions/charts |
| 140 | |
| 141 | ## Routing and Collision Rules |
| 142 | |
| 143 | Each metric series is routed to a chart and dimension based on template selectors. |
| 144 | The following rules apply when routing conflicts arise: |
| 145 | |
| 146 | | Rule | Behavior | |
| 147 | |-----------------------------------------------|------------------------------------------------------------------------------------------------| |
| 148 | | Template vs autogen chart ID collision | Template wins; autogen chart is replaced | |
| 149 | | Cross-template chart ID collision | Existing owner keeps ownership; subsequent series are **silently ignored** (see warning below) | |
| 150 | | Duplicate dimension observations within build | First observed dimension metadata wins; values are reduced (summed) | |
| 151 | |
| 152 | > [!WARNING] |
| 153 | > Cross-template chart ID collisions cause silent data loss — conflicting series are dropped with no error and no log entry. If metrics are missing, check for duplicate rendered chart IDs across template groups. |
| 154 | |
| 155 | ## Lifecycle Defaults and Policy |
| 156 | |
| 157 | Default lifecycle policy when template omits lifecycle: |
| 158 | |
| 159 | | Policy | Default | |
| 160 | |----------------------------------|----------------| |
| 161 | | `max_instances` | `0` (disabled) | |
| 162 | | `expire_after_cycles` | `5` | |
| 163 | | `dimensions.max_dims` | `0` (disabled) | |
| 164 | | `dimensions.expire_after_cycles` | `0` | |
| 165 | |
| 166 | ## Autogen Notes |
| 167 | |
| 168 | | Topic | Behavior | |
| 169 | |-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------| |
| 170 | | Trigger | Unmatched series only when autogen is enabled | |
| 171 | | Context namespace | Autogen context = top-level `context_namespace` + the full metric name (which includes any `SnapshotMeter` prefix); empty namespace leaves the bare name. A non-empty meter prefix stacks after `context_namespace`, so pair `context_namespace` with `SnapshotMeter("")` to avoid a doubled prefix | |
| 172 | | Structured families | Autogen has dedicated source builders for flattened `Histogram`, `Summary`, `StateSet`, and `MeasureSet` families | |
| 173 | | Metric metadata usage | Uses `metrix.MetricMeta` hints for title/family/unit where allowed | |
| 174 | | Type ID budget | Enforced via `AutogenPolicy.MaxTypeIDLen` + effective emit type-id prefix (`WithEmitTypeIDBudgetPrefix(...)`) | |
| 175 | | Lifecycle | Autogen applies `ExpireAfterSuccessCycles` to **both** chart and dimension expiry (unlike template lifecycle where they default independently) | |
| 176 | |
| 177 | `MeasureSet` autogen specifics: |
| 178 | |
| 179 | - chartengine treats `MeasureSet` as a structured family, similar to `StateSet`, not as grouped scalar coincidence |
| 180 | - flattened `MeasureSet` inputs are expected to carry: |
| 181 | - `SourceKind = MetricKindMeasureSet` |
| 182 | - `FlattenRole = FlattenRoleMeasureSetField` |
| 183 | - per-field metric names like `<name>_<field>` |
| 184 | - a synthetic reserved field label (`measure_field=<field>`) |
| 185 | - the synthetic `measure_field` label is the authoritative field-identity channel; the per-field metric-name suffix remains for `MetricMeta(name)` compatibility |
| 186 | - gauge-like `MeasureSet` fields autogen with absolute algorithm behavior; counter-like `MeasureSet` fields autogen with incremental algorithm behavior |
| 187 | |
| 188 | ### Reserved Flattened Label Keys |
| 189 | |
| 190 | These label keys are treated specially by chartengine when consuming flattened structured-family or distribution inputs: |
| 191 | |
| 192 | | Key / Pattern | Meaning | |
| 193 | |-----------------|------------------------------------------------------------------------------------------------------------------------| |
| 194 | | `le` | Histogram bucket bound label | |
| 195 | | `quantile` | Summary quantile label | |
| 196 | | `measure_field` | `MeasureSet` field identity label | |
| 197 | | `<metric-name>` | `StateSet` special case: the flattened state name is carried under a synthetic label whose key is the base metric name | |
| 198 | |
| 199 | Notes: |
| 200 | |
| 201 | - `le`, `quantile`, and `measure_field` are static reserved flattened-label keys in chartengine. |
| 202 | - `StateSet` is different: it does not use a global static key; it uses the base metric name itself as the synthetic flattened label key. |
| 203 | - These keys are part of the flatten contract between `metrix` and chartengine. Reusing them as ordinary user labels on those flattened inputs is not supported. |
| 204 | |
| 205 | ## Runtime Metrics |
| 206 | |
| 207 | `chartengine` self-instruments to a runtime store by default (disable with `WithRuntimeStore(nil)`). |
| 208 | |
| 209 | | Family | Examples | |
| 210 | |---------------------------|----------------------------------------------------------------| |
| 211 | | `ChartEngine/Build` | build success/error/skipped counters, build duration summaries | |
| 212 | | `ChartEngine/Actions` | action counters by kind | |
| 213 | | `ChartEngine/Series` | scanned/matched/filtered series counters | |
| 214 | | `ChartEngine/Route Cache` | hit/miss/entries/prune counters | |
| 215 | | `ChartEngine/Lifecycle` | removal counters by scope/reason | |
| 216 | | `ChartEngine/Plan` | gauges for chart instances/inferred dimensions | |
| 217 | |
| 218 | ## Engine State |
| 219 | |
| 220 | | Area | Design | |
| 221 | |--------------------|---------------------------------------------------------------------------------------------------------------------------------------------| |
| 222 | | Program | Immutable compiled IR per revision | |
| 223 | | Engine state | Serialized under `Engine.mu` for load/build transitions | |
| 224 | | Route cache | Series identity + revision keyed cache; retained by successful sequence, pruned on each build | |
| 225 | | Materialized state | Tracks existing chart/dimension instances for incremental create/update/remove decisions; persists across cycles, resets on template reload | |
| 226 | | Determinism | Sorted chart IDs and inferred dimensions provide stable action ordering | |