An interactive OpenTelemetry lab about the difference between a real zero, a historical value, and no current observation.
The default incident models an image-resize worker whose queue gauge stops reporting at second 197 while ingress continues at 50 items/second:
| Strategy | Dashboard value | What it claims |
|---|---|---|
| Last-value fill | 20 | The 420-second-old observation is still current |
| Zero fill | 0 | The missing queue is empty |
| Absence-aware | — | The current value is unknown |
| Cross-signal estimate | 20,170 | Ingress events and the stalled-drain boundary expose accumulating work |
The first two charts look calm. Both manufacture meaning that the telemetry did not contain.
The interface visualizes the moment observations stop, while the API emits the same incident as correlated OTLP-shaped signals:
- Metrics contain the real gauge observations followed by an explicit data
point with the
No recorded valueflag and no numeric field. - Logs record the worker stall, the staleness marker, and the freshness-budget alert.
- Traces compare the four rendering strategies and identify which ones preserve missingness.
- Independent ingress evidence produces a clearly labeled queue estimate rather than silently rewriting the missing gauge.
- Every response proves that GNU Make executed the model and includes the model’s SHA-256.
Inspect the evidence:
curl 'http://localhost:8080/api/simulate'
curl 'http://localhost:8080/api/telemetry'
curl 'http://localhost:8080/healthz'Requirements:
- Node.js 24+
- GNU Make
npm ci
npm startOpen http://localhost:8080.
Or use Docker:
docker compose up --buildmodel/lab.mk is the source of truth. It clamps the experiment
inputs, builds the observation timeline, computes the freshness boundary, and
writes JSON:
make -s -f model/lab.mk model \
WINDOW=600 \
OBSERVATION_INTERVAL=30 \
STALL=197 \
STALE_AFTER=90 \
LAST_QUEUE=20 \
INGRESS_RATE=50The last real observation is the scheduled observation strictly before the stall:
last_observation = floor((stall − 1) / interval) × interval
observation_age = window − last_observation
The cross-signal estimate assumes ingress remains measurable and the stalled worker stops draining:
estimated_queue =
last_observed_queue + ingress_rate × (window − stall)
That estimate is not substituted into the original gauge. It is exported under a different metric name with its derivation made explicit.
GET /api/simulate and GET /api/telemetry accept:
| Query parameter | Range | Default | Meaning |
|---|---|---|---|
windowSeconds |
120–1,800 | 600 | Observation window |
observationIntervalSeconds |
5–120 | 30 | Gauge reporting interval |
stallSecond |
1–window−1 | 197 | Time the callback stops |
staleAfterSeconds |
5–600 | 90 | Freshness budget |
lastObservedQueue |
0–1,000,000 | 20 | Stable pre-stall queue |
ingressRatePerSecond |
1–10,000 | 50 | Correlated incoming work |
Example:
curl 'http://localhost:8080/api/simulate?windowSeconds=900&observationIntervalSeconds=60&stallSecond=311&staleAfterSeconds=120&ingressRatePerSecond=80'The OpenTelemetry Metrics Data Model defines the No recorded value flag for
an explicitly missing point. It says that a previously present timeseries was
removed, should no longer be returned by queries, and that numeric fields on
the flagged point should be ignored:
The OpenTelemetry supplementary guidance also describes staleness markers as a way to indicate the start of a stream gap:
Prometheus similarly removes marked-stale series from queries rather than treating them as zero:
Zero is a measurement. Missing is state about a measurement. They need different storage and query behavior.
npm run checkThe test suite executes the real Make model, checks the observation and freshness boundaries, verifies flagged OTLP points contain no number, validates all three signal types, and exercises the live HTTP endpoints. CI repeats those checks and builds the container image.
MIT