first commit
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
# `StorageBackend` Observability
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## 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.
|
||||
Reference in New Issue
Block a user