# otel/instruments _(no submodules)_ ## 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`).