# `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 ```ts 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: ```ts 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.