Files
skills/docs/foundation/flow/workflow/src/otel/instruments.md
2026-05-29 08:20:56 +01:00

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_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

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).