first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
# `instruments`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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.