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

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.
  • 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

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 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.