first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,113 @@
# `MemoryBackend` Observability
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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.