first commit
This commit is contained in:
108
docs/api-stack/api/api-saga/src/instruments/instruments.md
Normal file
108
docs/api-stack/api/api-saga/src/instruments/instruments.md
Normal 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.
|
||||
Reference in New Issue
Block a user