4.9 KiB
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.
layoutattribute 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
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.
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 freshMeterProvidercan 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 separateread_errors_totalcounter 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.