3.6 KiB
instruments
(no submodules)
Purpose
The saga orchestrator emits an OTel surface matching design doc §9.1:
three spans (saga.instance, saga.step, saga.compensate), two
counters (saga.instances_total, saga.compensations_total), one
histogram (saga.step_duration_ms), and one observable gauge
(saga.in_flight). This module centralises construction so every
caller binds against the same names with the same attribute schema.
Tests construct the bag against an in-memory MeterProvider and
BasicTracerProvider and assert exact emissions — the assertion
catches name drift, attribute drift, or missing instruments before
they leak into the operator's dashboards.
Features
- One
createSagaInstruments(meter, tracer)factory — all instruments built lazily inside the call so tests can spin up a fresh registry per case. - Names + units lifted verbatim from design §9.1.
- Span factories —
startInstanceSpan,startStepSpan,startCompensateSpan— wrap the tracer call so attribute names (saga.name,saga.tenant,step.name,attempt) stay consistent across every call site. bindInFlight(observer)— the orchestrator passes a closure that returns the live count from the persistence layer; the OTel callback path forwards the value on every collector poll.
Usage: Basic
import { metrics, trace } from "@opentelemetry/api";
import { createSagaInstruments } from "@suaveplan/api-saga/instruments/instruments";
const instruments = createSagaInstruments(
metrics.getMeter("api-saga", "0.1.0"),
trace.getTracer("api-saga", "0.1.0")
);
const span = instruments.startInstanceSpan({
sagaName: "checkout",
tenantId: "acme",
});
try {
// ... run saga
instruments.instancesTotal.add(1, {
name: "checkout",
outcome: "completed",
});
} finally {
span.end();
}
Usage: Advanced
Bind the in-flight observer to the persistence layer so the gauge reads the live running-count from the database on every collector poll. The callback is invoked once per observation window so it should be O(1) wrt. the persistence query — count, not list:
import { createSagaInstruments } from "@suaveplan/api-saga";
const instruments = createSagaInstruments(meter, tracer);
instruments.bindInFlight(() => {
return persistence.listInstances({ state: "running" }).length
+ persistence.listInstances({ state: "compensating" }).length;
});
instruments.stepDurationMs.record(elapsedMs, {
name: "checkout",
step: "charge",
outcome: "completed",
});
API Reference
createSagaInstruments(meter, tracer): SagaInstruments— builder.SagaInstruments:instancesTotal: Counter<{ name, outcome }>,stepDurationMs: Histogram<{ name, step, outcome }>,compensationsTotal: Counter<{ name, step }>,inFlight: ObservableGauge,startInstanceSpan(args),startStepSpan(args),startCompensateSpan(args)— open named spans with the design's attribute schema,bindInFlight(observer: () => number): void.
SagaOutcome,SagaStepOutcome— literal-union labels written onto counters/histogram for safe partitioning.
Implementation Notes
The instruments are typed loosely (Counter<{ ... }> is type-narrowed
at the surface level, but OTel's runtime API erases the generic);
the type pins help downstream call sites stay honest about which
attributes belong to which counter. The factory function intentionally
does not cache the bag — the orchestrator owns the lifecycle so a
host restart yields a fresh registry along with a fresh DB
connection, fresh participant registry, etc.