instrument
@rotorsoft/act-root / act-otel / instrument
Function: instrument()
instrument(
app,options?):Disposer
Defined in: libs/act-otel/src/index.ts:206
Subscribe to a built Act's lifecycle events and maintain this Prometheus metric set:
| Metric | Type | Labels | Source event |
|---|---|---|---|
act_events_committed_total | counter | name | committed |
act_reactions_acked_total | counter | lane | acked |
act_reactions_blocked_total | counter | lane | blocked |
act_settled_total | counter | โ | settled |
act_streams_closed_total | counter | โ | closed |
act_events_forgotten_total | counter | โ | forgotten |
act_notifications_total | counter | โ | notified |
act_errors_total | counter | circuit | error |
act_streams_blocked | gauge | โ | 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 and act_errors_total{circuit="open"} growth 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 (#1702). 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 = {}
Returnsโ
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)