first commit
This commit is contained in:
128
docs/api-stack/api/api-audit-file/src/sink/sink.md
Normal file
128
docs/api-stack/api/api-audit-file/src/sink/sink.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# `sink`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The public entry point of `@suaveplan/api-audit-file`. The
|
||||
`createFileAuditSink` factory composes the schema, hmac, rotation,
|
||||
retention, instruments, and fs-helpers modules into a single object
|
||||
that implements the `RequestAuditSink` contract from
|
||||
`@suaveplan/api-host/request-pipeline`. The sink writes each accepted
|
||||
record as one JSON line with an HMAC-SHA-256 signature, rolls the file
|
||||
when size OR age limits are reached, and keeps at most `maxFiles`
|
||||
rotated siblings.
|
||||
|
||||
The sink is queue-buffered: callers `await emit()` and resolve as soon
|
||||
as the record has been appended to disk. A bounded queue protects the
|
||||
host from a stalled filesystem: records arriving while the queue is at
|
||||
`maxQueueSize` are dropped with a typed `AUDIT_FILE.QUEUE_FULL` and an
|
||||
`api.audit.dropped_total{sink="file"}` increment. `dispose()` flushes
|
||||
the queue and is idempotent.
|
||||
|
||||
## Features
|
||||
|
||||
- `createFileAuditSink({ path, hmacKey, ... })` — async factory.
|
||||
Returns a sink ready to `emit()`.
|
||||
- Schema validation at the boundary: invalid records throw
|
||||
`AUDIT_FILE.INVALID_RECORD` and increment `dropped_total`.
|
||||
- `strict-sync` (default) and `append` modes for the durability /
|
||||
throughput trade-off.
|
||||
- Boot-time `mkdir -p` of the parent directory + `W_OK` probe;
|
||||
unwriteable targets throw `AUDIT_FILE.PATH_NOT_WRITEABLE` from the
|
||||
factory itself so misconfiguration is visible at host startup.
|
||||
- Size + time rotation; `maxFiles` retention; per-eviction OTel counter.
|
||||
- Idempotent `dispose()`; observable `queueDepth` getter for probes.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createFileAuditSink } from "@suaveplan/api-audit-file/sink";
|
||||
|
||||
const sink = await createFileAuditSink({
|
||||
path: "/var/log/suaveplan/audit.log",
|
||||
hmacKey: new TextEncoder().encode(process.env.AUDIT_HMAC_KEY ?? ""),
|
||||
});
|
||||
|
||||
await sink.emit({
|
||||
at: new Date().toISOString(),
|
||||
requestId: "r-1",
|
||||
traceId: null,
|
||||
route: "/admin/users",
|
||||
plugin: "core-admin",
|
||||
method: "POST",
|
||||
principal: { id: "user-1", scopes: ["admin"] },
|
||||
tenantId: "tenant-a",
|
||||
status: 200,
|
||||
outcome: "success",
|
||||
});
|
||||
|
||||
await sink.dispose();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Run with a custom rotation policy, `append` mode, real `MeterProvider`,
|
||||
and an injected `FakeClock` for deterministic tests:
|
||||
|
||||
```ts
|
||||
import { FakeClock } from "@suaveplan/testing";
|
||||
import {
|
||||
MeterProvider,
|
||||
InMemoryMetricExporter,
|
||||
PeriodicExportingMetricReader,
|
||||
} from "@opentelemetry/sdk-metrics";
|
||||
import { createFileAuditSink } from "@suaveplan/api-audit-file/sink";
|
||||
import { createAuditFileInstruments } from "@suaveplan/api-audit-file/instruments";
|
||||
|
||||
const clock = new FakeClock(0);
|
||||
const exporter = new InMemoryMetricExporter(0);
|
||||
const reader = new PeriodicExportingMetricReader({ exporter });
|
||||
const meterProvider = new MeterProvider({ readers: [reader] });
|
||||
|
||||
const sink = await createFileAuditSink({
|
||||
path: "/tmp/audit.log",
|
||||
hmacKey: new TextEncoder().encode("secret"),
|
||||
clock: clock.read,
|
||||
rotation: { maxSizeBytes: 1024, maxAgeMs: 60_000, maxFiles: 5 },
|
||||
appendMode: "append",
|
||||
maxQueueSize: 256,
|
||||
instruments: createAuditFileInstruments({ meterProvider }),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface CreateFileAuditSinkOptions`
|
||||
- `path: string` (required)
|
||||
- `hmacKey: Uint8Array` (required, non-empty)
|
||||
- `clock?: Clock` — default `() => Date.now()`
|
||||
- `rotation?: Partial<RotationPolicy>`
|
||||
- `appendMode?: "strict-sync" | "append"` — default `"strict-sync"`
|
||||
- `maxQueueSize?: number` — default 1024
|
||||
- `fs?: FsAdapter` — default `nodeFsAdapter`
|
||||
- `logger?: LoggerLike` — default no-op
|
||||
- `instruments?: AuditFileInstruments` — default global meter
|
||||
- `interface FileAuditSink`
|
||||
- `emit(record): Promise<void>`
|
||||
- `dispose(): Promise<void>`
|
||||
- `readonly queueDepth: number`
|
||||
- `createFileAuditSink(options): Promise<FileAuditSink>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The drain loop is mutex-style: a single in-flight drain promise covers
|
||||
every concurrent `emit()` call so writes are serialised against the
|
||||
file handle even when callers fan out. Backpressure is signalled
|
||||
synchronously — `emit()` throws `QUEUE_FULL` before any await when the
|
||||
bounded queue is at capacity, so the host can apply load shedding
|
||||
without observing a write-latency spike. Rotation runs in the same
|
||||
drain frame as the triggering write so the rename always happens
|
||||
before the segment grows past the cap by one extra line. Retention
|
||||
runs after rotation; a retention failure is logged but does not
|
||||
propagate — the sink stays available even if `unlink` is transiently
|
||||
denied. The on-disk format is intentionally trivial (`{ record, hmac }`
|
||||
per line) so an operator with `jq` and `openssl dgst -sha256 -hmac` can
|
||||
verify integrity without any Suaveplan tooling on hand.
|
||||
Reference in New Issue
Block a user