4.1 KiB
retry-otel
(no submodules)
Purpose
Provides OpenTelemetry instrumentation helpers for @suaveplan/retry. This module defines the RetryMetrics interface and the createRetryMetrics factory that creates all four instruments from a caller-supplied OTel Meter. Instrument creation is lazy — no metrics are registered until createRetryMetrics is called — and the instruments are decoupled from the circuit breaker and retry engine, allowing callers to integrate metrics at the call site without modifying core retry logic.
The design follows the OTel semantic conventions for reliability metrics: counters track event totals, histograms capture distributions that matter for latency and load analysis.
Features
createRetryMetrics(meter)— creates all four instruments at oncerecordAttempt— incrementsretry.attempt.totalwithoutcomeandstrategyattributesrecordDelay— records toretry.attempt.delay_mshistogram withstrategyattributerecordCircuitStateChange— incrementscircuit_breaker.state_change.totalwithfrom/toattributesrecordCircuitCall— records tocircuit_breaker.call.duration_mshistogram withoutcomeattribute- Fully tree-shakeable — each helper is a standalone function
Usage: Basic Example
import { metrics } from "@opentelemetry/api";
import { createRetryMetrics, recordAttempt } from "@suaveplan/retry";
const meter = metrics.getMeter("my-service");
const retryMetrics = createRetryMetrics(meter);
// After each attempt:
recordAttempt(retryMetrics, { outcome: "failure", strategy: "exponential" });
Usage: Advanced Example
import { metrics } from "@opentelemetry/api";
import {
createRetryMetrics,
recordAttempt,
recordDelay,
recordCircuitStateChange,
recordCircuitCall,
CircuitBreaker,
} from "@suaveplan/retry";
const meter = metrics.getMeter("payment-service");
const m = createRetryMetrics(meter);
const breaker = new CircuitBreaker({ failureThreshold: 5, successThreshold: 2, timeout: 10_000 });
breaker.on("stateChange", ({ from, to }) => recordCircuitStateChange(m, from, to));
// Instrument a call manually:
const start = Date.now();
try {
const result = await breaker.call(() => chargeCard(payload));
recordCircuitCall(m, Date.now() - start, "success");
recordAttempt(m, { outcome: "success", strategy: "circuit-breaker" });
return result;
} catch (err) {
recordCircuitCall(m, Date.now() - start, "failure");
recordAttempt(m, { outcome: "failure", strategy: "circuit-breaker" });
throw err;
}
API Reference
createRetryMetrics(meter)
| Parameter | Type | Description |
|---|---|---|
meter |
Meter |
OTel Meter from @opentelemetry/api. |
Returns: RetryMetrics — { attemptTotal, attemptDelayMs, circuitStateChangeTotal, circuitCallDurationMs }.
recordAttempt(metrics, opts)
Increments retry.attempt.total by 1. Attributes: outcome ("success" | "failure"), strategy (string, default "unknown").
recordDelay(metrics, delayMs, strategy?)
Records delayMs to retry.attempt.delay_ms. Attribute: strategy.
recordCircuitStateChange(metrics, from, to)
Increments circuit_breaker.state_change.total by 1. Attributes: from, to (both CircuitState).
recordCircuitCall(metrics, durationMs, outcome)
Records durationMs to circuit_breaker.call.duration_ms. Attribute: outcome.
Implementation Notes
All helper functions accept RetryMetrics as their first parameter rather than using module-level singletons, making them safe for multi-tenant applications that may configure multiple meters. The functions do not set up any automatic instrumentation — callers instrument their retry loops explicitly. This is intentional: wrapping retry() internally with OTel would couple the core engine to the OTel API and add mandatory overhead even when metrics are not needed. The strategy attribute defaults to "unknown" when not provided to ensure all recorded events have a value for that dimension, preventing sparse attribute cardinality issues in OTel backends.