Files
skills/docs/localization/i18n/i18n-formatter-relative-time/src/observability/observability.md
2026-05-29 08:20:56 +01:00

4.3 KiB

RelativeTimeFormatter Observability

(no submodules)

Purpose

Per-package OpenTelemetry instruments for @suaveplan/i18n-formatter-relative-time. Exposes a histogram (i18n.format.relative_time.duration_ms) recording wall-clock duration of every formatRelativeTime call (tagged with locale, style, and the resolved unit) and a counter (i18n.format.cache_hit) tagged with formatter: "relative_time" that increments on every cache reuse.

The two instruments are constructed once via metrics.getMeter and are lazily initialised on first record so cold-start latency stays low. The module also retains an in-memory sample ring so spec-conformance tests can assert exact attribute payloads without standing up a full SDK exporter.

Features

  • Lazy instrument construction
  • Stable histogram name i18n.format.relative_time.duration_ms with attributes { locale, style, unit }
  • Stable counter name i18n.format.cache_hit with attributes { formatter: "relative_time" }
  • In-memory sample ring (relativeTimeDurationSamples, cacheHitSamples)
  • resetRelativeTimeFormatMetricsState() clears both arrays so tests stay isolated

Usage: Basic Example

import {
    recordRelativeTimeFormat,
    recordRelativeTimeCacheHit,
    getRelativeTimeFormatMetricsState,
    resetRelativeTimeFormatMetricsState,
} from "@suaveplan/i18n-formatter-relative-time/observability";

resetRelativeTimeFormatMetricsState();
recordRelativeTimeFormat("en-GB", "long", "minutes", 1.23);
recordRelativeTimeCacheHit();
const state = getRelativeTimeFormatMetricsState();
state.relativeTimeDurationSamples.length; // 1
state.cacheHitSamples.length; // 1

Usage: Advanced Example — Asserting OTel emission in a test

import { describe, expect, it } from "@suaveplan/testing/runner";
import {
    formatRelativeTime,
    clearRelativeTimeFormatterCache,
} from "@suaveplan/i18n-formatter-relative-time";
import {
    getRelativeTimeFormatMetricsState,
    resetRelativeTimeFormatMetricsState,
} from "@suaveplan/i18n-formatter-relative-time/observability";

describe("metrics emission", () => {
    it("emits duration histogram and cache-hit counter", { timeout: 5000 }, () => {
        clearRelativeTimeFormatterCache();
        resetRelativeTimeFormatMetricsState();
        formatRelativeTime(60_000, "en-GB");
        formatRelativeTime(60_000, "en-GB");
        const state = getRelativeTimeFormatMetricsState();
        expect(state.relativeTimeDurationSamples).toHaveLength(2);
        expect(state.cacheHitSamples).toHaveLength(1);
    });
});

API Reference

recordRelativeTimeFormat(locale, style, unit, durationMs): void

Records one observation on the i18n.format.relative_time.duration_ms histogram and pushes a { attributes: { locale, style, unit } } entry into the in-memory sample ring.

recordRelativeTimeCacheHit(): void

Adds 1 to the i18n.format.cache_hit counter with formatter: "relative_time" and pushes a matching entry into the in-memory sample ring.

getRelativeTimeFormatMetricsState(): RelativeTimeFormatMetricsState

Returns the in-memory sample ring (live reference — do not mutate).

resetRelativeTimeFormatMetricsState(): void

Empties both sample arrays in place.

RelativeTimeFormatMetricsState

{ relativeTimeDurationSamples: FormatMetricSample[]; cacheHitSamples: FormatMetricSample[] } where FormatMetricSample = { attributes: Record<string, string | number> }.

Implementation Notes

The instruments are built lazily so consumers who never call formatRelativeTime (e.g. tree-shaken builds that import only types) do not pay the OTel meter-lookup cost. The cache check is a strict identity check against the closure-scoped cached slot — once built, the instruments are never rebuilt, even across resetRelativeTimeFormatMetricsState calls (resetting state does not invalidate the meter).

The module deliberately keeps the in-memory sample ring unbounded. Tests should call resetRelativeTimeFormatMetricsState() between cases. Long-running production processes should not retain the ring; the slim-core wave (Wave 6) swaps the in-package observability module for the unified one in @suaveplan/i18n core. For now, every leaf package owns its own ring per the design doc's per-package shape.