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,134 @@
# otel instruments
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`instruments.ts` exposes the safe-by-construction OTel instruments emitted
from every `@suaveplan/vault-sync` production path. The instrument set is
deliberately narrow: only durations, outcome counters, and a queue-depth
gauge — no vault ids, no item ids, no key material, no vector-clock
values. Counters carry only a `status` attribute drawn from a fixed
allowlist (`success | conflict | rejected`, `encrypted | decrypted |
tampered`). The lazy singleton ensures the meter is acquired exactly once
per process; tests override the provider via `setVaultSyncMeterProvider`
to capture instrument calls deterministically.
## Features
- `getVaultSyncInstruments()` — lazy singleton accessor for the meter set
- `setVaultSyncMeterProvider(provider)` — inject test providers
- `resetVaultSyncInstruments()` — clear cache between test suites
- `getVaultSyncTracer()` / `setVaultSyncTracerProvider()` — span tracer access
- `resetVaultSyncTracer()` — clear tracer cache between suites
- Histogram trio for `sync`, `merge`, `patch.apply`, `payload.encrypt`,
`queue.flush` durations
- Observable gauge for queue depth (count only, no item identifiers)
- Outcome counters with bounded-string labels (no PII)
## Usage: Basic Example
```ts
import { getVaultSyncInstruments } from "@suaveplan/vault-sync/otel";
const i = getVaultSyncInstruments();
const start = Date.now();
// ... work ...
i.syncDurationMs.record(Date.now() - start);
i.mergeOutcomeTotal.add(1, { status: "success" });
```
## Usage: Advanced Example
```ts
import {
getVaultSyncInstruments,
resetVaultSyncInstruments,
setVaultSyncMeterProvider,
} from "@suaveplan/vault-sync/otel";
import type { MeterProvider } from "@opentelemetry/api";
interface Capture {
histograms: Map<string, number[]>;
counters: Map<string, Array<[number, Record<string, string> | undefined]>>;
}
function makeProvider(): { provider: MeterProvider; capture: Capture } {
const capture: Capture = {
histograms: new Map(),
counters: new Map(),
};
const meter = {
createHistogram: (name: string) => {
capture.histograms.set(name, []);
return { record: (v: number) => capture.histograms.get(name)?.push(v) };
},
createCounter: (name: string) => {
capture.counters.set(name, []);
return {
add: (v: number, attrs?: Record<string, string>) => {
capture.counters.get(name)?.push([v, attrs]);
},
};
},
createObservableGauge: (name: string) => ({
addCallback: () => {
capture.histograms.set(name, []);
},
}),
};
return {
provider: {
getMeter: () => meter as unknown as ReturnType<MeterProvider["getMeter"]>,
} as unknown as MeterProvider,
capture,
};
}
const { provider, capture } = makeProvider();
setVaultSyncMeterProvider(provider);
const i = getVaultSyncInstruments();
i.syncDurationMs.record(42);
console.log(capture.histograms.get("vault_sync.sync.duration_ms")); // [42]
setVaultSyncMeterProvider(undefined);
resetVaultSyncInstruments();
```
## API Reference
- `getVaultSyncInstruments(): VaultSyncInstruments` — lazily resolves the
meter and caches the instrument bundle. Returns the cached value on
subsequent calls.
- `setVaultSyncMeterProvider(provider: MeterProvider | undefined): void` —
rebuilds the cache against the provided meter; pass `undefined` to fall
back to the global default.
- `resetVaultSyncInstruments(): void` — clears the cached bundle so the
next `get` call rebinds to the (possibly new) global meter.
- `getVaultSyncTracer(): Tracer` — lazy tracer singleton.
- `setVaultSyncTracerProvider(provider): void` — inject test tracer.
- `resetVaultSyncTracer(): void` — clear tracer cache.
Instrument names:
- `vault_sync.sync.duration_ms` (Histogram, ms)
- `vault_sync.merge.duration_ms` (Histogram, ms)
- `vault_sync.patch.apply.duration_ms` (Histogram, ms)
- `vault_sync.payload.encrypt.duration_ms` (Histogram, ms)
- `vault_sync.queue.flush.duration_ms` (Histogram, ms)
- `vault_sync.queue.depth` (ObservableGauge, count)
- `vault_sync.merge.outcome.total` (Counter, status ∈ {success, conflict, rejected})
- `vault_sync.payload.outcome.total` (Counter, status ∈ {encrypted, decrypted, tampered})
Span names emitted by the consuming modules: `sync.pull`, `sync.merge`,
`sync.push`, `queue.flush`, `payload.encrypt`.
## Implementation Notes
The lazy singleton lets the package be imported even when no OTel SDK is
registered — `metrics.getMeter` returns a no-op meter in that case, and
recording through it is a fast inline call. Counters never carry vault or
item identifiers; the only label is `status` drawn from a documented
allowlist. This is enforced by code review — adding a new label requires
an OpenSpec amendment to the observability plan in the package proposal.