Author Netdata Health Alerts
Use this skill before changing a health-alert definition. Alerts are production policy: a syntactically valid expression
can still page incorrectly, manufacture a recovery, duplicate an incident owner, or make a disappearing entity look healthy.
The normal goal when translating an alert from another system is Netdata-adapted operational equivalence, not
execution-engine emulation. Preserve the operator-visible incident as closely as Netdata can express it through NIDL
instances, alert beats, database lookups, native gap behavior, and chart obsoletion. Treat differences from the source
engine as explicit design facts to document and test, not as defects by default.
Read The Right Sources
- Read
AGENTS.md, the active SOW, the target alert file, and its collector/profile source.
- Read
docs/NIDL-Framework.md, src/health/REFERENCE.md, src/health/README.md, and
src/health/alert-configuration-ordering.md.
- Read the existing alert owner and search for duplicate names, contexts, metric prefixes, and equivalent generic alerts.
- When lifecycle, missing-data, expression, or lookup semantics matter, confirm them in current runtime source:
src/health/health_event_loop.c
src/health/health_variable.c
src/web/api/queries/query-execute.c
- the selected grouping implementation below
src/web/api/queries/
src/libnetdata/eval/eval-evaluate.c
- When the source is a go.d collector or Prometheus profile, also load the matching collector/profile skill and its required
references. Do not treat this skill as a replacement for collector or profile authoring guidance.
Never query or reconfigure a live Agent merely to validate an alert unless the user has explicitly authorized that access.
Establish The Alert Contract Before Editing
Write down the following in the active SOW before changing a non-trivial alert:
- Incident and owner: What real incident does this alert represent? Which one source owns it? Do not create a
product-named duplicate of an existing collection-failure, component-failure, or generic host alert just to change
routing or severity.
- Signal contract: Identify every source value and its meanings, including zero, non-zero, tri-state values, absent
dimensions, temporary collection failure, and known entity disappearance.
- Source intent and adaptation: Separate the source alert's operator intent from its engine syntax. Record which
condition, scope, severity, persistence intent, identity, and recovery semantics Netdata preserves; record every
deliberate difference. Use
NETDATA-ADAPTED as the normal classification. Reserve EXACT for a proven coincidental
match across timing, gaps, identity, recovery, and removal—not merely a similar expression.
- NIDL instance map: Record the monitored component, context, one instance type, RRDSET/chart ID identity, dimensions,
stable identity labels, current metadata labels, and the collector's obsoletion condition. An alert attaches to one
RRDSET/chart instance; a template applies that same rule independently to matching instances in one context.
- Identity: Choose an
alarm only for a specific chart instance; choose a template for a context-wide rule. An
identity label MUST identify the same monitored entity across its intended lifetime. A label whose source value can
change for that entity is current metadata, not RRDSET identity: preserve the chart ID and update/promote the label.
Confirm labels preserve the incident identity and do not create unbounded instances.
Prove whether each promoted source label is live metadata, creation-time metadata, or source-frozen history. Do not
describe a label as "current" merely because Netdata can update labels.
- Variables and aggregation: An unqualified variable resolves in the alert's local monitored component/instance before
broader candidates. Use a fully qualified chart/dimension reference only after proving the other chart is the matching
instance and labels select it unambiguously. Never synthesize an infrastructure-level alert by combining multiple
RRDSET instances in alert configuration; require a source-owned aggregate RRDSET at the higher component level instead.
The alert
component: classification field does not replace this NIDL mapping.
- Lifecycle: Specify expected
UNINITIALIZED, CLEAR, WARNING/CRITICAL, UNDEFINED, and REMOVED behavior.
State whether an ordinary recovery emits a zero or whether the chart/dimension disappears instead.
- Timing: State the source update cadence, explicit alert cadence, lookup window, source persistence intent, selected
Netdata adaptation, startup behavior, partial-gap behavior, stale-chart behavior, and notification policy. Do not call
a lookup window an implementation of another engine's
for: state machine.
- Validation: List the boundary, transition, missing-data, recovery, and duplicate-ownership tests that prove the
contract.
Pause for a user decision when the change creates a public alert contract, changes default notification policy, changes
the owner of an incident, needs shared health/query framework work, or cannot preserve the approved operator-visible
incident closely enough with Netdata-native behavior. A difference from Prometheus execution alone is not such a blocker.
Combine Values Across Dimensions, Charts, And Alerts
Choose the smallest pattern that can express the condition truthfully. Do not introduce cross-chart or intermediate-alert
plumbing when the required values already share the alert's chart.
Pattern 1 — dimensions on the alert's chart
Use unqualified dimension variables in calc, warn, or crit when every required value is a dimension on the chart
named by on::
template: example_ratio
on: app.state
calc: $total - $used
Runtime facts:
- Matching is by dimension ID or name in the alert's own chart.
- The default dimension value is
collector.last_stored_value: the latest database-stored value after interpolation, not
a database-window aggregate and not necessarily the raw last collector sample.
- A missing dimension makes the expression fail with an unknown variable and the alert value become
NaN.
- Prefer explicit non-finite guards when
UNDEFINED is the required missing-input state; do not rely on arithmetic or
comparisons to preserve NaN.
Use $dimension_raw only when the contract specifically needs the last collected value before interpolation/storage
presentation, and $dimension_last_collected_t only when it needs that dimension's collection timestamp.
Pattern 2 — dimensions from another chart or context
Use a dotted chart/context reference when values live on different charts:
template: example_cross_chart
on: app.usage
calc: $this * 100 / ${app.capacity.total}
The dotted prefix is interpreted from right to left as chart/context plus the final dimension name. Runtime resolution:
- An exact chart ID is checked first.
- A chart name may match.
- If the prefix is a context, every chart instance in that context contributes candidates for the final dimension.
- Every candidate is scored against the alert's chart labels by counting equal key/value labels.
- The candidate with the highest score wins. Equal scores are resolved by candidate traversal order, so avoid designs
where a tie is possible.
Consequences:
- This is the natural way to compare related chart instances, but the matching labels must make the intended instance
unique. A generic shared label such as only
component=ceph is usually insufficient across many cluster instances.
- Like Pattern 1, the selected dimension value is the latest stored value, not a query over a time window.
- Fully qualified names containing punctuation must use
${...} braces.
- A reference that resolves to no candidate fails as an unknown variable and produces
NaN.
Pattern 3 — an intermediate alert as a computed variable
Create a non-notifying helper alert when the required value itself needs a database lookup or multi-stage computation,
then reference that alert by name from the consuming alert:
template: example_window
on: app.work
lookup: average -1h of requests
calc: $this
to: silent
template: example_consumes
on: app.current
lookup: average -1m of requests
calc: $this / $example_window
warn: $this > 1
to: sysadmin
Runtime facts:
- Every running alert with the referenced name is a candidate.
- Candidates are selected by the same equal-label score used for cross-chart variables.
- The selected candidate contributes its current alert value (
rc->value), after that helper's own lookup and calc.
- This is the only one of the three patterns that can combine database-window results such as “max of the last hour of X
with the average of the last minute of Y”.
- Health evaluates all lookup/calculation phases before warning/critical phases, but helper snapshots are published as
each alert completes. A consumer with a different cadence can therefore read the helper's previous published value on
its first beat or after cadence drift. Align
every: or disclose this timing difference.
- Prefer
to: silent on helpers and document why they exist; a helper is instrumentation, not a second incident owner.
Choosing a pattern
- Same chart, latest values: Pattern 1.
- Different charts/contexts, latest values: Pattern 2.
- Any input needs a database window or a staged calculation: Pattern 3.
- Never use Pattern 3 merely to avoid a qualified dotted reference; its added timing and lifecycle coupling must earn its
place.
- Never combine multiple RRDSET instances into one infrastructure-level alert on the alert side. If no source-owned chart
provides the required aggregate, that is a collector/profile/framework gap, not an alert-expression workaround.
Model The Lifecycle Truthfully
| Source situation |
Alert-lifecycle consequence |
| Valid numeric input and both conditions are false |
CLEAR |
| Valid numeric input and warning/critical condition is true |
WARNING or CRITICAL |
| Some values exist in a lookup window and some are NULL |
The lookup continues using numeric values; NULL is not automatically a failed condition |
| Collector misses a collection while the chart remains live |
A runnable alert may evaluate a lookup based on stored data; no fresh sample is fabricated |
| Lookup runs and its selected window has no usable values |
$this is NaN; the final state is UNDEFINED only when the condition preserves it |
| A lookup's newest stored sample is too old for its runnable-history gate |
The health loop skips that evaluation; it does not manufacture CLEAR or guarantee an UNDEFINED transition |
| Collector knows the entity is gone and obsoletes the chart |
The alert enters REMOVED, not an ordinary zero/CLEAR recovery |
Important rules:
- A collection failure is not disappearance. Collectors MUST preserve a gap when they cannot measure and MUST obsolete
only an entity they know is gone. Do not add a false zero to make an alert recover.
- A chart can remain alert-eligible during a gap. The health loop skips an obsolete chart, but does not require a
newly collected value for every scheduled evaluation.
$last_collected_t and $update_every expose freshness when an
alert is the deliberate stale-collection owner.
- A live chart does not guarantee every lookup remains runnable forever. For a relative database lookup, the health
loop eventually skips evaluation when the newest stored point is too old for the requested window plus its bounded
update-interval tolerance. Skipping evaluation preserves the prior alert state; it is different from evaluating an
all-null result to
UNDEFINED.
- Obsoletion ends the instance alert. Once the collector knows an entity is gone and obsoletes its RRDSET, the health
rule no longer evaluates that instance. Do not attempt to emulate an infrastructure-level continuation in another
alert; collect a source-owned aggregate instance if that is the required product signal.
- A null result does not fabricate healthy state. The health loop assigns
NaN to an empty lookup result. A condition
that itself evaluates to NaN is UNDEFINED.
- Comparisons can consume
NaN. In the expression evaluator, NaN is false in boolean contexts and a comparison
such as $this == 0 produces a finite false result. That can yield CLEAR, not UNDEFINED. If the contract requires
UNDEFINED, make and test an expression path that preserves NaN; never assume a comparison does so.
- Some NULLs are not all NULLs. Query aggregation accepts numeric points and omits non-numeric ones. Do not assume a
partial collection gap invalidates a window unless the current runtime and the approved product contract say it does.
A general strict-coverage rule is shared-framework work, not an alert-file shortcut.
- Do not duplicate freshness ownership. Use
$now - $last_collected_t for an explicit collection/staleness alert only
when no existing generic collection-failure alert already owns the incident. A data-state alert normally owns the
measured condition, not the collector outage.
For a data-state alert that MUST become UNDEFINED when its lookup is non-finite, use and test this condition shape:
warn: ($this == nan or $this == inf) ? (nan) : (<numeric predicate>)
Use crit: in the same way. This preserves the non-finite result only; it does not turn a partial-null window into a
collection-failure alert or replace a known-obsolete chart's REMOVED lifecycle.
Adapt Timing To Netdata
Current State
Use calc when the source's current value is the full condition. Test start-up, missing input, normal recovery, and chart
obsoletion separately. delay: controls Netdata alert transition/notification hysteresis; it is not a general substitute
for a Prometheus for: duration.
Control Flapping With Three Combinable Layers
Stability is a signal-shaping problem, a threshold-boundary problem, and a transition-confirmation problem. Address them
deliberately in that order. Do not label every anti-flapping technique “hysteresis”: only delay: postpones an alert
transition.
Layer 1 — stabilize the queried value
Use lookup: to make $this a stable aggregate over an explicit observation window:
lookup: average -5m unaligned of latency
average smooths noisy utilization, rate, latency, and utilization-like signals.
min requires every observed numeric sample to remain active, appropriate for persisted binary fault states.
max preserves worst-case excursions when the incident is defined by peaks.
countif expresses percent-of-observed-time semantics.
This reduces noise but cannot prevent a stable aggregate from hovering near one threshold. A 5-minute average near 100 ms
can still cross a 100 ms threshold repeatedly.
Layer 2 — separate raise and clear thresholds
For policy thresholds whose signal may hover near the boundary, use a state-dependent predicate:
warn: $this > (($status >= $WARNING) ? (90) : (100))
crit: $this > (($status == $CRITICAL) ? (95) : (99))
This changes one threshold into two thresholds:
- clear-to-warning raises above 100;
- active-warning clears below 90;
- critical can independently use another raise/clear pair.
Important semantics:
- This is not hysteresis and does not delay any transition.
- The alert continues evaluating at its normal
every: cadence.
- It only changes the boundary used by the current status, so a genuine crossing of the recovery threshold acts
immediately.
- Preserve a non-finite guard around the complete expression when
UNDEFINED must remain possible.
Prefer this when independent raise/clear boundaries are meaningful and the signal is expected to linger near one boundary.
Do not use it to redefine a categorical exact condition into a policy band.
Layer 3 — confirm transitions with true hysteresis
Use delay: only when a transition must remain selected for a duration before Netdata executes the transition
notification:
delay: down 5m multiplier 1.5 max 1h
up delays a state escalation; down delays a recovery/de-escalation.
multiplier grows the delay when the state changes during the delay.
max caps the accumulated delay.
This is the only layer that postpones an alert transition notification. It can suppress rapid clear/reactivate
notification cycles, but it can also postpone a real transition. Use it sparingly and record the expected transition
delay in the alert contract.
Selection procedure
- Establish whether the incident is categorical, threshold policy, or derived arithmetic.
- Select the smallest truthful
lookup: window and aggregation first.
- For a noisy policy threshold, choose explicit raise/clear thresholds before adding transition delay.
- Add
delay: only when rapid transition notifications are independently harmful and later notification is acceptable.
- Test each layer: input noise, boundary crossing, recovery, reactivation, non-finite input, partial gap, and the exact
expected notification time.
Persistence Intent
Treat another system's for: D as an operator intent to suppress transient conditions. It is not a requirement to emulate
that system's pending-state machine. Choose the closest safe Netdata-native behavior from the source's actual numeric
state space, set an explicit every:, and disclose the differences:
- For a binary source where 1 means the active fault,
min -5m unaligned with an active predicate is true only when
every observed numeric value in the selected window is active. This is a Netdata observation-window adaptation, not
Prometheus for: 5m.
- For a binary source where 0 means the active fault, use the complementary aggregation/predicate that requires all
observed values to be zero; consult the lookup reference and prove it with state-sequence tests.
- For a tri-state or enumerated source, do not apply a binary
min/max rule by analogy. Use an exact predicate over the
full state space. For example, countif(!=target) returns the percentage of observed values outside target; zero means
every observed value matched.
min, max, and countif operate on the numeric samples they receive. Test active-to-other-state transitions in both
directions and record the intended partial-gap behavior.
- A new chart may become runnable with up to one chart update interval less history than the requested lookup window.
Therefore an already-active condition can raise up to roughly one source beat before
D after chart creation. Do not
conceal this with a hand-authored "window complete" flag.
- A missing sample does not reset an observation window. Values before and after a partial gap may contribute to the same
lookup. Collection-failure ownership remains separate unless the alert contract explicitly owns freshness.
- One health rule has one historical database lookup. A predicate combining multiple dimensions cannot gain exact
historical persistence by looking up one dimension and combining it with the others' current values. Prefer, in order:
a source-owned derived dimension when the persisted compound condition is essential; otherwise a truthful current-state
adaptation; never a mixed-time formula that changes the incident meaning.
- Do not lengthen the window mechanically by one assumed collector interval. Collector cadence is configurable, and that
does not repair partial gaps or create a source-engine pending state.
For every persistence adaptation, prove: chart startup, observed active history, each recovery state, each higher/lower
enumerated state, a partial source gap, a stale non-runnable lookup, collection resumption, chart obsoletion, and the
configured evaluation cadence. Never invent a pending state that Netdata does not expose, and never declare fidelity
from the look of an expression alone.
Keep Ownership And Identity Non-Duplicating
- Search existing stock alerts before adding one. Reuse an existing alert only when it really owns the same logical
incident; disclose routing/severity/lifecycle differences rather than silently relabeling it.
- Keep source collection failure, component/API collection failure, source data-state failure, and client-observed failure
as separate owners when they identify different operator actions.
- Filter templates with chart labels only when they select the intended RRDSET instance without changing its identity.
A current metadata label may be used as a filter only when the alert contract explicitly wants that current metadata;
do not turn it into
instances.by_labels merely to make filtering convenient. Exclude known named rules from generic
fallbacks, including special sources whose recovery is chart removal rather than zero.
- Preserve ordinary zero recovery. Do not convert an active-to-zero source into disappearance, and do not treat a
disappearing source as a normal CLEAR.
- Use the ordering guide for template/alarm precedence and user-versus-stock override behavior. Same-name definitions are
an override mechanism; different names coexist and can therefore duplicate incidents.
Validate The Actual Contract
Run the smallest relevant tests first, then the full affected suite. A complete alert change normally needs all applicable
items below:
- Run
/usr/sbin/netdata -W healthconfigtest for the built-in health parser and lookup suite.
- Add or update a focused test that reads the shipped alert template and asserts its context, labels, lookup, units,
cadence, expressions, routing, source ownership, and declared fidelity—not merely a copied expected string.
- Test the signal's lifecycle through the real query/health runtime where practical. If a lower-level deterministic model
is necessary, derive it directly from runtime timestamps and numeric-point selection; do not pass an arbitrary
windowComplete flag or label skipped NULLs as continuous evaluation. Cover startup, active transition, normal zero
recovery, a true all-null query, partial-null behavior, stale non-runnable behavior, collection resumption, known
disappearance/REMOVED, and label identity.
- For an expression that relies on
NaN, test the evaluator result directly. Check both direct NaN propagation and any
comparison/conditional branch; do not infer the result from ordinary floating-point intuition.
- Run collector/profile validation when the alert depends on a collector or profile change. Include source absence and
collision-bearing labels where relevant.
- Search for same-incident alerts, duplicate contexts, old names, fallback overlap, and generated artifacts. Record the
result in the SOW.
- For an externally sourced alert pack, commit a source-pinned mapping that records the original condition, scope,
severity, persistence intent, supported releases, Netdata owner, adaptation, and known differences. Tests MUST consume
that mapping rather than restating the intended result independently.
- Validate every published configuration example through the same job-construction prerequisites users need. In
particular, a job referencing
vnode: is incomplete unless the example defines or clearly links the required vnode.
- Run
git diff --check and the project-required validation/review gate before claiming completion.
healthconfigtest runs built-in health parser and lookup cases. It does not load every stock health template, prove that a
template attaches to the intended chart, or prove the runtime lifecycle. Cover those separately with source-aware contract
and transition tests.
Completion Check
Before requesting review, confirm all of the following:
- The alert has exactly one logical owner and a stable scope.
- Its NIDL instance map shows one monitored component and one instance-level alert target; any required aggregate is a
source-owned higher-level RRDSET, not an alert-side merge.
- Its source state values, gaps, absence, and recovery have been tested rather than assumed.
- Its
NaN, UNDEFINED, CLEAR, and REMOVED transitions match the recorded contract.
- Its timing is the closest safe Netdata adaptation, with startup/gap/stale differences disclosed;
delay: is not
standing in for another engine's persistence state machine.
- Notification defaults follow the approved product policy.
- No shared health/query behavior was added or assumed without the required separate scope approval.
Authoritative References
- Syntax, variables, lookups, and stock patterns:
src/health/REFERENCE.md
- State model and missing-data summary:
src/health/README.md
- NIDL component/instance/dimension/label model:
docs/NIDL-Framework.md
- Template/alarm and user/stock precedence:
src/health/alert-configuration-ordering.md
- Alert eligibility, lookup execution, and
REMOVED: src/health/health_event_loop.c
$now, $last_collected_t, $update_every, dimension freshness, same-chart variables, cross-chart/context
variables, alert variables, and label-score selection: src/health/health_variable.c
- Equal-label score implementation:
src/database/rrdlabels.c:rrdlabels_common_count()
- Query gaps and grouping:
src/web/api/queries/query-execute.c and the relevant grouping implementation
NaN expression semantics: src/libnetdata/eval/eval-evaluate.c
1---2name: health-alert-authoring3description: Author, adapt, modify, or review Netdata health alerts and alert templates. Use when translating alerts from another system; changing `src/health/health.d/*.conf`, lookup/calc/warn/crit expressions, lifecycle, timing, routing, ownership, or missing-data behavior; writing health-config tests; or selecting an alert's chart/context/label identity.4---5
6# Author Netdata Health Alerts
7
8Use this skill before changing a health-alert definition. Alerts are production policy: a syntactically valid expression
9can still page incorrectly, manufacture a recovery, duplicate an incident owner, or make a disappearing entity look healthy.
10
11The normal goal when translating an alert from another system is **Netdata-adapted operational equivalence**, not
12execution-engine emulation. Preserve the operator-visible incident as closely as Netdata can express it through NIDL
13instances, alert beats, database lookups, native gap behavior, and chart obsoletion. Treat differences from the source
14engine as explicit design facts to document and test, not as defects by default.
15
16## Read The Right Sources
17
181. Read `AGENTS.md`, the active SOW, the target alert file, and its collector/profile source.
192. Read `docs/NIDL-Framework.md`, `src/health/REFERENCE.md`, `src/health/README.md`, and
20 `src/health/alert-configuration-ordering.md`.
213. Read the existing alert owner and search for duplicate names, contexts, metric prefixes, and equivalent generic alerts.
224. When lifecycle, missing-data, expression, or lookup semantics matter, confirm them in current runtime source:
23 - `src/health/health_event_loop.c`
24 - `src/health/health_variable.c`
25 - `src/web/api/queries/query-execute.c`
26 - the selected grouping implementation below `src/web/api/queries/`
27 - `src/libnetdata/eval/eval-evaluate.c`
285. When the source is a go.d collector or Prometheus profile, also load the matching collector/profile skill and its required
29 references. Do not treat this skill as a replacement for collector or profile authoring guidance.
30
31Never query or reconfigure a live Agent merely to validate an alert unless the user has explicitly authorized that access.
32
33## Establish The Alert Contract Before Editing
34
35Write down the following in the active SOW before changing a non-trivial alert:
36
37- **Incident and owner:** What real incident does this alert represent? Which one source owns it? Do not create a
38 product-named duplicate of an existing collection-failure, component-failure, or generic host alert just to change
39 routing or severity.
40- **Signal contract:** Identify every source value and its meanings, including zero, non-zero, tri-state values, absent
41 dimensions, temporary collection failure, and known entity disappearance.
42- **Source intent and adaptation:** Separate the source alert's operator intent from its engine syntax. Record which
43 condition, scope, severity, persistence intent, identity, and recovery semantics Netdata preserves; record every
44 deliberate difference. Use `NETDATA-ADAPTED` as the normal classification. Reserve `EXACT` for a proven coincidental
45 match across timing, gaps, identity, recovery, and removal—not merely a similar expression.
46- **NIDL instance map:** Record the monitored component, context, one instance type, RRDSET/chart ID identity, dimensions,
47 stable identity labels, current metadata labels, and the collector's obsoletion condition. An alert attaches to one
48 RRDSET/chart instance; a template applies that same rule independently to matching instances in one context.
49- **Identity:** Choose an `alarm` only for a specific chart instance; choose a `template` for a context-wide rule. An
50 identity label MUST identify the same monitored entity across its intended lifetime. A label whose source value can
51 change for that entity is current metadata, not RRDSET identity: preserve the chart ID and update/promote the label.
52 Confirm labels preserve the incident identity and do not create unbounded instances.
53 Prove whether each promoted source label is live metadata, creation-time metadata, or source-frozen history. Do not
54 describe a label as "current" merely because Netdata can update labels.
55- **Variables and aggregation:** An unqualified variable resolves in the alert's local monitored component/instance before
56 broader candidates. Use a fully qualified chart/dimension reference only after proving the other chart is the matching
57 instance and labels select it unambiguously. Never synthesize an infrastructure-level alert by combining multiple
58 RRDSET instances in alert configuration; require a source-owned aggregate RRDSET at the higher component level instead.
59 The alert `component:` classification field does not replace this NIDL mapping.
60- **Lifecycle:** Specify expected `UNINITIALIZED`, `CLEAR`, `WARNING`/`CRITICAL`, `UNDEFINED`, and `REMOVED` behavior.
61 State whether an ordinary recovery emits a zero or whether the chart/dimension disappears instead.
62- **Timing:** State the source update cadence, explicit alert cadence, lookup window, source persistence intent, selected
63 Netdata adaptation, startup behavior, partial-gap behavior, stale-chart behavior, and notification policy. Do not call
64 a lookup window an implementation of another engine's `for:` state machine.
65- **Validation:** List the boundary, transition, missing-data, recovery, and duplicate-ownership tests that prove the
66 contract.
67
68Pause for a user decision when the change creates a public alert contract, changes default notification policy, changes
69the owner of an incident, needs shared health/query framework work, or cannot preserve the approved operator-visible
70incident closely enough with Netdata-native behavior. A difference from Prometheus execution alone is not such a blocker.
71
72## Combine Values Across Dimensions, Charts, And Alerts
73
74Choose the smallest pattern that can express the condition truthfully. Do not introduce cross-chart or intermediate-alert
75plumbing when the required values already share the alert's chart.
76
77### Pattern 1 — dimensions on the alert's chart
78
79Use unqualified dimension variables in `calc`, `warn`, or `crit` when every required value is a dimension on the chart
80named by `on:`:
81
82```text
83template: example_ratio
84 on: app.state
85 calc: $total - $used
86```
87
88Runtime facts:
89
90- Matching is by dimension ID or name in the alert's own chart.
91- The default dimension value is `collector.last_stored_value`: the latest database-stored value after interpolation, not
92 a database-window aggregate and not necessarily the raw last collector sample.
93- A missing dimension makes the expression fail with an unknown variable and the alert value become `NaN`.
94- Prefer explicit non-finite guards when `UNDEFINED` is the required missing-input state; do not rely on arithmetic or
95 comparisons to preserve `NaN`.
96
97Use `$dimension_raw` only when the contract specifically needs the last collected value before interpolation/storage
98presentation, and `$dimension_last_collected_t` only when it needs that dimension's collection timestamp.
99
100### Pattern 2 — dimensions from another chart or context
101
102Use a dotted chart/context reference when values live on different charts:
103
104```text
105template: example_cross_chart
106 on: app.usage
107 calc: $this * 100 / ${app.capacity.total}
108```
109
110The dotted prefix is interpreted from right to left as chart/context plus the final dimension name. Runtime resolution:
111
1121. An exact chart ID is checked first.
1132. A chart name may match.
1143. If the prefix is a context, every chart instance in that context contributes candidates for the final dimension.
1154. Every candidate is scored against the alert's chart labels by counting equal key/value labels.
1165. The candidate with the highest score wins. Equal scores are resolved by candidate traversal order, so avoid designs
117 where a tie is possible.
118
119Consequences:
120
121- This is the natural way to compare related chart instances, but the matching labels must make the intended instance
122 unique. A generic shared label such as only `component=ceph` is usually insufficient across many cluster instances.
123- Like Pattern 1, the selected dimension value is the latest stored value, not a query over a time window.
124- Fully qualified names containing punctuation must use `${...}` braces.
125- A reference that resolves to no candidate fails as an unknown variable and produces `NaN`.
126
127### Pattern 3 — an intermediate alert as a computed variable
128
129Create a non-notifying helper alert when the required value itself needs a database lookup or multi-stage computation,
130then reference that alert by name from the consuming alert:
131
132```text
133template: example_window
134 on: app.work
135 lookup: average -1h of requests
136 calc: $this
137 to: silent
138
139template: example_consumes
140 on: app.current
141 lookup: average -1m of requests
142 calc: $this / $example_window
143 warn: $this > 1
144 to: sysadmin
145```
146
147Runtime facts:
148
149- Every running alert with the referenced name is a candidate.
150- Candidates are selected by the same equal-label score used for cross-chart variables.
151- The selected candidate contributes its current alert value (`rc->value`), after that helper's own lookup and `calc`.
152- This is the only one of the three patterns that can combine database-window results such as “max of the last hour of X
153 with the average of the last minute of Y”.
154- Health evaluates all lookup/calculation phases before warning/critical phases, but helper snapshots are published as
155 each alert completes. A consumer with a different cadence can therefore read the helper's previous published value on
156 its first beat or after cadence drift. Align `every:` or disclose this timing difference.
157- Prefer `to: silent` on helpers and document why they exist; a helper is instrumentation, not a second incident owner.
158
159### Choosing a pattern
160
161- Same chart, latest values: Pattern 1.
162- Different charts/contexts, latest values: Pattern 2.
163- Any input needs a database window or a staged calculation: Pattern 3.
164- Never use Pattern 3 merely to avoid a qualified dotted reference; its added timing and lifecycle coupling must earn its
165 place.
166- Never combine multiple RRDSET instances into one infrastructure-level alert on the alert side. If no source-owned chart
167 provides the required aggregate, that is a collector/profile/framework gap, not an alert-expression workaround.
168
169## Model The Lifecycle Truthfully
170
171| Source situation | Alert-lifecycle consequence |
172|---|---|
173| Valid numeric input and both conditions are false | `CLEAR` |
174| Valid numeric input and warning/critical condition is true | `WARNING` or `CRITICAL` |
175| Some values exist in a lookup window and some are NULL | The lookup continues using numeric values; NULL is not automatically a failed condition |
176| Collector misses a collection while the chart remains live | A runnable alert may evaluate a lookup based on stored data; no fresh sample is fabricated |
177| Lookup runs and its selected window has no usable values | `$this` is `NaN`; the final state is `UNDEFINED` only when the condition preserves it |
178| A lookup's newest stored sample is too old for its runnable-history gate | The health loop skips that evaluation; it does not manufacture `CLEAR` or guarantee an `UNDEFINED` transition |
179| Collector knows the entity is gone and obsoletes the chart | The alert enters `REMOVED`, not an ordinary zero/CLEAR recovery |
180
181Important rules:
182
183- **A collection failure is not disappearance.** Collectors MUST preserve a gap when they cannot measure and MUST obsolete
184 only an entity they know is gone. Do not add a false zero to make an alert recover.
185- **A chart can remain alert-eligible during a gap.** The health loop skips an obsolete chart, but does not require a
186 newly collected value for every scheduled evaluation. `$last_collected_t` and `$update_every` expose freshness when an
187 alert is the deliberate stale-collection owner.
188- **A live chart does not guarantee every lookup remains runnable forever.** For a relative database lookup, the health
189 loop eventually skips evaluation when the newest stored point is too old for the requested window plus its bounded
190 update-interval tolerance. Skipping evaluation preserves the prior alert state; it is different from evaluating an
191 all-null result to `UNDEFINED`.
192- **Obsoletion ends the instance alert.** Once the collector knows an entity is gone and obsoletes its RRDSET, the health
193 rule no longer evaluates that instance. Do not attempt to emulate an infrastructure-level continuation in another
194 alert; collect a source-owned aggregate instance if that is the required product signal.
195- **A null result does not fabricate healthy state.** The health loop assigns `NaN` to an empty lookup result. A condition
196 that itself evaluates to `NaN` is `UNDEFINED`.
197- **Comparisons can consume `NaN`.** In the expression evaluator, `NaN` is false in boolean contexts and a comparison
198 such as `$this == 0` produces a finite false result. That can yield `CLEAR`, not `UNDEFINED`. If the contract requires
199 `UNDEFINED`, make and test an expression path that preserves `NaN`; never assume a comparison does so.
200- **Some NULLs are not all NULLs.** Query aggregation accepts numeric points and omits non-numeric ones. Do not assume a
201 partial collection gap invalidates a window unless the current runtime and the approved product contract say it does.
202 A general strict-coverage rule is shared-framework work, not an alert-file shortcut.
203- **Do not duplicate freshness ownership.** Use `$now - $last_collected_t` for an explicit collection/staleness alert only
204 when no existing generic collection-failure alert already owns the incident. A data-state alert normally owns the
205 measured condition, not the collector outage.
206
207For a data-state alert that MUST become `UNDEFINED` when its lookup is non-finite, use and test this condition shape:
208
209```text
210warn: ($this == nan or $this == inf) ? (nan) : (<numeric predicate>)
211```
212
213Use `crit:` in the same way. This preserves the non-finite result only; it does not turn a partial-null window into a
214collection-failure alert or replace a known-obsolete chart's `REMOVED` lifecycle.
215
216## Adapt Timing To Netdata
217
218### Current State
219
220Use `calc` when the source's current value is the full condition. Test start-up, missing input, normal recovery, and chart
221obsoletion separately. `delay:` controls Netdata alert transition/notification hysteresis; it is not a general substitute
222for a Prometheus `for:` duration.
223
224## Control Flapping With Three Combinable Layers
225
226Stability is a signal-shaping problem, a threshold-boundary problem, and a transition-confirmation problem. Address them
227deliberately in that order. Do not label every anti-flapping technique “hysteresis”: only `delay:` postpones an alert
228transition.
229
230### Layer 1 — stabilize the queried value
231
232Use `lookup:` to make `$this` a stable aggregate over an explicit observation window:
233
234```text
235lookup: average -5m unaligned of latency
236```
237
238- `average` smooths noisy utilization, rate, latency, and utilization-like signals.
239- `min` requires every observed numeric sample to remain active, appropriate for persisted binary fault states.
240- `max` preserves worst-case excursions when the incident is defined by peaks.
241- `countif` expresses percent-of-observed-time semantics.
242
243This reduces noise but cannot prevent a stable aggregate from hovering near one threshold. A 5-minute average near 100 ms
244can still cross a 100 ms threshold repeatedly.
245
246### Layer 2 — separate raise and clear thresholds
247
248For policy thresholds whose signal may hover near the boundary, use a state-dependent predicate:
249
250```text
251warn: $this > (($status >= $WARNING) ? (90) : (100))
252crit: $this > (($status == $CRITICAL) ? (95) : (99))
253```
254
255This changes one threshold into two thresholds:
256
257- clear-to-warning raises above 100;
258- active-warning clears below 90;
259- critical can independently use another raise/clear pair.
260
261Important semantics:
262
263- This is **not hysteresis** and does not delay any transition.
264- The alert continues evaluating at its normal `every:` cadence.
265- It only changes the boundary used by the current status, so a genuine crossing of the recovery threshold acts
266 immediately.
267- Preserve a non-finite guard around the complete expression when `UNDEFINED` must remain possible.
268
269Prefer this when independent raise/clear boundaries are meaningful and the signal is expected to linger near one boundary.
270Do not use it to redefine a categorical exact condition into a policy band.
271
272### Layer 3 — confirm transitions with true hysteresis
273
274Use `delay:` only when a transition must remain selected for a duration before Netdata executes the transition
275notification:
276
277```text
278delay: down 5m multiplier 1.5 max 1h
279```
280
281- `up` delays a state escalation; `down` delays a recovery/de-escalation.
282- `multiplier` grows the delay when the state changes during the delay.
283- `max` caps the accumulated delay.
284
285This is the only layer that postpones an alert transition notification. It can suppress rapid clear/reactivate
286notification cycles, but it can also postpone a real transition. Use it sparingly and record the expected transition
287delay in the alert contract.
288
289### Selection procedure
290
2911. Establish whether the incident is categorical, threshold policy, or derived arithmetic.
2922. Select the smallest truthful `lookup:` window and aggregation first.
2933. For a noisy policy threshold, choose explicit raise/clear thresholds before adding transition delay.
2944. Add `delay:` only when rapid transition notifications are independently harmful and later notification is acceptable.
2955. Test each layer: input noise, boundary crossing, recovery, reactivation, non-finite input, partial gap, and the exact
296 expected notification time.
297
298### Persistence Intent
299
300Treat another system's `for: D` as an operator intent to suppress transient conditions. It is not a requirement to emulate
301that system's pending-state machine. Choose the closest safe Netdata-native behavior from the source's actual numeric
302state space, set an explicit `every:`, and disclose the differences:
303
304- For a binary source where **1 means the active fault**, `min -5m unaligned` with an active predicate is true only when
305 every **observed numeric value** in the selected window is active. This is a Netdata observation-window adaptation, not
306 Prometheus `for: 5m`.
307- For a binary source where **0 means the active fault**, use the complementary aggregation/predicate that requires all
308 observed values to be zero; consult the lookup reference and prove it with state-sequence tests.
309- For a tri-state or enumerated source, do not apply a binary `min`/`max` rule by analogy. Use an exact predicate over the
310 full state space. For example, `countif(!=target)` returns the percentage of observed values outside `target`; zero means
311 every observed value matched.
312- `min`, `max`, and `countif` operate on the numeric samples they receive. Test active-to-other-state transitions in both
313 directions and record the intended partial-gap behavior.
314- A new chart may become runnable with up to one chart update interval less history than the requested lookup window.
315 Therefore an already-active condition can raise up to roughly one source beat before `D` after chart creation. Do not
316 conceal this with a hand-authored "window complete" flag.
317- A missing sample does not reset an observation window. Values before and after a partial gap may contribute to the same
318 lookup. Collection-failure ownership remains separate unless the alert contract explicitly owns freshness.
319- One health rule has one historical database lookup. A predicate combining multiple dimensions cannot gain exact
320 historical persistence by looking up one dimension and combining it with the others' current values. Prefer, in order:
321 a source-owned derived dimension when the persisted compound condition is essential; otherwise a truthful current-state
322 adaptation; never a mixed-time formula that changes the incident meaning.
323- Do not lengthen the window mechanically by one assumed collector interval. Collector cadence is configurable, and that
324 does not repair partial gaps or create a source-engine pending state.
325
326For every persistence adaptation, prove: chart startup, observed active history, each recovery state, each higher/lower
327enumerated state, a partial source gap, a stale non-runnable lookup, collection resumption, chart obsoletion, and the
328configured evaluation cadence. Never invent a `pending` state that Netdata does not expose, and never declare fidelity
329from the look of an expression alone.
330
331## Keep Ownership And Identity Non-Duplicating
332
333- Search existing stock alerts before adding one. Reuse an existing alert only when it really owns the same logical
334 incident; disclose routing/severity/lifecycle differences rather than silently relabeling it.
335- Keep source collection failure, component/API collection failure, source data-state failure, and client-observed failure
336 as separate owners when they identify different operator actions.
337- Filter templates with chart labels only when they select the intended RRDSET instance without changing its identity.
338 A current metadata label may be used as a filter only when the alert contract explicitly wants that current metadata;
339 do not turn it into `instances.by_labels` merely to make filtering convenient. Exclude known named rules from generic
340 fallbacks, including special sources whose recovery is chart removal rather than zero.
341- Preserve ordinary zero recovery. Do not convert an active-to-zero source into disappearance, and do not treat a
342 disappearing source as a normal CLEAR.
343- Use the ordering guide for template/alarm precedence and user-versus-stock override behavior. Same-name definitions are
344 an override mechanism; different names coexist and can therefore duplicate incidents.
345
346## Validate The Actual Contract
347
348Run the smallest relevant tests first, then the full affected suite. A complete alert change normally needs all applicable
349items below:
350
3511. Run `/usr/sbin/netdata -W healthconfigtest` for the built-in health parser and lookup suite.
3522. Add or update a focused test that reads the shipped alert template and asserts its context, labels, lookup, units,
353 cadence, expressions, routing, source ownership, and declared fidelity—not merely a copied expected string.
3543. Test the signal's lifecycle through the real query/health runtime where practical. If a lower-level deterministic model
355 is necessary, derive it directly from runtime timestamps and numeric-point selection; do not pass an arbitrary
356 `windowComplete` flag or label skipped NULLs as continuous evaluation. Cover startup, active transition, normal zero
357 recovery, a true all-null query, partial-null behavior, stale non-runnable behavior, collection resumption, known
358 disappearance/`REMOVED`, and label identity.
3594. For an expression that relies on `NaN`, test the evaluator result directly. Check both direct `NaN` propagation and any
360 comparison/conditional branch; do not infer the result from ordinary floating-point intuition.
3615. Run collector/profile validation when the alert depends on a collector or profile change. Include source absence and
362 collision-bearing labels where relevant.
3636. Search for same-incident alerts, duplicate contexts, old names, fallback overlap, and generated artifacts. Record the
364 result in the SOW.
3657. For an externally sourced alert pack, commit a source-pinned mapping that records the original condition, scope,
366 severity, persistence intent, supported releases, Netdata owner, adaptation, and known differences. Tests MUST consume
367 that mapping rather than restating the intended result independently.
3688. Validate every published configuration example through the same job-construction prerequisites users need. In
369 particular, a job referencing `vnode:` is incomplete unless the example defines or clearly links the required vnode.
3709. Run `git diff --check` and the project-required validation/review gate before claiming completion.
371
372`healthconfigtest` runs built-in health parser and lookup cases. It does not load every stock health template, prove that a
373template attaches to the intended chart, or prove the runtime lifecycle. Cover those separately with source-aware contract
374and transition tests.
375
376## Completion Check
377
378Before requesting review, confirm all of the following:
379
380- The alert has exactly one logical owner and a stable scope.
381- Its NIDL instance map shows one monitored component and one instance-level alert target; any required aggregate is a
382 source-owned higher-level RRDSET, not an alert-side merge.
383- Its source state values, gaps, absence, and recovery have been tested rather than assumed.
384- Its `NaN`, `UNDEFINED`, `CLEAR`, and `REMOVED` transitions match the recorded contract.
385- Its timing is the closest safe Netdata adaptation, with startup/gap/stale differences disclosed; `delay:` is not
386 standing in for another engine's persistence state machine.
387- Notification defaults follow the approved product policy.
388- No shared health/query behavior was added or assumed without the required separate scope approval.
389
390## Authoritative References
391
392- Syntax, variables, lookups, and stock patterns: `src/health/REFERENCE.md`
393- State model and missing-data summary: `src/health/README.md`
394- NIDL component/instance/dimension/label model: `docs/NIDL-Framework.md`
395- Template/alarm and user/stock precedence: `src/health/alert-configuration-ordering.md`
396- Alert eligibility, lookup execution, and `REMOVED`: `src/health/health_event_loop.c`
397- `$now`, `$last_collected_t`, `$update_every`, dimension freshness, same-chart variables, cross-chart/context
398 variables, alert variables, and label-score selection: `src/health/health_variable.c`
399- Equal-label score implementation: `src/database/rrdlabels.c:rrdlabels_common_count()`
400- Query gaps and grouping: `src/web/api/queries/query-execute.c` and the relevant grouping implementation
401- `NaN` expression semantics: `src/libnetdata/eval/eval-evaluate.c`