3.2 KiB
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_totalwith{outcome, step_id}. - Counter
workflow.step.compensated_totalwith{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
import { recordStepExecuted } from "@suaveplan/workflow/otel";
recordStepExecuted("charge", "completed", 42);
Usage: Advanced Example
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).