# 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 ```typescript 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 ```typescript 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 }`. ## 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.