# `MemoryBackend` Observability _(no submodules)_ ## Purpose This module owns the OpenTelemetry instrumentation for `@suaveplan/i18n-backend-memory`. It ships per-call metric emitters (`recordRead`, `recordWrite`) that the backend invokes around every `read` / `save` operation, plus a small in-memory state ring so spec- conformance tests can assert exact attribute payloads without standing up a full SDK + InMemoryMetricExporter pipeline. Every instrument is built lazily and is a no-op when no MeterProvider is registered — consumers pay zero overhead unless they wire up the SDK. The metric naming follows the per-backend prefix convention agreed in the Wave 5 design: `i18n.backend.memory.*`. Sibling backend packages (`i18n-backend-http`, `i18n-backend-storage`, etc.) emit under their own prefixes so consumers can group dashboard panels per backend without shared-namespace collisions. ## Features - Five instruments — two histograms, three counters — covering both read and write paths. - In-memory state ring for test assertions (no exporter required). - Lazy meter construction; idempotent `getMemoryBackendMetrics()`. - Test-only `resetMemoryBackendMetricsCache()` so suites can rebuild the meter under a fresh provider. ## Usage: Basic Example ```ts import { getMemoryBackendMetricsState, resetMemoryBackendMetricsState, } from "@suaveplan/i18n-backend-memory/observability"; import { MemoryBackend } from "@suaveplan/i18n-backend-memory"; resetMemoryBackendMetricsState(); const backend = new MemoryBackend({ seed: { en: { common: { hi: "Hi" } } } }); await backend.read("en", "common"); const state = getMemoryBackendMetricsState(); expect(state.readTotal).toEqual([{ attributes: { outcome: "hit" } }]); ``` ## Usage: Advanced Example Mismatched reads emit BOTH an outcome counter AND an `read_errors_total` counter labelled by error code. Tests can assert the full payload to detect regressions in error classification. ```ts import { getMemoryBackendMetricsState, resetMemoryBackendMetricsState, } from "@suaveplan/i18n-backend-memory/observability"; import { MemoryBackend } from "@suaveplan/i18n-backend-memory"; resetMemoryBackendMetricsState(); const backend = new MemoryBackend(); await backend.read("en", "missing").catch(() => undefined); const state = getMemoryBackendMetricsState(); expect(state.readTotal).toEqual([{ attributes: { outcome: "miss" } }]); expect(state.readErrorsTotal).toEqual([ { attributes: { code: "I18N.BACKEND_LOCALE_NOT_AVAILABLE" } }, ]); ``` ## API Reference ### Emitted instruments | Instrument | Kind | Labels | Unit | |---|---|---|---| | `i18n.backend.memory.read_duration_ms` | Histogram | `locale`, `namespace`, `outcome` | ms | | `i18n.backend.memory.read_total` | Counter | `outcome` (hit \| miss) | count | | `i18n.backend.memory.read_errors_total` | Counter | `code` (I18N error code) | count | | `i18n.backend.memory.write_duration_ms` | Histogram | `locale`, `namespace`, `outcome` | ms | | `i18n.backend.memory.write_total` | Counter | `outcome` (ok \| error) | count | ### Functions - `getMemoryBackendMetrics()` — returns cached instruments; builds them on first call. - `recordRead(locale, namespace, outcome, durationMs, errorCode?)` — emit a read sample. - `recordWrite(locale, namespace, outcome, durationMs)` — emit a write sample. - `getMemoryBackendMetricsState()` — frozen-shape snapshot of every emitted sample since last reset. - `resetMemoryBackendMetricsState()` — clear the in-memory state ring (test fixture). - `resetMemoryBackendMetricsCache()` — drop the cached instruments so a fresh `MeterProvider` can be wired (test fixture). ## Implementation Notes - The state ring is intentionally cardinality-bounded by the caller's test cases. It exists for assertion ergonomics, not for production telemetry — production telemetry flows through the registered OTel `MeterProvider`. - Cardinality safety: aggregate counters use only `outcome` (hit / miss / ok / error); per-error-code attribution lives on a separate `read_errors_total` counter so consumers can drop the high-cardinality counter without losing aggregate visibility. - Metric names are stable: every `i18n.backend.memory.*` name is part of the package's public API contract and will not change without a major bump.