Heartbeat
The heartbeat subsystem is your component’s liveness beacon on the Unified Namespace. On every tick it does two things, with zero component code:
- Publishes a
statekeepalive to the component’s UNS state topicecv1/{device}/{component}/state— an envelope namedstatewith body{"status": "RUNNING", "uptimeSecs": <n>}. This is the liveness signal a console, historian, or watchdog subscribes to (ecv1/+/+/state, plusecv1/+/+/+/statefor instance-scoped publishers). - Emits the enabled system measures (CPU, memory, disk, threads, open files, file descriptors)
as a single metric named
systhrough the normal metrics subsystem — so the measures inherit the metric subsystem’s full sink routing (log file, messaging, CloudWatch, Prometheus).
On graceful shutdown the library additionally publishes a best-effort {"status": "STOPPED"} state
(at most once, without uptimeSecs), so consumers can distinguish a clean stop from a silent
disappearance.
The heartbeat is on by default — every component announces itself every 5 seconds in all
four languages the moment it starts. You shape it with the heartbeat config section; the one
runtime lever is the optional per-instance connectivity provider.
The state keepalive
Section titled “The state keepalive”Each tick publishes one envelope to ecv1/{device}/{component}/state:
{ "header": { "name": "state", "version": "1.0", "timestamp": "…", "uuid": "…" }, "identity": { "hier": [ { "level": "device", "value": "gw-01" } ], "path": "gw-01", "component": "my-component", "instance": "main" }, "body": { "status": "RUNNING", "uptimeSecs": 125 }}- The message name and version are
state/1.0in all four languages. uptimeSecsis the seconds since component start (monotonic clock). The shutdownSTOPPEDstate omits it.heartbeat.destination(local|northbound, defaultlocal) selects where the keepalive goes: the local/IPC transport, or AWS IoT Core. It governs the keepalive only — the measures route through the metric subsystem’s own target (metricEmission).
Watch a device’s components come alive from any MQTT client:
# Bring up the local broker (EMQX), then subscribe to all state keepalives.docker compose -f test-infra/compose.yaml up -dmosquitto_sub -h localhost -p 1883 -t 'ecv1/+/+/state' -t 'ecv1/+/+/+/state' -v# Bring up the local broker (EMQX), then subscribe to all state keepalives.docker compose -f test-infra/compose.yaml up -dmosquitto_sub -h localhost -p 1883 -t 'ecv1/+/+/state' -t 'ecv1/+/+/+/state' -vPer-instance connectivity
Section titled “Per-instance connectivity”A single component often fronts many southbound connections: an OPC UA adapter talking to several
servers, a Modbus adapter polling several slaves, a file-replicator watching several source
directories. Each can be reachable or down independently — and an operator wants to see which. The
state keepalive carries this per-connection reachability in an optional instances[] array,
without minting a phantom UNS instance per connection (the component stays one component, addressed
at component scope with no instance token).
You surface it by registering a connectivity provider — the heartbeat samples it on every RUNNING tick and, when it returns entries, folds them into the state body:
gg.setInstanceConnectivityProvider(() -> List.of( InstanceConnectivity.of("kep1", device1.isConnected(), "opc.tcp://kep1:49320"), InstanceConnectivity.of("kep2", device2.isConnected()))); // detail optionalgg.set_instance_connectivity_provider(lambda: [ InstanceConnectivity.of("plc1", dev1.is_connected(), "tcp://10.0.0.50:502 unit=1"), InstanceConnectivity.of("plc2", dev2.is_connected()), # detail optional])gg.set_instance_connectivity_provider(Some(Arc::new(|| vec![ InstanceConnectivity::new("src1", src1.is_reachable(), Some("/mnt/in".into())), InstanceConnectivity::of("src2", src2.is_reachable()), // no detail])));gg.setInstanceConnectivityProvider(() => [ InstanceConnectivity.of("kep1", dev1.connected, "opc.tcp://kep1:49320"), InstanceConnectivity.of("kep2", dev2.connected),]);Each element is { instance, connected, detail? } — the connection id, its live reachability, and an
optional human detail (typically the endpoint, or a down reason). The RUNNING state body then reads:
{ "status": "RUNNING", "uptimeSecs": 125, "instances": [ { "instance": "kep1", "connected": true, "detail": "opc.tcp://kep1:49320" }, { "instance": "kep2", "connected": false } ]}Rules
- RUNNING only. The provider is sampled on RUNNING keepalives; the shutdown
STOPPEDstate never carriesinstances[]. - Best-effort, never fatal. A provider that returns
null/None/ an empty list — or one that throws or panics — simply omits theinstances[]section for that tick; it never suppresses the keepalive itself. Keep the callback cheap and non-blocking (sample a cached flag, don’t do I/O). - Set once, replace or clear anytime. Registering again replaces the provider; passing
null/None/undefinedclears it (the section disappears on the next tick).
Why an array and not per-connection instances. A multi-server adapter is still one component —
its identity, data, config, and lifecycle all live at component scope (no instance token). Reporting each connection as a
first-class UNS instance would mint phantom components a console then has to reconcile. Instead the one
state topic carries a connectivity sub-table, so a console shows a single component with a
per-connection health view. The reference adapters use exactly this: opcua-adapter reports one
entry per configured server (detail = the endpoint), modbus-adapter one per slave
(detail = tcp://host:port unit=N), and file-replicator one per watched source directory.
The sys measures metric
Section titled “The sys measures metric”When measures are enabled, each tick also emits a metric named sys through the metric
subsystem, carrying one value per enabled measure (flattened: cpu_usage, memory_usage,
disk_total/disk_used/disk_free, threads, files, fds). Where it lands is decided by your
metricEmission config — a local metric log by default, or messaging (the UNS
metric class), CloudWatch, or Prometheus. The metric’s storageResolution is 1 when the
interval is under 60 seconds, otherwise 60.
What it measures
Section titled “What it measures”Counter availability varies by language and platform. Where a counter is enabled but unavailable on the current platform, the value falls back as noted.
| Measure | Unit | Availability and fallbacks |
|---|---|---|
cpu |
Percent | All languages, all platforms. |
memory |
MB (resident set) | All languages, all platforms. Java reports whole-number MB (integer division); Python, Rust, and TypeScript report fractional MB. |
disk |
Gigabytes (disk_total, disk_used, disk_free; disk_used = disk_total − disk_free) |
All languages, all platforms. Java and Python measure the filesystem holding the parent of the working directory (..); Rust and TypeScript measure the current working directory’s filesystem. (Usually the same mount, so the numbers match.) |
threads |
Count | Java and Python: all platforms. Rust: Linux and Windows. TypeScript: Linux only (returns 0 elsewhere). |
files |
Count (open files) | Java and Python: all platforms. Rust: Linux (regular-file descriptors) and Windows (total handles). TypeScript: Linux only (returns 0 elsewhere). |
fds |
Count (open file descriptors) | Java: Unix only (-1 on Windows). Python: all platforms (Windows reports handle count). Rust: Linux and Windows. TypeScript: Linux only (returns 0 elsewhere). |
Configuration
Section titled “Configuration”The heartbeat section has four keys — and the defaults are identical in all four languages,
so an absent section gives you a working keepalive out of the box:
{ "heartbeat": { "enabled": true, "intervalSecs": 5, "measures": { "cpu": true, "memory": true, "disk": false, "threads": false, "files": false, "fds": false }, "destination": "local" }}| Field | Type | Default | Meaning |
|---|---|---|---|
enabled |
boolean | true |
Whether the heartbeat (state keepalive + sys measures metric) runs. |
intervalSecs |
integer (min 1) | 5 |
Tick period in seconds. |
measures.* |
boolean | cpu/memory on, rest off |
Per-measure toggles for the sys metric. |
destination |
string | local |
local or northbound — where the state keepalive publishes. The measures route through metricEmission. |
Behavior notes
Section titled “Behavior notes”- First tick fires at roughly
t = 0(component start), then everyintervalSecs. - Every tick is exception-guarded, and the state and metric halves are individually best-effort: a failure in one is logged and does not suppress the other, and the next tick still fires.
- No messaging transport (a metrics-only run) skips the keepalive; the
sysmetric still flows through the metric subsystem. - The
STOPPEDstate is best-effort — published at most once on graceful shutdown (SIGTERM/close); a hard kill obviously cannot publish it. Generic component MQTT config does not expose Last-Will; the only first-party LWT is the private deriveduns-bridgesite-uplink reachability signal. - Liveness consumers should treat a missed keepalive window (e.g. 2–3 intervals) as stale, and the
STOPPEDstate or theuns-bridgesite LWT as authoritative offline signals.