# `StorageBackend` Observability _(no submodules)_ ## Purpose This module owns the OpenTelemetry instrumentation for `@suaveplan/i18n-backend-storage`. 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.storage.*`. Sibling backend packages emit under their own prefixes so consumers can group dashboard panels per backend without shared-namespace collisions. Every counter and histogram carries a low-cardinality `layout` attribute (`single-key-per-namespace` | `single-key-per-locale` | `flat-keys`) so production teams can drill into per-layout latency or miss-rate. ## Features - Five instruments — two histograms, three counters — covering both read and write paths. - `layout` attribute on every aggregate so per-layout dashboards work without parsing namespaces. - In-memory state ring for test assertions (no exporter required). - Lazy meter construction; idempotent `getStorageBackendMetrics()`. - Test-only `resetStorageBackendMetricsCache()` so suites can rebuild the meter under a fresh provider. ## Usage: Basic Example ```ts import { getStorageBackendMetricsState, resetStorageBackendMetricsState, } from "@suaveplan/i18n-backend-storage/observability"; import { StorageBackend } from "@suaveplan/i18n-backend-storage"; import { MemoryStorage } from "@suaveplan/storage"; resetStorageBackendMetricsState(); const backend = new StorageBackend({ storage: new MemoryStorage() }); await backend.save("en", "auth", { ok: "Ok" }); await backend.read("en", "auth"); const state = getStorageBackendMetricsState(); expect(state.readTotal).toEqual([ { attributes: { outcome: "hit", layout: "single-key-per-namespace" } }, ]); ``` ## Usage: Advanced Example A read miss emits both an outcome counter AND a `read_errors_total` counter labelled by error code. Both carry the `layout` attribute so per-layout error rates are charted directly. ```ts import { getStorageBackendMetricsState, resetStorageBackendMetricsState, } from "@suaveplan/i18n-backend-storage/observability"; import { StorageBackend } from "@suaveplan/i18n-backend-storage"; import { MemoryStorage } from "@suaveplan/storage"; resetStorageBackendMetricsState(); const backend = new StorageBackend({ storage: new MemoryStorage(), layout: "flat-keys", }); await backend.read("en", "missing").catch(() => undefined); const state = getStorageBackendMetricsState(); expect(state.readErrorsTotal).toEqual([ { attributes: { code: "I18N.BACKEND_LOCALE_NOT_AVAILABLE", layout: "flat-keys", }, }, ]); ``` ## API Reference ### Emitted instruments | Instrument | Kind | Labels | Unit | |---|---|---|---| | `i18n.backend.storage.read_duration_ms` | Histogram | `locale`, `namespace`, `outcome`, `layout` | ms | | `i18n.backend.storage.read_total` | Counter | `outcome` (hit \| miss \| error), `layout` | count | | `i18n.backend.storage.read_errors_total` | Counter | `code` (I18N error code), `layout` | count | | `i18n.backend.storage.write_duration_ms` | Histogram | `locale`, `namespace`, `outcome`, `layout` | ms | | `i18n.backend.storage.write_total` | Counter | `outcome` (ok \| error), `layout` | count | ### Functions - `getStorageBackendMetrics()` — returns cached instruments; builds them on first call. - `recordRead(locale, namespace, layout, outcome, durationMs, errorCode?)` — emit a read sample. - `recordWrite(locale, namespace, layout, outcome, durationMs)` — emit a write sample. - `getStorageBackendMetricsState()` — frozen-shape snapshot of every emitted sample since last reset. - `resetStorageBackendMetricsState()` — clear the in-memory state ring (test fixture). - `resetStorageBackendMetricsCache()` — 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` + `layout` (each three or fewer values); 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.storage.*` name is part of the package's public API contract and will not change without a major bump.