Skip to main content
Version: Current

instrument

@rotorsoft/act-root


@rotorsoft/act-root / act-otel / instrument

Function: instrument()

instrument(app, options?): Disposer

Defined in: libs/act-otel/src/index.ts:207

Subscribe to a built Act's lifecycle events and maintain this Prometheus metric set:

MetricTypeLabelsSource event
act_events_committed_totalcounternamecommitted
act_reactions_acked_totalcounterlaneacked
act_reactions_blocked_totalcounterlaneblocked
act_settled_totalcounter—settled
act_streams_closed_totalcounter—closed
act_events_forgotten_totalcounter—forgotten
act_notifications_totalcounter—notified
act_errors_totalcountercircuiterror
act_streams_blockedgauge—blocked_streams() per scrape

Label cardinality is bounded by design: event names come from the registry, lanes are declared at build, circuit states are three. There is deliberately no per-stream label — stream ids are unbounded and would blow up a Prometheus instance.

The alerting split from the observability guide applies: a non-zero act_streams_blocked, act_errors_total{circuit="open"} growth, and sustained act_errors_total growth in any state are page-worthy; the rest are dashboard material.

Scrape resilience. The act_streams_blocked gauge reads blocked_streams() inside its per-scrape collect(). A rejection there (a degraded store) is swallowed and logged via the Logger port — every other metric still scrapes, and when no provider answered the gauge is left alone, so prom-client keeps serving its last good sample. That matters because max(act_streams_blocked) > 0 is a documented page: publishing 0 would resolve it during the very store incident that broke the read. A partial failure still publishes the reduced sum — a failing app contributes nothing this scrape. prom-client would otherwise reject the whole registry.metrics() if any collect() rejected, blinding the dashboard exactly when the store is in trouble.

Idempotent registration. Calling instrument twice against the same registry is safe: each metric is registered idempotently (an existing metric of that name is reused rather than re-created), so a second bridge shares the counters instead of throwing prom-client's "already been registered". The two bridges then feed the same metrics. Disposing either removes the shared metrics from the registry — call each returned disposer when tearing the app down, and treat one registry as backing one logical bridge even if you constructed it in two calls.

Returns a Disposer that unsubscribes the listeners and removes the metrics from the registry — hand it to Act's dispose(...) registry so Ctrl-C tears the bridge down with everything else:

Parameters​

app​

ActSurface

A built Act orchestrator

options?​

InstrumentOptions = {}

See InstrumentOptions

Returns​

Disposer

Disposer that detaches listeners and unregisters the metrics

Example​

import { act, dispose } from "@rotorsoft/act";
import { instrument } from "@rotorsoft/act-otel";
import { register } from "prom-client";

const app = act().withState(Task).build();
dispose(instrument(app));

// serve the scrape endpoint on your HTTP surface
root.get("/metrics", async (c) => c.text(await register.metrics()));

Throws​

ZodError when options are out of range (misconfiguration surfaces at startup, not on the first scrape)