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

4.4 KiB

NumberFormatter Observability

(no submodules)

Purpose

Per-package OpenTelemetry instruments for @suaveplan/i18n-formatter-number. Exposes a histogram (i18n.format.number.duration_ms) recording wall-clock duration of every formatNumber call and a counter (i18n.format.cache_hit) tagged with formatter: "number" 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 (getNumberFormatMetricsState() and resetNumberFormatMetricsState()) so spec-conformance tests can assert the exact attribute payloads produced by recordNumberFormat and recordNumberCacheHit without needing to wire a full SDK exporter inside every test file.

Features

  • Lazy instrument construction: the first call to recordNumberFormat or recordNumberCacheHit builds both instruments via the OTel global meter
  • Stable histogram name i18n.format.number.duration_ms with attributes { locale, style }
  • Stable counter name i18n.format.cache_hit with attributes { formatter: "number" }
  • In-memory sample ring (numberDurationSamples, cacheHitSamples) for spec-conformance assertions
  • resetNumberFormatMetricsState() clears both arrays so tests stay isolated

Usage: Basic Example

import {
    recordNumberFormat,
    recordNumberCacheHit,
    getNumberFormatMetricsState,
    resetNumberFormatMetricsState,
} from "@suaveplan/i18n-formatter-number/observability";

resetNumberFormatMetricsState();
recordNumberFormat("en-GB", "decimal", 1.23);
recordNumberCacheHit();
const state = getNumberFormatMetricsState();
state.numberDurationSamples; // [{ attributes: { locale: "en-GB", style: "decimal" } }]
state.cacheHitSamples;       // [{ attributes: { formatter: "number" } }]

Usage: Advanced Example — Asserting OTel emission in a test

import { describe, expect, it } from "@suaveplan/testing/runner";
import { formatNumber, clearNumberFormatterCache } from "@suaveplan/i18n-formatter-number";
import {
    getNumberFormatMetricsState,
    resetNumberFormatMetricsState,
} from "@suaveplan/i18n-formatter-number/observability";

describe("metrics emission", () => {
    it("emits duration histogram and cache-hit counter", { timeout: 5000 }, () => {
        clearNumberFormatterCache();
        resetNumberFormatMetricsState();
        formatNumber(1, "en-GB", { style: "decimal" });
        formatNumber(1, "en-GB", { style: "decimal" });
        const state = getNumberFormatMetricsState();
        expect(state.numberDurationSamples).toHaveLength(2);
        expect(state.cacheHitSamples).toHaveLength(1);
    });
});

API Reference

recordNumberFormat(locale: string, style: string, durationMs: number): void

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

recordNumberCacheHit(): void

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

getNumberFormatMetricsState(): NumberFormatMetricsState

Returns the in-memory sample ring (live reference — do not mutate). Use resetNumberFormatMetricsState between tests to keep state isolated.

resetNumberFormatMetricsState(): void

Empties both sample arrays in place.

NumberFormatMetricsState

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

Implementation Notes

The instruments are built lazily so that consumers who never call formatNumber (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 resetNumberFormatMetricsState calls (resetting state does not invalidate the meter).

The module deliberately keeps the in-memory sample ring unbounded. Tests should call resetNumberFormatMetricsState() between cases. Long-running production processes do not retain the ring because the slim-core wave 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.