Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
export {
createStdoutAuditInstruments,
type StdoutAuditInstruments,
type StdoutAuditInstrumentsConfig,
} from "./instruments.js";

View File

@@ -0,0 +1,70 @@
# `instruments` — OTel counters for the stdout audit sink
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module owns the two counters the stdout sink emits at runtime,
matching design doc §12.3's metric surface for the audit pipeline.
Operators wire the `MeterProvider` once at process boot; the sink
picks up the configured provider through this factory. When no SDK
is installed, the OTel global resolves to a no-op meter so the sink
runs cleanly in tests without any ceremony.
The factory returns a frozen bundle so the sink does zero meter
lookups on the hot path — both counters are constructed once and the
record's `type` / `outcome` labels are attached at observation time.
## Features
- `api.audit.records_total{sink, type, outcome}` — successful write
counter.
- `api.audit.dropped_total{sink, reason}` — drop counter for
queue-full and writer-failed paths.
- Default meter provider falls back to the OTel global.
- Frozen bundle — references stable for the sink's lifetime.
## Usage: Basic
```ts
import { createStdoutAuditInstruments } from "@suaveplan/api-audit-stdout/instruments";
const instruments = createStdoutAuditInstruments();
instruments.recordsTotal.add(1, {
sink: "stdout",
type: "request.success",
outcome: "success",
});
```
## Usage: Advanced
```ts
import { MeterProvider } from "@opentelemetry/sdk-metrics";
import { createStdoutAuditInstruments } from "@suaveplan/api-audit-stdout/instruments";
const meterProvider = new MeterProvider();
const instruments = createStdoutAuditInstruments({ meterProvider });
// Hand them into createStdoutAuditSink so the sink emits through the
// SDK-installed provider rather than the global no-op.
```
## API Reference
- `createStdoutAuditInstruments(config?)`
- `meterProvider?: MeterProvider` — defaults to
`metrics.getMeterProvider()`.
- Returns `StdoutAuditInstruments` with `meter`, `recordsTotal`,
`droppedTotal`.
## Implementation Notes
Counters are constructed eagerly so the first write does zero
allocations beyond the attribute object. The bundle is frozen to
prevent accidental shadowing. The `sink="stdout"` attribute is
attached at observation time by the sink so the same provider can
serve multiple sink implementations side by side (file, vault, etc.)
without conflict on the metric name.

View File

@@ -0,0 +1,90 @@
import {
AggregationTemporality,
InMemoryMetricExporter,
MeterProvider,
PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { createStdoutAuditInstruments } from "./instruments.js";
async function readCounter(
provider: MeterProvider,
reader: PeriodicExportingMetricReader,
exporter: InMemoryMetricExporter,
name: string
): Promise<number> {
exporter.reset();
await provider.forceFlush();
const records = exporter.getMetrics();
let total = 0;
for (const rm of records) {
for (const scope of rm.scopeMetrics) {
for (const m of scope.metrics) {
if (m.descriptor.name !== name) continue;
for (const dp of m.dataPoints) {
total += dp.value as number;
}
}
}
}
return total;
}
describe("createStdoutAuditInstruments", () => {
it("falls back to the OTel global meter provider", {
timeout: 5000,
}, () => {
const instruments = createStdoutAuditInstruments();
expect(instruments.recordsTotal).toBeDefined();
expect(instruments.droppedTotal).toBeDefined();
expect(instruments.meter).toBeDefined();
});
it("returned bundle is frozen", { timeout: 5000 }, () => {
const instruments = createStdoutAuditInstruments();
expect(Object.isFrozen(instruments)).toBe(true);
});
it("counters increment through a real MeterProvider", {
timeout: 5000,
}, async () => {
const exporter = new InMemoryMetricExporter(
AggregationTemporality.CUMULATIVE
);
const reader = new PeriodicExportingMetricReader({
exporter,
exportIntervalMillis: 60_000,
});
const provider = new MeterProvider({ readers: [reader] });
const instruments = createStdoutAuditInstruments({
meterProvider: provider,
});
instruments.recordsTotal.add(1, {
sink: "stdout",
type: "x",
outcome: "success",
});
instruments.recordsTotal.add(2, {
sink: "stdout",
type: "x",
outcome: "success",
});
instruments.droppedTotal.add(1, { sink: "stdout" });
const records = await readCounter(
provider,
reader,
exporter,
"api.audit.records_total"
);
const dropped = await readCounter(
provider,
reader,
exporter,
"api.audit.dropped_total"
);
expect(records).toBe(3);
expect(dropped).toBe(1);
await provider.shutdown();
});
});

View File

@@ -0,0 +1,47 @@
import type { Counter, Meter, MeterProvider } from "@opentelemetry/api";
import { metrics } from "@opentelemetry/api";
/**
* Config consumed by {@link createStdoutAuditInstruments}. The meter
* provider defaults to the OTel global (which no-ops without an
* installed SDK) so the sink works in test envs without ceremony.
*/
export interface StdoutAuditInstrumentsConfig {
readonly meterProvider?: MeterProvider;
}
/**
* Frozen bundle of the two counters this sink emits.
*
* - `api.audit.records_total{sink="stdout", type, outcome}` — one
* increment per successful write.
* - `api.audit.dropped_total{sink="stdout"}` — one increment per
* dropped record (queue-full or writer-failed). Drops are NEVER
* silent — every drop bumps this counter before the sink returns.
*/
export interface StdoutAuditInstruments {
readonly meter: Meter;
readonly recordsTotal: Counter;
readonly droppedTotal: Counter;
}
const METER_NAME = "@suaveplan/api-audit-stdout";
/**
* Build the sink's instrument registry. Counters are created once at
* construction so the hot-path writes do zero meter lookups.
*/
export function createStdoutAuditInstruments(
config: StdoutAuditInstrumentsConfig = {}
): StdoutAuditInstruments {
const meterProvider = config.meterProvider ?? metrics.getMeterProvider();
const meter = meterProvider.getMeter(METER_NAME);
const recordsTotal = meter.createCounter("api.audit.records_total", {
description:
"Audit records successfully written, by sink + type + outcome",
});
const droppedTotal = meter.createCounter("api.audit.dropped_total", {
description: "Audit records dropped by backpressure or writer failure",
});
return Object.freeze({ meter, recordsTotal, droppedTotal });
}