Append-only local storage for device metrics, sensor readings, and task heartbeats.
| Event kind | Source | When |
|---|---|---|
device |
iot.telemetry.publish, IoT hub dispatch |
Each published device metric |
sensor |
Robot sensor.read() / fusion inputs |
Each sensor read during runtime |
topic |
Robot publish on declared topics |
Recorded as device telemetry (device_id=robot, metric=topic path) |
heartbeat |
Task scheduler watchdog heartbeats | Latest index on every tick; history throttled (5s per task) |
device_heartbeat |
IoT device register, liveness metrics (heartbeat/liveness/alive/ping), fleet agent health |
Latest index updated each touch; history throttled (5s per device) |
health |
Health status transitions (overall + per-check) | Runtime health polling |
session |
Run boundaries when --persist-telemetry is enabled |
start at run begin; end at completion with optional mission trace path |
runtime_metrics |
End-of-run scheduler snapshot | Same payload as --metrics-json / run JSON metrics field |
Runtime scheduler metrics are also captured as a runtime_metrics event at the end of each
persisted run. Mission traces (--record) can be linked via the session end event’s
mission_trace_path.
During an active run, every recorded event is tagged with session_id so spanda telemetry list
--session <id> returns exactly that run’s sensor reads, heartbeats, and publishes (legacy events
without session_id still match by timestamp window).
Per run:
spanda run rover.sd --persist-telemetry
spanda sim rover.sd --persist-telemetry
Or globally for the process:
export SPANDA_TELEMETRY_STORE=1
spanda run rover.sd
| File | Purpose |
|---|---|
.spanda/telemetry-store.jsonl |
Append-only event log (JSONL) |
.spanda/telemetry-heartbeats.json |
Latest heartbeat timestamp per task and device |
Set SPANDA_TELEMETRY_BACKEND=sqlite to use an indexed SQLite database instead of JSONL:
| File | Purpose |
|---|---|
.spanda/telemetry-store.db |
SQLite database (telemetry_events + heartbeat_liveness tables) |
Heartbeat liveness is stored in the heartbeat_liveness table (no JSON sidecar in SQLite mode).
On first open, an empty SQLite database automatically imports a sibling telemetry-store.jsonl (and
telemetry-heartbeats.json beside it), then renames the JSONL file to telemetry-store.jsonl.bak.
Override paths:
| Variable | Purpose |
|---|---|
SPANDA_TELEMETRY_BACKEND |
sqlite for SQLite; omit for JSONL (default) |
SPANDA_TELEMETRY_STORE_PATH |
Event log file (.jsonl) or database (.db) |
SPANDA_TELEMETRY_HEARTBEAT_PATH |
Heartbeat index file (JSONL mode only) |
SPANDA_TELEMETRY_MAX_EVENTS |
Trim oldest events when the log exceeds this count |
Files live under .spanda/ (gitignored) like deploy and fleet state.
spanda telemetry list [--device <id>] [--sensor <id>] [--task <name>] [--session <id>] [--kind device|sensor|heartbeat|device_heartbeat|health|session|runtime_metrics] [--since <ms>] [--limit <n>] [--json]
spanda telemetry latest [--device <id> [--metric <name>] | --sensor <id> | --task <name>] [--json]
spanda telemetry heartbeats [--json]
spanda telemetry devices [--json]
spanda telemetry stats [--json]
spanda telemetry info [--json]
spanda telemetry sessions [--json]
spanda telemetry replay --session <id> [--from T+mm:ss] [--deterministic] [--playback] [--json]
spanda telemetry export [--out <file.jsonl>]
spanda telemetry prometheus [--out <file.prom>]
spanda telemetry otlp [--out <file.json>]
spanda telemetry push --endpoint <url> [--token <t>] [--watch] [--interval <ms>]
spanda telemetry fleet-push --mesh <url> --endpoint <collector> [--token <t>]
spanda telemetry serve [--bind <addr>] [--once]
spanda sim examples/end_to_end/validated_telemetry.sd --persist-telemetry --record
spanda telemetry list --kind session --json
spanda telemetry sessions --json
spanda telemetry replay --session <id> --deterministic
spanda telemetry list --kind runtime_metrics --json
spanda telemetry stats
spanda telemetry list --kind sensor --json
spanda telemetry latest --device TelemetryRover --metric /telemetry
spanda telemetry heartbeats
spanda telemetry devices
export SPANDA_TELEMETRY_BACKEND=sqlite
spanda run rover.sd --persist-telemetry
spanda telemetry stats
Or run the scripted golden path: ./scripts/telemetry_store_golden_path.sh
Scrape the store as Prometheus text exposition (no server required):
spanda telemetry prometheus
spanda telemetry prometheus --out metrics.prom
Exports event totals, heartbeat timestamps, latest runtime_metrics scheduler/task counters,
numeric device metrics, and health scores. Point Prometheus at a file written by --out, or pipe
stdout into your collector.
Emit OTLP/JSON metrics for OpenTelemetry collectors:
spanda telemetry otlp
spanda telemetry otlp --out metrics.otlp.json
POST the current store snapshot to an OpenTelemetry HTTP collector:
export SPANDA_OTLP_ENDPOINT=http://collector:4318/v1/metrics
spanda telemetry push
spanda telemetry push --endpoint https://collector.example/v1/metrics --token "$OTEL_TOKEN"
spanda telemetry push --watch --interval 15000
When a run ends with persistent telemetry enabled, push OTLP metrics automatically:
export SPANDA_TELEMETRY_STORE=1
export SPANDA_OTLP_AUTO_PUSH=1
export SPANDA_OTLP_ENDPOINT=http://collector:4318/v1/metrics
# optional: SPANDA_OTLP_TOKEN, SPANDA_OTLP_PUSH_INTERVAL_MS (watch mode default 30000)
spanda run examples/demo.sd
--watch runs a blocking push loop (default interval from SPANDA_OTLP_PUSH_INTERVAL_MS, 30s).
Auto-push fires once per session end on both Rust and TypeScript run paths.
When a run ends with persistent telemetry enabled, push the local OTLP snapshot to the fleet mesh:
export SPANDA_TELEMETRY_STORE=1
export SPANDA_FLEET_TELEMETRY_AUTO_INGEST=1
export SPANDA_FLEET_MESH_URL=http://mesh:8788
export SPANDA_ROBOT_ID=rover-a
# optional: SPANDA_FLEET_MESH_TOKEN
spanda run examples/demo.sd
Robots POST OTLP snapshots to the fleet mesh; operators push merged metrics to a collector:
# Agent / runtime ingest (per robot)
curl -X POST "$SPANDA_FLEET_MESH_URL/v1/fleet/telemetry/ingest" \
-H "Content-Type: application/json" \
-d '{"robot_id":"rover-a","otlp_json":"..."}'
# Operator push merged fleet OTLP
export SPANDA_FLEET_MESH_URL=http://mesh:8788
export SPANDA_OTLP_ENDPOINT=http://collector:4318/v1/metrics
spanda telemetry fleet-push
Mesh endpoints: POST /v1/fleet/telemetry/ingest, GET /v1/fleet/telemetry (merged OTLP/JSON with
spanda.robot.id resource attributes).
The payload matches spanda telemetry otlp (OTLP/JSON resourceMetrics shape). Use with spanda
telemetry serve for local collector testing.
Run a local metrics endpoint for Prometheus or OTLP polling:
spanda telemetry serve
spanda telemetry serve --bind 0.0.0.0:9090
| Path | Format |
|---|---|
GET /metrics |
Prometheus text |
GET /otlp/v1/metrics |
OTLP/JSON |
GET /healthz |
Liveness (ok) |
Device liveness is recorded when:
iot.device.register succeedsiot.telemetry.publish uses metric heartbeat, liveness, alive, or pingspanda fleet agent health checks succeed (protocol=fleet-agent)/v1/health (protocol=deploy-agent)publish on declared topics (metric = topic path, e.g. /telemetry)Implementation: crates/spanda-telemetry-store (TelemetryEvent, PersistentTelemetryStore,
in-memory MemoryTelemetryStore for WASM).
TypeScript mirror: src/telemetry-store.ts records sensor reads, topic publishes, task/device
heartbeats, health transitions, session boundaries, and runtime_metrics (scheduler, task,
execution, pipeline, watchdog, trigger, topic QoS deadline misses, and provider-call counters) when
persistTelemetry is set on run() or SPANDA_TELEMETRY_STORE=1. The TS interpreter uses
src/runtime/trigger-registry.ts for unified trigger dispatch (every, when, while, and legacy
on handlers) with per-tick storm limits and missed timer-deadline metrics aligned with Rust. With
SPANDA_TELEMETRY_BACKEND=sqlite, Node.js 22+ (node:sqlite) is required for the TS path;
otherwise use the native Rust CLI. spanda telemetry tries the native Rust CLI first, then falls
back to src/telemetry-cli.ts (including serve, push, replay --session
inspect/playback/deterministic verification, and info). Mission traces written by the Rust CLI
use snake_case JSON fields; the TS replay loader normalizes them for inspect and verify.
wasm_telemetry_clear, wasm_telemetry_append (JSONL line), wasm_telemetry_stats,
wasm_telemetry_prometheus, and wasm_telemetry_otlp. Successful wasm_run calls also append a
runtime_metrics snapshot to the in-memory buffer. TypeScript wrappers live in
packages/web/src/spanda-wasm.ts (telemetryClear, telemetryAppend, telemetryStats,
telemetryPrometheus, telemetryOtlp). The WASM crate depends on spanda-telemetry-store with
default-features = false (no SQLite/rusqlite).
See also iot.md, watchdogs.md, replay.md.