Skip to content

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:

local.<moduleId>.status.<componentName>.<componentId>

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-Type header. A consumer that always treats a field as float64 is wrong - convert with the platform's float helper instead.
  • status is always the health axis, never the config-update axis. The two are independent and neither is derived from the other - see RFC-021. Values are DELETED -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-interval tick, 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 the int64 range, 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 0 next to a uint32 reading (sys.gsm.<id>.signalQuality), and two producers disagreeing (<column>_count, from aggregator and from gate'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.

  • flower publishes app metrics as flower.sys.mem, not flower.app.main. flower.app.main is constructed but never used for telemetry.
  • chirpstack and secomea have no application-level component and no sys/mem block.
  • trigger.app.main carries the sys/mem block but no status field, unlike every other module that publishes one - which is why status is optional in the published app schema.