Telemetry Reference¶
Every status message the platform publishes, by component.
Components with a registered status struct (RFC-029 D4) are documented in the generated Typed Status Reference instead - their per-module tables here shrink to pointers as modules adopt. This page keeps everything else: the not-yet-typed components, the wire encoding, the numeric-kind rules and the module id table.
Telemetry is a measurement published on the status messaging context, one
message per component per tick:
The mechanism is ADR-007; this page is the inventory.
A field is addressed as an attribute reference, <moduleId>.<componentName>.<componentId>.<field>,
which is what a trigger status_val() expression names.
How to read this page¶
- Component ref is the pattern, with the module id at its flag default (
--id). A device running two instances of a module has two sets of everything below. - Type is the published kind of the field. Durations are float seconds. Enumerated
values (component health, LED colour/pattern, recorder export and recstorage states)
are published as their integer index. Local status and data_status use InfluxDB line
protocol (RFC-004, RFC-029),
which preserves signed / unsigned / float kinds. JSON is the decode fallback for a
message published without the
Content-Typeheader. A consumer that always treats a field asfloat64is wrong - convert with the platform's float helper instead. statusis always the health axis, never the config-update axis. The two are independent and neither is derived from the other - see RFC-021. Values areDELETED -3,DISABLED -2,ERROR -1,UNKNOWN 0,OK 1,WAIT 2.- Telemetry is lossy by design. A producer that cannot send without blocking drops the sample and logs a warning.
- Cadence is the module's
--status-pub-intervaltick, always. A component whose state a consumer acts on additionally publishes at the transition, so a state shorter than the interval is still observable (RFC-029 D1); an on-change emit is the component's complete measurement, never a subset. Which components do this is noted in the tables below. It is never the only path to a state - a consumer that starts late learns it from the next tick.
Numeric kinds on the wire¶
Line protocol distinguishes 123i signed, 123u unsigned and 123 float. A time-series
store fixes a column's type on the first point it sees and rejects later points of another
kind. The previous JSON encoding carried no kind, so the store inferred one - which is why
existing columns are signed and new fields have to stay compatible with them.
Two rules:
- An integral field is published as
int64, never as unsigned. Counters cannot reach theint64range, so the conversion is lossless. New fields follow the same rule unless the quantity is genuinely fractional. - One field name has one kind, on every branch and from every producer. Both failures
have occurred: a fallback branch publishing an untyped
0next to auint32reading (sys.gsm.<id>.signalQuality), and two producers disagreeing (<column>_count, fromaggregatorand fromgate's re-aggregation).
A duration field is a float column (seconds). Where an integer is wanted, publish
milliseconds as int64 and name the field for it - recorder.sink.<id>.channelTimeSkewMs,
recorder.<profile>.<id>.runCopyMs.
Module ids¶
| Module | Default --id |
Module | Default --id |
|---|---|---|---|
aggregator |
aggregator |
nats |
nats |
board-controller |
bc |
recorder |
recorder |
chirpstack |
chrp |
retx |
retx |
chronyd |
chrony |
secomea |
secomea |
configurer |
cfg |
sys |
sys |
flower |
flower |
trigger |
trigger |
gate |
gate |
webui |
webui |
gps |
gps |
Shared field blocks¶
Three blocks are contributed by shared code and appear under many module ids. They are described once here and referenced by name in the per-module tables below.
sys/mem block¶
Appended to whichever measurement a module designates as its application-level one.
This block is typed as part of the app status
(RFC-029 D4; blocks SysMem / SysCpu, version and uptime as direct fields), and
every app.main producer builds it through the typed path. The table below stays
only for the one ref that is not app.main - flower.sys.mem, the recorded
mis-ref, which keeps the legacy append path; a parity test keeps the two paths'
field sets identical.
| Field | Type | Meaning |
|---|---|---|
version |
string | Algol version string |
uptime |
int64 | Seconds since process start |
memAlloc |
int64 | Heap bytes currently allocated |
memTotalAlloc |
int64 | Cumulative heap bytes allocated |
memSys |
int64 | Bytes obtained from the OS |
memNumGC |
int64 | Completed garbage collections |
cpuUserCounter |
int64 | User jiffies from /proc/self/stat (Linux only) |
cpuSystemCounter |
int64 | System jiffies from /proc/self/stat (Linux only) |
Data source block¶
Emitted under the host module's id by any module that runs a data source
(gps, retx, board-controller, and any future one). Typed (RFC-029 D4) -
field tables in the generated reference:
| Component ref | Typed as |
|---|---|
<mod>.dsource.<dataSourceId> |
dsource |
<mod>.channel.<channelId> |
channel |
<mod>.dsproc_frameValidator.<id> |
dsproc_frameValidator |
<mod>.chproc_frameValidator.<id> |
chproc_frameValidator |
<mod>.dsproc_resampler.<id> |
dsproc_resampler |
<mod>.chproc_resampler.<id> |
chproc_resampler |
The processor prefix carries the chain - dsproc_ for the data source's ingress
chain (which always holds one frameValidator, id <dataSourceId>_ingress),
chproc_ for a per-channel chain - so each processor type occupies two component
names with one shared field set. windower, columnSelector, fft and
reducerExpr emit nothing.
Config command runner block¶
Constructed by secomea and chronyd, but only secomea currently collects
it. Typed (RFC-029 D4): <mod>.runner.main publishes
runner in the generated reference.
sys¶
Every sys component is typed (RFC-029 D4) - the field tables are in the generated reference:
| Component ref | Typed as | Note |
|---|---|---|
sys.app.main |
app |
status is always OK today; the only producer of appStartCounter |
sys.sysapp.main |
sysapp |
|
sys.system.main |
system |
Only published with --proc-dir; carries the dynamic thermal-zone tail (below) |
sys.gsm.<connectionId> |
gsm |
Also on change of status (below) |
sys.ethernet.<id> |
ethernet |
Also on change of status (below) |
sys.wlan.<id> |
wlan |
Also on change of status (below); the radio block is absent while the device is associated with no access point |
sys.bridge.<id> |
bridge |
Also on change of status (below) |
sys.led.<ledId> |
led |
One field per element the config declares; an undeclared element is absent. Also on change |
sys.systemdscript.<id> |
systemdscript |
|
sys.systemdapp.<id> |
systemdapp |
|
sys.systemdmount.<slot> |
systemdmount |
|
sys.filecache.main |
filecache |
filecache.cacheEvictionCounter is not the same signal as the directory bound being
reached: --max-non-volatile-file-dir-bytes covers the whole non-volatile tree, so a
tree filled with data:/file: payloads can refuse an http apply with the counter
still at 0. dirBytes is that tree, which is why it is the field to compare against the
bound - cacheBytes is only the part of it the cache can free. Its cache half is live
and the rest is as of the last file apply, since walking the tree on the collector's
tick would be a directory read on the main loop. A sweep of an unretained artifact is
deliberately not counted.
Every network config type that owns an interface publishes its health and that
interface's counters; dummyintf and dns have a config-status axis only. A dummy
interface carries what is routed to it on purpose, so its counters would restate a
routing decision rather than report a link.
The four connection types share one status, derived from the profile and the live
connection exactly as the config-update axis derives it - the same statement on both
axes, published on the same component ref. sys emits it at the transition as well as on
the tick, which is what makes a link going down reachable to a trigger: the config axis
carries the transition immediately too, but no expr can match that axis. The guard is
status alone, so a wlan's drifting signalStrength does not turn the seam into a
second, faster tick.
recorder¶
Every recorder component is typed (RFC-029 D4) - the field tables are in the generated reference:
| Component ref | Typed as | Note |
|---|---|---|
recorder.app.main |
app |
status is recording-pipeline health from recstorage.HealthStatus(); DISABLED while recstorage is off |
recorder.mdispatcher.main |
mdispatcher |
|
recorder.sink.<measurementId> |
sink |
|
recorder.sinkchannel.<base32(channelRef)> |
sinkchannel |
|
recorder.recstorage.main |
recstorage |
Emitted only while a recstorage config exists |
recorder.s3upload.<id> |
s3upload |
See export profiles |
recorder.usbexport.<id> |
usbexport |
See export profiles |
Export profiles¶
Both export profile types publish under their config component ref, and both go
through the same worker, so the field set is shared. A profile with no worker
(recstorage inactive, or a refused config) publishes only status and exportState -
which is why only those two are required in the schema.
The field sets are typed (RFC-029 D4): see s3upload and
usbexport in the generated reference, declared as status
structs in lib/messaging/status and published as
schema/status/{s3upload,usbexport}.v1.schema.json. usbexport's destination fields
are contributed by the transport (usb.Copier.Destination) through the profile
type's status composer, because a transport's destination is a filesystem, which
nothing above the export seam knows.
trigger¶
Every code-named trigger component is typed (RFC-029 D4) - the field tables are in the generated reference:
| Component ref | Typed as | Note |
|---|---|---|
trigger.app.main |
app |
No status field - see Inconsistencies |
trigger.triggerapp.main |
triggerapp |
Also emitted on every config update and delete |
trigger.trigger.<triggerId> |
trigger |
exprStoreSize only for a running streamExpr rule |
trigger.cmd.publisher |
cmd |
|
trigger.<name>.<triggerId> |
operator-defined | The status trigger action - dynamic, see below |
The status action's fields are checked at config-update time (RFC-029 D6): a literal
field value must be a string, a number or a boolean, and the action's name must not
be a registered status component name - the name is the downstream table, and an
operator-defined field set must not write into a typed one. Known gap: a status
action nested inside a composite action never emits (the composite does not forward
metrics collection).
configurer (cfg)¶
Typed (RFC-029 D4) - field tables in the generated reference:
| Component ref | Typed as | Note |
|---|---|---|
cfg.app.main |
app |
status is always OK today |
cfg.store.main |
store |
|
cfg.authz.main |
authz |
the RFC-035 decision point; published whether or not any role is configured |
board-controller (bc)¶
Typed (RFC-029 D4) - field tables in the generated reference:
| Component ref | Typed as | Note |
|---|---|---|
bc.app.main |
app |
status is always OK today |
bc.bcunit.<connectorName> |
bcunit |
|
bc.bcreg.<registerComponentName> |
dynamic (RFC-029 D6, declared as bcreg) |
One int64 field per configured telemetry register, named by the register's attribute name; an errored read is absent that tick |
bc.dsource.*, bc.channel.* |
data source block (typed, see above) |
bcunit's six counters were the platform's last unsigned columns; the typing converted
them to int64 while no cloud column existed yet, per
Numeric kinds on the wire - the first point written to the
cloud creates the columns signed.
gps¶
Typed (RFC-029 D4) - field tables in the generated reference:
| Component ref | Typed as | Note |
|---|---|---|
gps.app.main |
app |
status is always OK today |
gps.gpsapp.main |
gpsapp |
|
gps.dsource.*, gps.channel.* |
data source block (typed, see above) |
gate¶
Typed (RFC-029 D4) - field tables in the generated reference:
| Component ref | Typed as | Note |
|---|---|---|
gate.app.main |
app |
status is OK when any target is enabled and holds a connection object, else DISABLED. Not a reachability signal - the object exists from the moment the entry applied |
gate.remoteconn.<gateConfigId> |
remoteconn |
authzErrors is cumulative - that a refusal has happened, not that it is happening; the live signal is status, ERROR while the remote is still refusing |
Gate also re-publishes other modules' data_status measurements under the original
producer's component ref, re-aggregated over the forward window
(ADR-009). Those field sets
are dynamic - <column>_sum, _min, _max, _avg are float64 and _count is int64,
the same shape the aggregator publishes directly.
flower¶
| Component ref | Field | Type |
|---|---|---|
flower.sys.mem |
sys/mem block + status |
Not flower.app.main - see Inconsistencies |
flower.flow.main |
flow (typed, RFC-029 D4) |
flower.sys.mem stays untyped on purpose: typing it would mean registering the
component name sys platform-wide for a legacy mis-ref. It keeps the hand-written
sys/mem block row until the ref itself is fixed.
retx¶
Typed (RFC-029 D4) - field tables in the generated reference:
| Component ref | Typed as | Note |
|---|---|---|
retx.app.main |
app |
status is always OK today |
retx.retxdsource.<sourceName> |
retxdsource |
|
retx.bytesreader.main |
bytesreader |
|
retx.dsource.*, retx.channel.* |
data source block (typed, see above) |
chronyd (chrony)¶
Typed (RFC-029 D4) - field tables in the generated reference:
| Component ref | Typed as | Note |
|---|---|---|
chrony.app.main |
app |
status is always OK today |
chrony.chronyapp.main |
chronyapp |
secomea¶
Typed (RFC-029 D4) - field tables in the generated reference:
| Component ref | Typed as | Note |
|---|---|---|
secomea.app.main |
app |
status is the remote-access state: DISABLED while the config is disabled, WAIT while connecting inside the grace window, ERROR when the sitemanager should be connected but is not, UNKNOWN while its status cannot be read |
secomea.runner.main |
runner |
Before 2026-09 the health was published as secomea.manager.main (typed manager,
a bare status); that component and its status type were removed when the module
adopted app.main.
chirpstack (chrp)¶
Typed (RFC-029 D4): chrp.lorastats.D<deveui> publishes
lorastats in the generated reference.
No application-level component and no sys/mem block. The DevEUI is lowercased and
prefixed with D so it is a valid component id.
aggregator¶
Publishes no telemetry of its own. It derives per-batch column statistics from the data
stream and publishes them on data_status under the source channel's component ref:
<column>_min, _max, _avg, _sum are float64 and _count is int64. gate
re-publishes the same field names after re-aggregating them and must use the same kinds.
A consumer must not treat _count as float64.
nats¶
nats.app.main is not a telemetry message. It is a NATS micro request/reply endpoint
returning a ComponentStatus on demand.
nats.auth.main is published on the tick (--status-pub-interval) under --auth only
(RFC-034 D10); with bus authentication off the component does not exist. It is typed
(RFC-029 D4): auth in the generated reference.
webui¶
webui.httpserver.main is published on the tick (--status-pub-interval, default 10 s).
It is typed (RFC-029 D4): httpserver in the generated
reference. notFound counts every 404 the server answered, whichever handler wrote it.
No webui.app.main and no sys/mem block.
Modules with no telemetry¶
cli, algold, audio, docs, test-ret.
Dynamic field sets¶
Four field sets cannot be enumerated statically, because they are named by configuration or by the host rather than by code:
| Component | Named by |
|---|---|
trigger.<name>.<triggerId> |
The operator's status trigger action fields object |
bc.bcreg.<registerComponentName> |
The configured telemetry registers' attribute names |
sys.system.main thermal zones |
The host's thermal zone types |
data_status re-emissions (aggregator, gate) |
The data channel's column names |
The thermal zones are a dynamic tail on an otherwise typed status: the system
struct spells every code-named field, and the zone fields are appended after the
projection because no struct can spell a host-chosen name.
sys.led.<ledId> used to sit in this table as the bounded case - element names drawn
from a closed set - and is now simply typed: the registered led status declares every
element as an optional field, absent for an element the indicator does not declare.
Named scalar types¶
sys.led.<id>'s per-element fields are an enumerated pattern (off / on / blink)
published as an integer index - declared as LedPattern on the registered led
status. JSON and line protocol agree on that numeric index.
A new enumerated telemetry field must publish as that integer index. A named type that
does not is stringified by line protocol instead - the build gate that catches this
(RFC-029 D4/D5, the status target of
tools/schemagen) covers only components with a registered status struct; a field on a
component that has not adopted RFC-029 D4 yet stays unchecked.
Non-scalar fields¶
recorder.sinkchannel.<id>.lastTimestamp is formatted to an RFC3339Nano string by the
producer. A non-scalar field that is not formatted explicitly is stringified by line
protocol instead - format it rather than relying on that.
Known inconsistencies¶
Recorded here rather than fixed: a component ref is the measurement (table) name in every downstream time-series store, so renaming one is a breaking change for dashboards and queries, not a cleanup.
flowerpublishes app metrics asflower.sys.mem, notflower.app.main.flower.app.mainis constructed but never used for telemetry.chirpstackandsecomeahave no application-level component and no sys/mem block.trigger.app.maincarries the sys/mem block but nostatusfield, unlike every other module that publishes one - which is whystatusis optional in the publishedappschema.