ALGOSTREAM / guides / telemetry
operations /

Operations

telemetry

The monitoring layer: what it measures, where each number comes from, and the one figure it cannot honestly report during a replay.

librarylib/telemetry/coverspercentiles · health · alertssourcetelemetry.md ↗

01What the collector exposes

The collector aggregates from every subsystem without depending on any of them — subsystems register a closure, so algostream.telemetry links neither ingestion, the processors nor the runtime.

SourceWhat it contributes
Event_buspublish/drop counts per priority band, dispatch count, handler errors, queue depth
Telemetry.Histogramlatency percentiles — p50, p90, p99, p99.9, max — and SLA violation counts
Memory_monitor.latestGC statistics and heap size
{Analytics,Pairs,Time_series}.Processor.statsticks observed, processed, dropped; active keys
Ingestion.live_statsper-exchange drops, sequence gaps, stale ticks, crossed books
Runtime.Supervisorinstances, NAV, allocation, events processed
Risk_management.Monitorthe most recent risk snapshot, with its age

02Percentiles, not averages

Telemetry.Histogram is a fixed array of int Atomic.t buckets, HdrHistogram-style, with sub_bits = 4 giving 16 sub-buckets per octave and therefore at most 6.25% relative error. Recording is a bit-twiddle and one Atomic.incr — wait-free, allocation-free, and correct under concurrent producers.

An average latency is close to useless for a system whose contract is stated as a tail bound, which is why the percentiles live in library code rather than being computed by a benchmark binary.

Do not use Time_utils.LatencyMonitor.

It is backed by a Queue.t and plain refs, but Publish_to_enqueue is recorded from every producer Domain — a data race in the hot path. It remains in the tree; Telemetry.Histogram is the supported replacement.

Percentiles are exact with respect to the bucketed data: no sampling, no reservoir, no RNG. That matters, because the repo's other percentile facility (Math_utils.Statistics.create_percentile_tracker) reservoir-samples off Random.State.make_self_init and is both approximate and non-reproducible.

03Measured cost

Apple Silicon, release profile, from make tel-bench:

Operationns/op
histogram.record~13
histogram.percentile~389
bus.publish with the collector attached~474
collector.snapshot~4 010

record is what runs inline on the dispatcher for every event, so 13 ns is the number that matters. snapshot allocates and is pulled at the UI's refresh rate — 4 µs at 4 Hz is 0.0016% of a core.

04Why the collector is not a processor

The three existing bus consumers all use the same shape: an O(1) handler that enqueues into an SPSC ring, a dedicated Domain that drains it, and an Atomic.set snapshot. That shape exists because those layers do real per-tick work.

The collector does not. Its handler records one latency sample and returns. Adding a queue and a Domain would cost more than the work it defers. Snapshot assembly, which does allocate, is pulled by the caller instead of pushed.

05Decoupling

algostream.telemetry depends on nothing but the event bus. Subsystems register as closures:

Telemetry.Collector.register collector
  { Telemetry.Collector.name = "ingestion";
    metrics = (fun () -> [ ("bus_drops", 3.0); ("stale_ticks", 1.0) ]);
    health  = Some (fun () -> Health.Degraded "one feed quiet") }

Metrics are name/value pairs rather than a closed record, following the precedent Performance.Metrics.to_assoc set. A provider that raises is reported as Failed and contained — the dashboard is what you look at when something is already broken, so it must not break with it.

06Alerts

Nothing in the repo had an alert sink before this. Risk_alert exists as a bus payload and is published by five sites in data_ingestion, but its only consumer was a printf in bin/ingest.ml.

The problem an alert system actually has to solve is repetition: a degraded feed re-evaluates every sample period, and an alert that fires four times a second is noise. A code is therefore raised at most once per window, with repeats folded into a count.

The window is measured from the start of the current burst, not from the last raise. Comparing against the last raise would mean a condition re-evaluated faster than the window never re-notifies at all: each raise pushes the deadline out, so a permanently broken feed would alert once and then go quiet forever.

07Health

GET /api/health is the aggregate, and it answers 503 when anything reports Failed. It is what the Kubernetes liveness, readiness and startup probes gate on, what the container HEALTHCHECK curls, and what the AlgoStreamUnhealthy Prometheus rule watches — so it has to be able to fail.

Each subsystem registers a closure; the worst status wins.

SubsystemDegradedFailed
ingestionconnecting, or reconnecting, or a feed silent for 30 scircuit breaker open, feed silent for 120 s, or any critical_drops
runtimestrategy Domain has dropped events≥ 10,000 dropped
analytics · pairsdrain loop has dropped ticks≥ 10,000 dropped

A connected-but-silent feed is the case worth understanding. The counters cannot detect it: a feed producing nothing has also dropped nothing, so every quality metric reads zero and looks perfect. Only the connection mirror's time_since_last_message_ns distinguishes a quiet feed from a dead one, which is why ingestion's check reads it rather than the counters.

A check that raises is reported as Failed rather than propagating — one broken probe must not take down the endpoint.

08Latency during replay

End-to-end latency is now - event.timestamp_ns. For a live feed that is exactly the delivery latency. For a replayed log it is not: the events carry the timestamps they had when they were recorded, so the histogram measures the age of the log — hours or days — and the SLA alert fires on every event.

The daemon publishes "source": "replay" | "live" for exactly this reason, and the dashboard shows n/a for SLA breaches rather than a red 100%. Treat latency as meaningful only when the source is live.

09Source map

ModulePath
Lock-free histogramlib/telemetry/histogram.{ml,mli}
Health checkslib/telemetry/health.{ml,mli}
Alert registrylib/telemetry/alert.{ml,mli}
Bus subscription, providers, snapshotlib/telemetry/collector.{ml,mli}
Published aggregatelib/telemetry/snapshot.{ml,mli}
Bus flow counterslib/infrastructure/event_bus/event_bus.{ml,mli}
Benchtest/performance/telemetry_throughput.ml

10Scope and limitations

Latency, throughput, alerting, correlation status and health checks are all live.

The 50,000 ev/s throughput target and the 5 ms latency target are tracked here, but whether they hold against a live venue is unmeasured — this project has no venue connectivity, so there is no order leg to include. The figures below are publish-to-handler only, and that is the whole of what they claim.