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
recordNumberFormatorrecordNumberCacheHitbuilds both instruments via the OTel global meter - Stable histogram name
i18n.format.number.duration_mswith attributes{ locale, style } - Stable counter name
i18n.format.cache_hitwith 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.