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,86 @@
# otel/instruments
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module is the only place in `@suaveplan/workflow` that talks to the
OpenTelemetry API directly. Every other module reaches metrics through the
`recordStepExecuted` and `recordCompensation` helpers exposed here, which
keeps OTel-specific concerns (meter resolution, attribute conventions, unit
labels) in a single audit-surface.
The instruments are registered eagerly at import time against
`metrics.getMeter("@suaveplan/workflow")`. When no SDK provider is
configured the OTel API resolves to no-op instruments, so importing this
module in unit tests is safe — nothing is exported, nothing is exfiltrated,
and the helpers degrade to cheap function calls.
## Features
- Counter `workflow.step.executed_total` with `{outcome, step_id}`.
- Counter `workflow.step.compensated_total` with `{outcome, step_id}`.
- Histogram `workflow.step.duration_ms` (unit: ms) with `{step_id}`.
- `recordStepExecuted(stepId, outcome, durationMs)` helper that fans the
attributes correctly and skips the histogram for `"skipped"` outcomes
(durations are meaningless for a guard-rejected step).
- `recordCompensation(stepId, outcome)` helper for the compensator and the
saga.
## Usage: Basic Example
```ts
import { recordStepExecuted } from "@suaveplan/workflow/otel";
recordStepExecuted("charge", "completed", 42);
```
## Usage: Advanced Example
```ts
import { metrics } from "@opentelemetry/api";
import { MeterProvider } from "@opentelemetry/sdk-metrics";
import { recordStepExecuted, recordCompensation } from "@suaveplan/workflow/otel";
const reader = new InMemoryMetricReader();
const provider = new MeterProvider({ readers: [reader] });
metrics.setGlobalMeterProvider(provider);
await engine.run({ initialData });
const snapshot = await reader.collect();
const stepCounter = snapshot.resourceMetrics.scopeMetrics[0].metrics.find(
(m) => m.descriptor.name === "workflow.step.executed_total",
);
expect(stepCounter?.dataPoints.length).toBeGreaterThan(0);
// Direct helpers also work outside the engine.
recordStepExecuted("custom", "failed", 17);
recordCompensation("custom", "compensated");
```
## API Reference
- `stepExecutedCounter: Counter` — exported for advanced inspection / typed
assertions in tests.
- `stepCompensatedCounter: Counter` — same.
- `stepDurationHistogram: Histogram` — same.
- `recordStepExecuted(stepId: string, outcome: "completed" | "failed" | "skipped", durationMs: number): void`
— increments the executed counter and records the duration histogram
(when the outcome is not `"skipped"`).
- `recordCompensation(stepId: string, outcome: "compensated" | "compensation-failed"): void`
— increments the compensated counter only.
## Implementation Notes
We deliberately skip the histogram emit on `"skipped"` outcomes because the
guard never enters `execute`, so any duration we recorded would be the wall
time of `shouldExecute` — a different signal from "step running time".
Consumers who want guard-evaluation timing should add their own histogram
inside the guard.
Attribute keys follow the OpenTelemetry semantic-conventions style
(snake_case) so they group cleanly alongside other `@suaveplan/*` metrics
(e.g. `step_id` matches `entity_id` in `@suaveplan/ecs`).