Skip to content

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:

  1. Publishes a state keepalive to the component’s UNS state topic ecv1/{device}/{component}/state — an envelope named state with body {"status": "RUNNING", "uptimeSecs": <n>}. This is the liveness signal a console, historian, or watchdog subscribes to (ecv1/+/+/state, plus ecv1/+/+/+/state for instance-scoped publishers).
  2. Emits the enabled system measures (CPU, memory, disk, threads, open files, file descriptors) as a single metric named sys through 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.

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.0 in all four languages.
  • uptimeSecs is the seconds since component start (monotonic clock). The shutdown STOPPED state omits it.
  • heartbeat.destination (local | northbound, default local) 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:

Terminal window
# Bring up the local broker (EMQX), then subscribe to all state keepalives.
docker compose -f test-infra/compose.yaml up -d
mosquitto_sub -h localhost -p 1883 -t 'ecv1/+/+/state' -t 'ecv1/+/+/+/state' -v

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 optional

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 STOPPED state never carries instances[].
  • Best-effort, never fatal. A provider that returns null / None / an empty list — or one that throws or panics — simply omits the instances[] 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 / undefined clears 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.

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.

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).

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.
  • First tick fires at roughly t = 0 (component start), then every intervalSecs.
  • 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 sys metric still flows through the metric subsystem.
  • The STOPPED state 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 derived uns-bridge site-uplink reachability signal.
  • Liveness consumers should treat a missed keepalive window (e.g. 2–3 intervals) as stale, and the STOPPED state or the uns-bridge site LWT as authoritative offline signals.