Files
skills/docs/observability/monitor/logger/src/sink/json-sink.md
2026-05-29 08:20:56 +01:00

3.4 KiB

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

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

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.