# 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.