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,80 @@
# json-sink
## Purpose
`JsonSink` is the machine-readable structured output sink for `@suaveplan/logger`. It serialises each `LogRecord` to a single JSON line and passes it to a configurable `writer` callback — defaulting to `process.stdout.write` — producing newline-delimited JSON (NDJSON) that is directly ingestible by log aggregators such as Fluentd, Logstash, Loki, or Datadog.
The design is deliberately thin: one serialisation step, one write call, no buffering, no formatting decisions. Buffering is the responsibility of `BufferedSink`; formatting decisions belong to the caller. `JsonSink` is the composable primitive that sits at the bottom of a sink stack.
## Features
- Serialises `LogRecord` to a compact JSON line via `JSON.stringify`
- Pluggable `writer` callback — defaults to `process.stdout.write` with a trailing newline
- Throws a structured `LoggerError(LOGGER.SERIALIZE)` if serialisation fails (e.g., circular references in `ctx`)
- Synchronous `write` — no buffering, no internal state; each call is independent
- Trivially composable with `BufferedSink` for async, bounded queuing
- No external runtime dependencies beyond the `LogRecord` type and `LoggerError`
## Usage: Basic Example
```ts
import { JsonSink } from "@suaveplan/logger/sink";
const sink = new JsonSink();
// Writes: {"ts":"2024-01-01T00:00:00.000Z","level":"info","msg":"hello","ctx":{}}\n
sink.write({
ts: new Date().toISOString(),
level: "info",
msg: "hello",
ctx: {},
});
```
## Usage: Advanced Example
```ts
import { JsonSink } from "@suaveplan/logger/sink";
import { BufferedSink } from "@suaveplan/logger/sink";
import { createWriteStream } from "node:fs";
// Write NDJSON to a rotating log file
const stream = createWriteStream("/var/log/app/app.ndjson", { flags: "a" });
const json = new JsonSink({
writer: (line) => stream.write(line + "\n"),
});
// Wrap in a buffer to absorb bursts
const buffered = new BufferedSink({ sink: json, maxQueueSize: 2000 });
// Forward records from the logger to the buffered sink
logger.addSink(buffered);
// Drain and close on process exit
process.on("SIGTERM", async () => {
await buffered.close();
stream.end();
});
```
## API Reference
### `class JsonSink implements Sink`
**Constructor**: `new JsonSink(options?: JsonSinkOptions)`
| Option | Type | Default | Description |
|---|---|---|---|
| `writer` | `(line: string) => void` | `process.stdout.write(line + "\n")` | Receives each serialised JSON line |
**Methods**
- `write(record: LogRecord): void` — Serialise the record and invoke the writer callback. Throws `LoggerError` with code `LOGGER.SERIALIZE` if `JSON.stringify` throws (e.g., due to a circular reference inside `ctx`).
## Implementation Notes
The default `writer` uses `process.stdout.write` rather than `console.log` to avoid the extra newline that `console.log` appends to its own output and to prevent interference with any `console` mocking installed by test frameworks.
`JsonSink` does not implement `flush` or `close` — it has no internal buffer to drain. When wrapped in `BufferedSink`, those lifecycle methods are handled by the buffer layer. This clean separation of concerns keeps `JsonSink` trivially testable with a synchronous `writer` stub.
Because `JSON.stringify` can throw on circular structures, `JsonSink` wraps the call in a try/catch and re-throws as a typed `LoggerError` so callers can distinguish serialisation failures from other sink errors without inspecting error message strings.