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,186 @@
# @suaveplan/logger - Foundation logging package — structured JSON output, level filtering, redaction, sampling, OTel correlation, and pluggable sinks.
## Summary
Enterprise-grade structured logger for Suaveplan. Provides a full pipeline: level filtering → OTel trace enrichment → sampling → redaction → sinks. Supports multiple output strategies (console, JSON/NDJSON, event bus, storage) with buffering and backpressure.
<!-- begin:auto-docs -->
## Documentation tree
- [abstract](./src/abstract/abstract-logger.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [abstract-logger](./src/abstract/abstract-logger.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [console](./src/console/console-logger.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [console-logger](./src/console/console-logger.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [error-bridge](./src/error-bridge/error-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [error-bridge](./src/error-bridge/error-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [errors](./src/errors/logger-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [logger-error](./src/errors/logger-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [event](./src/event/event-logger.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [event-logger](./src/event/event-logger.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [init](./src/init/init.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [init](./src/init/init.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [otel](./src/otel/otel-context.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [otel-context](./src/otel/otel-context.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [record](./src/record/log-record.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [log-record](./src/record/log-record.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [redaction](./src/redaction/redaction.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [redaction](./src/redaction/redaction.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [sampling](./src/sampling/sampling.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [sampling](./src/sampling/sampling.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [sink](./src/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [buffered-sink](./src/sink/buffered-sink.md) — `BufferedSink` is an in-memory queuing wrapper around any `Sink` implementation. It decouples the hot path of log recor…
- [console-sink](./src/sink/console-sink.md) — `ConsoleSink` is the human-readable terminal output sink for `@suaveplan/logger`. It formats each `LogRecord` into a si…
- [json-sink](./src/sink/json-sink.md) — `JsonSink` is the machine-readable structured output sink for `@suaveplan/logger`. It serialises each `LogRecord` to a…
- [sink](./src/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [storage](./src/storage/storage-logger.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [storage-logger](./src/storage/storage-logger.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [abstract-logger](./src/abstract/abstract-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [console](./src/console/console-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [console-logger](./src/console/console-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-bridge](./src/error-bridge/error-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-bridge](./src/error-bridge/error-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/logger-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [logger-error](./src/errors/logger-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [event](./src/event/event-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [event-logger](./src/event/event-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [init](./src/init/init.md) — <!-- begin:auto-docs --> _(no submodules)_- [init](./src/init/init.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel-context](./src/otel/otel-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [record](./src/record/log-record.md) — <!-- begin:auto-docs --> _(no submodules)_- [log-record](./src/record/log-record.md) — <!-- begin:auto-docs --> _(no submodules)_- [redaction](./src/redaction/redaction.md) — <!-- begin:auto-docs --> _(no submodules)_- [redaction](./src/redaction/redaction.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampling](./src/sampling/sampling.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampling](./src/sampling/sampling.md) — <!-- begin:auto-docs --> _(no submodules)_- [sink](./src/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_- [buffered-sink](./src/sink/buffered-sink.md) — `BufferedSink` is an in-memory queuing wrapper around any `Sink` implementation. It decouples the hot path of log recor…
- [console-sink](./src/sink/console-sink.md) — `ConsoleSink` is the human-readable terminal output sink for `@suaveplan/logger`. It formats each `LogRecord` into a si…
- [json-sink](./src/sink/json-sink.md) — `JsonSink` is the machine-readable structured output sink for `@suaveplan/logger`. It serialises each `LogRecord` to a…
- [sink](./src/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_- [storage](./src/storage/storage-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [storage-logger](./src/storage/storage-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [abstract-logger](./src/abstract/abstract-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [console](./src/console/console-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [console-logger](./src/console/console-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-bridge](./src/error-bridge/error-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-bridge](./src/error-bridge/error-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/logger-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [logger-error](./src/errors/logger-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [event](./src/event/event-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [event-logger](./src/event/event-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [init](./src/init/init.md) — <!-- begin:auto-docs --> _(no submodules)_- [init](./src/init/init.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel-context](./src/otel/otel-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [record](./src/record/log-record.md) — <!-- begin:auto-docs --> _(no submodules)_- [log-record](./src/record/log-record.md) — <!-- begin:auto-docs --> _(no submodules)_- [redaction](./src/redaction/redaction.md) — <!-- begin:auto-docs --> _(no submodules)_- [redaction](./src/redaction/redaction.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampling](./src/sampling/sampling.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampling](./src/sampling/sampling.md) — <!-- begin:auto-docs --> _(no submodules)_- [sink](./src/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_- [buffered-sink](./src/sink/buffered-sink.md) — `BufferedSink` is an in-memory queuing wrapper around any `Sink` implementation. It decouples the hot path of log recor…
- [console-sink](./src/sink/console-sink.md) — `ConsoleSink` is the human-readable terminal output sink for `@suaveplan/logger`. It formats each `LogRecord` into a si…
- [json-sink](./src/sink/json-sink.md) — `JsonSink` is the machine-readable structured output sink for `@suaveplan/logger`. It serialises each `LogRecord` to a…
- [sink](./src/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_- [storage](./src/storage/storage-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [storage-logger](./src/storage/storage-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [abstract-logger](./src/abstract/abstract-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [console](./src/console/console-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [console-logger](./src/console/console-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-bridge](./src/error-bridge/error-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-bridge](./src/error-bridge/error-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/logger-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [logger-error](./src/errors/logger-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [event](./src/event/event-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [event-logger](./src/event/event-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [init](./src/init/init.md) — <!-- begin:auto-docs --> _(no submodules)_- [init](./src/init/init.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel-context](./src/otel/otel-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [record](./src/record/log-record.md) — <!-- begin:auto-docs --> _(no submodules)_- [log-record](./src/record/log-record.md) — <!-- begin:auto-docs --> _(no submodules)_- [redaction](./src/redaction/redaction.md) — <!-- begin:auto-docs --> _(no submodules)_- [redaction](./src/redaction/redaction.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampling](./src/sampling/sampling.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampling](./src/sampling/sampling.md) — <!-- begin:auto-docs --> _(no submodules)_- [sink](./src/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_- [buffered-sink](./src/sink/buffered-sink.md) — `BufferedSink` is an in-memory queuing wrapper around any `Sink` implementation. It decouples the hot path of log recor…
- [console-sink](./src/sink/console-sink.md) — `ConsoleSink` is the human-readable terminal output sink for `@suaveplan/logger`. It formats each `LogRecord` into a si…
- [json-sink](./src/sink/json-sink.md) — `JsonSink` is the machine-readable structured output sink for `@suaveplan/logger`. It serialises each `LogRecord` to a…
- [sink](./src/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_- [storage](./src/storage/storage-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [storage-logger](./src/storage/storage-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [abstract-logger](./src/abstract/abstract-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [console](./src/console/console-logger.md) — <!-- begin:auto-docs --> _(no submodules)_ - [console-logger](./src/console/console-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-bridge](./src/error-bridge/error-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_ - [error-bridge](./src/error-bridge/error-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/logger-error.md) — <!-- begin:auto-docs --> _(no submodules)_ - [logger-error](./src/errors/logger-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [event](./src/event/event-logger.md) — <!-- begin:auto-docs --> _(no submodules)_ - [event-logger](./src/event/event-logger.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_ - [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [init](./src/init/init.md) — <!-- begin:auto-docs --> _(no submodules)_ - [init](./src/init/init.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel-context.md) — <!-- begin:auto-docs --> _(no submodules)_ - [otel-context](./src/otel/otel-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [record](./src/record/log-record.md) — <!-- begin:auto-docs --> _(no submodules)_ - [log-record](./src/record/log-record.md) — <!-- begin:auto-docs --> _(no submodules)_- [redaction](./src/redaction/redaction.md) — <!-- begin:auto-docs --> _(no submodules)_ - [redaction](./src/redaction/redaction.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampling](./src/sampling/sampling.md) — <!-- begin:auto-docs --> _(no submodules)_ - [sampling](./src/sampling/sampling.md) — <!-- begin:auto-docs --> _(no submodules)_- [sink](./src/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_ - [buffered-sink](./src/sink/buffered-sink.md) — `BufferedSink` is an in-memory queuing wrapper around any `Sink` implementation. It decouples the hot path of log recor…
- [console-sink](./src/sink/console-sink.md) — `ConsoleSink` is the human-readable terminal output sink for `@suaveplan/logger`. It formats each `LogRecord` into a si…
- [json-sink](./src/sink/json-sink.md) — `JsonSink` is the machine-readable structured output sink for `@suaveplan/logger`. It serialises each `LogRecord` to a…
- [sink](./src/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_- [storage](./src/storage/storage-logger.md) — <!-- begin:auto-docs --> _(no submodules)_ - [storage-logger](./src/storage/storage-logger.md) — <!-- begin:auto-docs --> _(no submodules)_
## Features
- **Structured logging** — `LogRecord` with level, message, ISO timestamp, context bag, trace IDs
- **Level filtering** — `debug | info | warn | error` with hierarchical cutoff
- **OTel enrichment** — auto-injects `trace_id` / `span_id` from the active OpenTelemetry span
- **Sampling** — `HeadSampler` (deterministic FNV1a), `TailSampler` (buffered), `RuleSampler`, `SampleAll/None`
- **Redaction** — path-based and regex-pattern rules; respects `@suaveplan/error` `isSensitive()` markers
- **Pluggable sinks** — `ConsoleSink`, `JsonSink` (NDJSON), `BufferedSink` (async queue, backpressure), `MultiSink`
- **Concrete loggers** — `ConsoleLogger`, `EventLogger` (event-bus integration), `StorageLogger` (KV persistence)
- **Error bridge** — installs a global `SuaveplanError` interceptor so unhandled errors are logged automatically
- **Factory API** — `createLogger(config)` for declarative setup; `initLogger(config)` for one-call bootstrap
## Installation
```bash
bun add @suaveplan/logger
```
## Quick Start
```ts
import { initLogger } from "@suaveplan/logger/init/init";
import { ConsoleLogger } from "@suaveplan/logger/console/console-logger";
// Option 1 — factory bootstrap
const logger = initLogger({ type: "console", level: "info" });
logger.info("server started", { port: 3000 });
// Option 2 — direct construction
const log = new ConsoleLogger({ level: "debug", colors: true, timestamp: true });
log.debug("connecting", { host: "db.local" });
log.warn("slow query", { ms: 412 });
```
## Modules
| Module | Description |
|--------|-------------|
| [`src/abstract/abstract-logger`](src/abstract/abstract-logger.md) | Base pipeline class — all concrete loggers extend this |
| [`src/record/log-record`](src/record/log-record.md) | `LogRecord` type and Zod validation schemas |
| [`src/errors/logger-error`](src/errors/logger-error.md) | `LoggerError` + `LOGGER_CODES` error registry |
| [`src/otel/otel-context`](src/otel/otel-context.md) | OTel trace/span context extractor |
| [`src/sampling/sampling`](src/sampling/sampling.md) | All sampler implementations and `createSampler` factory |
| [`src/redaction/redaction`](src/redaction/redaction.md) | `RedactionEngine` and `applyRedaction` utility |
| [`src/sink/sink`](src/sink/sink.md) | Sink interface, `MultiSink`, `flushSink`, `closeSink` |
| [`src/sink/console-sink`](src/sink/console-sink.md) | ANSI-colored human-readable console output |
| [`src/sink/json-sink`](src/sink/json-sink.md) | NDJSON structured output to any writer |
| [`src/sink/buffered-sink`](src/sink/buffered-sink.md) | Async queue with configurable overflow policies |
| [`src/console/console-logger`](src/console/console-logger.md) | Ready-to-use console logger |
| [`src/event/event-logger`](src/event/event-logger.md) | Event-bus logger with per-level subscriptions |
| [`src/storage/storage-logger`](src/storage/storage-logger.md) | KV-backed persistent logger with batch flush |
| [`src/error-bridge/error-bridge`](src/error-bridge/error-bridge.md) | Global `SuaveplanError` interceptor |
| [`src/factory/factory`](src/factory/factory.md) | Declarative `createLogger` factory |
| [`src/init/init`](src/init/init.md) | One-call `initLogger` bootstrap with global accessor |
## Usage: Redaction
```ts
import { ConsoleLogger } from "@suaveplan/logger/console/console-logger";
const log = new ConsoleLogger({
level: "info",
redaction: {
rules: [
{ kind: "path", path: "user.password" },
{ kind: "pattern", pattern: /token/i },
],
},
});
log.info("login", { user: { password: "s3cr3t", email: "a@b.com" }, token: "xyz" });
// → password and token fields are replaced with "[REDACTED]"
```
## Usage: Sampling
```ts
import { createLogger } from "@suaveplan/logger/factory/factory";
const logger = createLogger({
type: "console",
level: "debug",
sampling: { kind: "head", options: { rate: 0.1 } }, // keep 10%
});
```
## Usage: Buffered JSON sink
```ts
import { BufferedSink } from "@suaveplan/logger/sink/buffered-sink";
import { JsonSink } from "@suaveplan/logger/sink/json-sink";
const inner = new JsonSink({ writer: (line) => fs.appendFileSync("app.log", line + "\n") });
const sink = new BufferedSink({ sink: inner, maxQueueSize: 1000, overflowPolicy: "drop" });
// ... on shutdown:
await sink.flush();
await sink.close();
```
## Usage: StorageLogger
```ts
import { StorageLogger } from "@suaveplan/logger/storage/storage-logger";
const logger = new StorageLogger(kvStorage, {
level: "info",
autoFlush: true,
flushInterval: 10_000,
maxBatchSize: 100,
});
logger.error("payment failed", { orderId: "ord_123" });
const recent = await logger.getLogs({ limit: 50 });
await logger.clearLogs(new Date(Date.now() - 7 * 86400_000)); // purge >7 days old
```
## Usage: Error bridge
```ts
import { installSuaveplanErrorInterceptor } from "@suaveplan/logger/error-bridge/error-bridge";
import { ConsoleLogger } from "@suaveplan/logger/console/console-logger";
const log = new ConsoleLogger({ level: "warn" });
installSuaveplanErrorInterceptor(log); // all future SuaveplanErrors are logged automatically
```
## API Reference
See each module's co-located `.md` file (linked in the table above) for full parameter and return-type documentation.
## License
Private — Suaveplan internal use only.

View File

@@ -0,0 +1,109 @@
# abstract-logger
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`AbstractLogger` is the backbone of `@suaveplan/logger`. It owns the log pipeline: level filtering → metadata merging → redaction → OTel context attachment → structured record construction → sampling decision → sink dispatch → backward-compatible `writeLog()` hook. Every concrete logger (Console, Event, Storage) extends it and only overrides `writeLog()`. By centralising the pipeline here, every logger automatically inherits enterprise features (redaction, sampling, OTel correlation, metrics, clock injection) without duplicating logic.
The `Logger<TMetadata>` interface provides the stable consumer API. Consumers that only need to accept a logger in function signatures should type against `Logger<TMetadata>` rather than `AbstractLogger<TMetadata>`.
## Features
- Level filtering with runtime `setLevel()` / `getLevel()`
- `enable()` / `disable()` master switch
- Pluggable `sinks: Sink[]` — each `LogRecord` is dispatched to all sinks after pipeline processing
- Declarative redaction via `RedactionEngine` — applied to merged context before record construction
- Pluggable sampling via `Sampler` instance or `SamplingPolicy` discriminated union
- OTel trace/span ID auto-attachment from `@opentelemetry/api` active span (configurable, default on)
- Injectable `clock: Clock` from `@suaveplan/types` for deterministic timestamps in tests
- OTel metrics: `logger.records_total` counter, `logger.record_size_bytes` histogram, `logger.dropped_total` counter — all no-op when no OTel exporter is registered
- `getMetrics()` returns a snapshot including `droppedBySampling`, `droppedByBackpressure`, and `droppedBySinkError` counts. The three counters are mutually exclusive: a sink throw or rejection increments `droppedBySinkError` (and is reported to a fallback sink as `LoggerError(LOGGER.SINK)`), NOT `droppedByBackpressure`. Tests asserting on dropped-record counts must use the counter that matches the failure mode.
- Per-sink failure isolation: a thrown sink does NOT halt dispatch to remaining sinks. Failures are emitted to a configurable `fallbackSink` (default: bare `console.error`). This is what makes multi-sink logging safe to compose — one bad sink cannot take the others down.
## Usage: Basic Example
```ts
import { AbstractLogger } from "@suaveplan/logger/abstract";
import type { LogEntry } from "@suaveplan/logger/record";
class MyLogger extends AbstractLogger {
protected writeLog(entry: LogEntry): void {
// custom output logic
process.stdout.write(`[${entry.level}] ${entry.message}\n`);
}
}
const logger = new MyLogger({ level: "debug" });
logger.info("App started");
```
## Usage: Advanced Example
```ts
import { AbstractLogger } from "@suaveplan/logger/abstract";
import { JsonSink } from "@suaveplan/logger/sink";
import { HeadSampler } from "@suaveplan/logger/sampling";
class ProductionLogger extends AbstractLogger {
protected writeLog(_entry: LogEntry): void {
// no-op: all output goes through sinks
}
}
const logger = new ProductionLogger({
level: "info",
sinks: [new JsonSink()],
redaction: { rules: [{ kind: "path", path: "user.token" }] },
sampling: new HeadSampler({ rate: 0.1 }),
otel: true,
});
logger.info("Payment processed", { orderId: "o-1", user: { token: "abc" } });
// Sink sees: { ..., ctx: { orderId: "o-1", user: { token: "[REDACTED]" } } }
```
## API Reference
### `interface Logger<TMetadata>`
```ts
interface Logger<TMetadata> {
debug(message, metadata?, tags?): void;
info(message, metadata?, tags?): void;
warn(message, metadata?, tags?): void;
error(message, metadata?, tags?): void;
setLevel(level): void;
getLevel(): LogLevel;
enable(): void;
disable(): void;
isEnabled(): boolean;
getMetrics(): LoggerMetrics;
resetMetrics(): void;
}
```
### `abstract class AbstractLogger<TMetadata> implements Logger<TMetadata>`
**Constructor**: `new ConcreteLogger(config?: AbstractLoggerConfig<TMetadata>)`
| Config field | Type | Default | Description |
|-------------|------|---------|-------------|
| `level` | `LogLevel` | `"info"` | Minimum emit level |
| `enabled` | `boolean` | `true` | Master on/off |
| `metadata` | `TMetadata` | `{}` | Default context merged into every record |
| `tags` | `string[]` | `[]` | Global tags merged into every record |
| `sinks` | `Sink[]` | `[]` | Pluggable output targets |
| `redaction` | `RedactionOptions` | `{}` | Field-path and pattern redaction rules |
| `sampling` | `SamplingPolicy \| Sampler` | keep-all | Sampling policy |
| `clock` | `Clock` | `Date` | Injectable clock for timestamps |
| `otel` | `boolean` | `true` | Auto-attach OTel trace context |
#### `protected abstract writeLog(entry: LogEntry<TMetadata>): void`
Called after sink dispatch for backward-compatible concrete logger implementations.
## Implementation Notes
Tags are stored in the log record under `ctx.__tags` (a `string[]`) rather than as a top-level field because the `LogRecord` schema has only `ctx` for context data. Sinks can extract `__tags` for display; the `ConsoleSink` does this. The `writeLog()` hook receives a `LogEntry` (the legacy shape) so that subclasses written for the old API still function without changes.

View File

@@ -0,0 +1,70 @@
# console-logger
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ConsoleLogger` extends `AbstractLogger` to provide human-readable, ANSI-coloured terminal output — the default logger for development and local debugging. It wires a `ConsoleSink` as the first element of its sink chain, so the full enterprise pipeline (level filtering, redaction, sampling, OTel correlation, metrics) runs before any output reaches the terminal. Configuration mirrors the existing v1 API exactly, preserving backward compatibility.
## Features
- ANSI colour per severity level (configurable at construction and runtime)
- ISO timestamp prefix (configurable)
- Optional tag rendering as `#tag` tokens
- Optional metadata rendering as compact JSON suffix
- All `AbstractLogger` features: redaction, sampling, OTel, clock injection, `getMetrics()`
## Usage: Basic Example
```ts
import { ConsoleLogger } from "@suaveplan/logger/console";
const log = new ConsoleLogger({ level: "info" });
log.info("Server started", { port: 3000 });
```
## Usage: Advanced Example
```ts
import { ConsoleLogger } from "@suaveplan/logger/console";
const log = new ConsoleLogger({
level: "debug",
colors: true,
timestamp: true,
includeMetadata: true,
tags: ["api"],
redaction: { rules: [{ kind: "pattern", pattern: /token|secret/i }] },
});
log.info("User authenticated", { userId: "u-1", token: "abc" }, ["auth"]);
// [2024-…] [INFO] [#api #auth] User authenticated
// { "userId": "u-1", "token": "[REDACTED]" }
```
## API Reference
### `class ConsoleLogger<TMetadata> extends AbstractLogger<TMetadata>`
**Constructor**: `new ConsoleLogger(config?: ConsoleLoggerConfig<TMetadata>)`
Extends `AbstractLoggerConfig` with:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `colors` | `boolean` | `true` | ANSI colour codes |
| `timestamp` | `boolean` | `true` | ISO timestamp prefix |
| `includeMetadata` | `boolean` | `false` | Compact JSON metadata suffix |
| `includeTags` | `boolean` | `true` | `#tag` tokens inline |
#### Runtime mutation methods
- `setColors(enabled: boolean)`
- `setTimestamp(enabled: boolean)`
- `setIncludeMetadata(enabled: boolean)`
- `setIncludeTags(enabled: boolean)`
## Implementation Notes
The `writeLog()` hook is a no-op — all output is delegated to `ConsoleSink` registered in the `sinks` array. This ensures records pass through redaction and sampling (handled by `AbstractLogger`) before any console call. Runtime mutation methods write directly to the `ConsoleSink`'s protected fields.

View File

@@ -0,0 +1,72 @@
# error-bridge
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`error-bridge` connects `@suaveplan/error`'s `SuaveplanError` to the logger, providing a single function (`logSuaveplanError`) that routes a structured error to whichever logger was installed via `installSuaveplanErrorInterceptor`. The install function is idempotent — calling it multiple times is safe and returns the same uninstall handle, preventing double-registration in environments where multiple modules bootstrap logging.
The implementation differs from the main-branch version: rather than hooking into a global interceptor registry (which does not yet exist in `@suaveplan/error`), error routing is explicit — callers invoke `logSuaveplanError(err)` in their catch blocks. `installSuaveplanErrorInterceptor` still establishes the logger reference and provides the idempotency/cleanup contract.
## Features
- `installSuaveplanErrorInterceptor(logger?)` — stores a logger; defaults to a `ConsoleLogger`; idempotent
- `logSuaveplanError(err)` — routes any `SuaveplanError` to the active logger at `"error"` level with code, context, stack, and serialised JSON payload
- `SEVERITY_TO_LEVEL` — mapping from `SuaveplanError` severity strings to `LogLevel`
- `__resetDefaultLoggerForTests()` — test-only helper to clear all state between test cases
## Usage: Basic Example
```ts
import { installSuaveplanErrorInterceptor, logSuaveplanError } from "@suaveplan/logger/error-bridge";
import { ConsoleLogger } from "@suaveplan/logger/console";
const logger = new ConsoleLogger({ level: "info" });
const uninstall = installSuaveplanErrorInterceptor(logger);
try {
await doRiskyThing();
} catch (err) {
if (err instanceof SuaveplanError) logSuaveplanError(err);
}
```
## Usage: Advanced Example
```ts
import { installSuaveplanErrorInterceptor, __resetDefaultLoggerForTests } from "@suaveplan/logger/error-bridge";
// In bootstrap:
const uninstall = installSuaveplanErrorInterceptor(); // uses default ConsoleLogger
// In tests (afterEach):
afterEach(() => __resetDefaultLoggerForTests());
// Multiple modules can safely call install — only the first call registers:
installSuaveplanErrorInterceptor(); // returns same uninstall as above
```
## API Reference
### `installSuaveplanErrorInterceptor(logger?: Logger): UninstallErrorInterceptor`
Stores `logger` (or a default `ConsoleLogger`) as the active error sink. Returns the uninstall function. Re-calling returns the existing uninstall without side effects.
### `logSuaveplanError(err: SuaveplanError): void`
Routes `err` to the active logger at `"error"` level. Context includes `code`, `context`, `stack`, and the JSON-serialised wire payload from `toJSON(err)`.
### `SEVERITY_TO_LEVEL: SeverityLevelMap`
```ts
{ fatal: "error", error: "error", warning: "warn", info: "info" }
```
### `__resetDefaultLoggerForTests(): void`
Clears all module-level state. Call in `afterEach` when testing code that uses the error bridge.
## Implementation Notes
The active logger is stored in module-level state (a singleton pattern). This is intentional — bootstrap code typically runs once, and the idempotency guard prevents multiple callers from interfering. `__resetDefaultLoggerForTests` exists as an escape hatch for test environments where module state must be reset between cases.

View File

@@ -0,0 +1,81 @@
# logger-error
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`LoggerError` is the structured error class used by every failure path in `@suaveplan/logger`. It extends `SuaveplanError` from `@suaveplan/error`, ensuring all logger errors carry a stable, machine-readable code, an optional cause chain, and a typed context payload. Consumer code can discriminate on `err.code` without string-matching the message.
`LOGGER_CODES` is a frozen constant record produced by `defineCodes` that maps each failure reason to its fully-qualified `"LOGGER.<REASON>"` string. Using the constant instead of raw strings ensures a typo is caught at compile time.
## Features
- Extends `SuaveplanError<LoggerErrorCode>` — inherits `code`, `cause`, `context`, `toJSON()`, `toSpanEvent()`, and severity fields
- Four stable codes: `LOGGER.SINK`, `LOGGER.SERIALIZE`, `LOGGER.CONFIG`, `LOGGER.FLUSH`
- Frozen `LOGGER_CODES` constant for type-safe code references
## Usage: Basic Example
```ts
import { LOGGER_CODES, LoggerError } from "@suaveplan/logger/errors";
throw new LoggerError(LOGGER_CODES.SINK, "Sink write failed", {
cause: originalError,
});
```
## Usage: Advanced Example
```ts
import { LOGGER_CODES, LoggerError } from "@suaveplan/logger/errors";
try {
await sink.write(record);
} catch (err) {
throw new LoggerError(LOGGER_CODES.SINK, "sink.write threw", {
cause: err instanceof Error ? err : undefined,
context: { sinkType: "JsonSink", recordLevel: record.level },
});
}
// Consumer discrimination:
if (caughtErr instanceof LoggerError && caughtErr.code === LOGGER_CODES.FLUSH) {
// handle flush-aborted case specifically
}
```
## API Reference
### `LOGGER_CODES`
```ts
const LOGGER_CODES: {
readonly SINK: "LOGGER.SINK";
readonly SERIALIZE: "LOGGER.SERIALIZE";
readonly CONFIG: "LOGGER.CONFIG";
readonly FLUSH: "LOGGER.FLUSH";
}
```
Frozen record of all stable error codes for `@suaveplan/logger`.
### `class LoggerError extends SuaveplanError<LoggerErrorCode>`
**Constructor**: `new LoggerError(code, messageKey, options?)`
| Parameter | Type | Description |
|-----------|------|-------------|
| `code` | `LoggerErrorCode` | Stable machine-readable code |
| `messageKey` | `string` | Human-readable message or i18n key |
| `options` | `SuaveplanErrorOptions?` | Optional `{ cause, context, severity }` |
Inherits all `SuaveplanError` methods: `toJSON()`, `toSpanEvent()`, `walkCauses()`.
### `type LoggerErrorCode`
Union of all four stable code strings: `"LOGGER.SINK" | "LOGGER.SERIALIZE" | "LOGGER.CONFIG" | "LOGGER.FLUSH"`.
## Implementation Notes
`LoggerError` delegates everything to `SuaveplanError` — the constructor body is intentionally minimal. The four codes cover the complete surface: sink I/O failures (`SINK`), JSON serialisation failures (`SERIALIZE`), invalid configuration detected at construction time (`CONFIG`), and flush cancellation via `AbortSignal` (`FLUSH`). Adding new codes requires updating the `LOGGER_CODES` call and the `LoggerErrorCode` union — both are in this file to keep changes atomic.

View File

@@ -0,0 +1,89 @@
# event-logger
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`EventLogger` routes each log entry through an `EventBusLike<LoggerEvents>` emitter after the standard `AbstractLogger` pipeline (level filtering, redaction, sampling, OTel). It exists for environments where other parts of an application need to react to log events — for example, a UI component that surfaces recent errors, a test harness that asserts on emitted entries, or an aggregator that combines logs from multiple services. The emitter is injected so any conformant `EventBusLike` works (including `@suaveplan/events`' `EventBus`).
## Features
- Emits `"log"` for every accepted record
- Emits `"log:debug"`, `"log:info"`, `"log:warn"`, `"log:error"` per level
- Emits `"metrics:updated"` after every log with the current `LoggerMetrics` snapshot
- `on*/off*` helper methods for typed subscription/unsubscription
- Proper unsubscription via the `() => void` handle returned by `EventBusLike.on`
- Compatible with any `EventBusLike<LoggerEvents<TMetadata>>` — no hard dep on `@suaveplan/events`
## Usage: Basic Example
```ts
import { EventBus } from "@suaveplan/events";
import { EventLogger } from "@suaveplan/logger/event";
const bus = new EventBus();
const logger = new EventLogger(bus, { level: "info" });
logger.onError((entry) => {
alerting.report(entry.message, entry.metadata);
});
logger.error("Payment gateway unreachable");
```
## Usage: Advanced Example
```ts
import { EventBus } from "@suaveplan/events";
import { EventLogger } from "@suaveplan/logger/event";
const bus = new EventBus();
const logger = new EventLogger(bus, { level: "debug" });
// Subscribe
const handleInfo = (entry) => console.log("[INFO]", entry.message);
logger.onInfo(handleInfo);
// Log
logger.info("Started");
// Unsubscribe
logger.offInfo(handleInfo);
```
## API Reference
### `class EventLogger<TMetadata> extends AbstractLogger<TMetadata>`
**Constructor**: `new EventLogger(emitter, config?)`
| Parameter | Type | Description |
|-----------|------|-------------|
| `emitter` | `LoggerEventBus<TMetadata>` | Any `EventBusLike` conformant bus |
| `config` | `EventLoggerConfig<TMetadata>?` | Inherits all `AbstractLoggerConfig` options |
#### Subscription helpers (all return `this`)
`onLog` / `offLog`, `onDebug` / `offDebug`, `onInfo` / `offInfo`, `onWarn` / `offWarn`, `onError` / `offError`, `onMetricsUpdate` / `offMetricsUpdate`
#### `getEmitter(): LoggerEventBus<TMetadata>`
Returns the injected bus.
### `interface LoggerEvents<TMetadata>`
```ts
{
"log": LogEntry<TMetadata>;
"log:debug": LogEntry<TMetadata>;
"log:info": LogEntry<TMetadata>;
"log:warn": LogEntry<TMetadata>;
"log:error": LogEntry<TMetadata>;
"metrics:updated": LoggerMetrics;
}
```
## Implementation Notes
Unsubscription is implemented by storing the `() => void` cleanup function returned by `emitter.on()` in a `WeakMap<listener, cleanup>`. Calling `off*` looks up and invokes the cleanup. The `WeakMap` avoids memory leaks when the listener function is garbage collected without explicit unsubscription.

View File

@@ -0,0 +1,56 @@
# factory
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createLogger` is an overloaded factory that returns the concrete logger class corresponding to the `type` discriminant in the configuration object. It eliminates the need for consumers to import each concrete class separately and provides a type-narrowing API — the return type is `ConsoleLogger`, `StorageLogger`, or `EventLogger` depending on which overload matches, so consumers get full intellisense on type-specific methods.
## Features
- Overloaded TypeScript signatures for `"console"`, `"storage"`, and `"event"` types
- Returns the precise concrete class (`ConsoleLogger | StorageLogger | EventLogger`)
- Exhaustive switch with TypeScript `never` guard — compile-time protection against unhandled types
- Forwards all config options to the underlying constructor unchanged
## Usage: Basic Example
```ts
import { createLogger } from "@suaveplan/logger/factory";
const log = createLogger({ type: "console", level: "info", colors: false });
log.info("Server ready");
```
## Usage: Advanced Example
```ts
import { createLogger } from "@suaveplan/logger/factory";
import type { LoggerFactoryConfig } from "@suaveplan/logger/factory";
// Dynamic selection at runtime
const config: LoggerFactoryConfig = process.env.LOG_TARGET === "json"
? { type: "console", colors: false }
: { type: "console", colors: true };
const log = createLogger(config);
```
## API Reference
### `createLogger(config): ConsoleLogger | StorageLogger | EventLogger`
Three overloads — see `factory.types.ts` for `ConsoleLoggerFactoryConfig`, `StorageLoggerFactoryConfig`, and `EventLoggerFactoryConfig`.
### `type LoggerFactoryConfig<TMetadata>`
Discriminated union with `type: "console" | "storage" | "event"`.
## Implementation Notes
The `_exhaustive: never` pattern on the default branch ensures TypeScript catches missing cases at compile time without requiring runtime `throw new Error("unreachable")` to satisfy coverage — though the throw is retained as a safety net for JavaScript consumers.
### Factory Shape
`createLogger` is the declarative entry-point for assembling a concrete logger from a `LoggerConfig` bundle: level, sinks, sampler, redactor, OTel trace enrichment, and global-error bridge. Each option has a sensible default so the zero-config call `createLogger({ name: "app" })` yields a console-sink logger at `info` level. Returned loggers satisfy the `LoggerLike` contract from `@suaveplan/types` for safe downstream injection.

View File

@@ -0,0 +1,65 @@
# init
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`initLogger` is the single bootstrap entry point for `@suaveplan/logger`. A caller invokes it once at application startup to get a fully configured logger with the `@suaveplan/error` interceptor bridge already wired. The function is idempotent-safe — the underlying `installSuaveplanErrorInterceptor` guards against double-registration — so calling `initLogger` from multiple modules during startup is safe.
## Features
- Accepts an optional pre-built `logger` to wire, otherwise creates a `ConsoleLogger`
- Accepts `consoleConfig` to configure the default `ConsoleLogger` without constructing it manually
- Wires `installSuaveplanErrorInterceptor` and returns the `uninstall` handle alongside the logger
- Returns a typed `InitLoggerResult` so callers can cleanly tear down on shutdown
## Usage: Basic Example
```ts
import { initLogger } from "@suaveplan/logger/init";
const { logger, uninstall } = initLogger();
logger.info("Application started");
// On shutdown:
process.on("SIGTERM", () => uninstall());
```
## Usage: Advanced Example
```ts
import { initLogger } from "@suaveplan/logger/init";
import { JsonSink } from "@suaveplan/logger/sink";
const jsonSink = new JsonSink({ writer: writeToLogAggregator });
const { logger, uninstall } = initLogger({
consoleConfig: {
level: "info",
sinks: [jsonSink],
redaction: { rules: [{ kind: "pattern", pattern: /token|secret/i }] },
},
});
logger.info("Server ready", { port: 8080 });
```
## API Reference
### `initLogger<TMetadata>(options?): InitLoggerResult`
| Option | Type | Description |
|--------|------|-------------|
| `logger` | `Logger?` | Pre-built logger to wire into the error bridge |
| `consoleConfig` | `ConsoleLoggerConfig?` | Config for the default `ConsoleLogger` when `logger` is absent |
Returns `{ logger: Logger, uninstall: () => void }`.
## Implementation Notes
`initLogger` is intentionally thin — it delegates to `ConsoleLogger` and `installSuaveplanErrorInterceptor`. Keeping it as a thin coordinator makes it testable: callers that need a custom logger or specific sink configuration pass them in, rather than configuring a global singleton.
### Startup Hook
`initLogger` is the process-startup counterpart to `createLogger` — it installs a global error bridge that catches uncaught exceptions, unhandled promise rejections, and `SIGTERM` / `SIGINT` signals, then flushes any buffered sinks before exiting. Calling `initLogger` more than once is a no-op: the module tracks its own install state on a `Symbol.for` marker.

View File

@@ -0,0 +1,62 @@
# otel-context
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`otel-context` extracts the active OpenTelemetry trace context (`trace_id` and `span_id`) from the current OTel context and returns them as lowercase hex strings. This allows `AbstractLogger` to automatically annotate every log record with the correlation IDs of the span in which the log call was made — without requiring callers to pass trace context explicitly. When no span is active or the OTel API is configured with a no-op provider, the function returns an empty object without throwing.
## Features
- Reads `trace_id` and `span_id` from the active span via `@opentelemetry/api`
- Returns `{}` when no span is active, the span context has all-zero IDs (no-op provider), or the OTel API throws unexpectedly
- Never throws — defensive `try/catch` wraps the entire extraction
- Zero side effects: pure function, no caching, safe to call on every log line
## Usage: Basic Example
```ts
import { getOtelTraceContext } from "@suaveplan/logger/otel";
const traceCtx = getOtelTraceContext();
// When inside an active span: { trace_id: "abc123...", span_id: "def456..." }
// When no span is active: {}
```
## Usage: Advanced Example
```ts
import { buildRecord } from "@suaveplan/logger/record";
import { getOtelTraceContext } from "@suaveplan/logger/otel";
// Used internally by AbstractLogger on every log call:
const traceCtx = getOtelTraceContext();
const record = buildRecord(level, msg, ctx, ts, traceCtx, errorPayload);
// record.trace_id and record.span_id are populated when a span is active
```
## API Reference
### `getOtelTraceContext(): OtelTraceContext`
Returns an object with optional `trace_id` and `span_id` string fields.
Returns `{}` when:
- No span is active in the current context
- The active span has an all-zero trace ID (indicates a no-op OTel SDK)
- `@opentelemetry/api` throws for any reason
### `interface OtelTraceContext`
```ts
interface OtelTraceContext {
trace_id?: string;
span_id?: string;
}
```
## Implementation Notes
The zero-ID guard (`00000000000000000000000000000000`) exists because the OTel no-op provider returns a valid-looking `SpanContext` with all-zero IDs rather than returning `undefined`. Emitting those zero IDs would mislead log consumers into thinking a real trace exists. The `try/catch` guard exists because OTel API implementations can throw in unusual environments (e.g., incompatible versions, patched globals in test environments).

View File

@@ -0,0 +1,88 @@
# log-record
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`log-record` defines the canonical structured format (`LogRecord`) that every log emission produces before being handed to sinks. By standardising on this shape — `ts`, `level`, `msg`, `ctx`, `trace_id`, `span_id`, `error` — every downstream component (redaction, sampling, sinks, OTel) operates on a single well-typed contract rather than ad-hoc strings. The Zod schema in `log-record.schemas.ts` provides runtime validation and serves as the serialisation contract.
## Features
- `LogRecord` structured type with ISO timestamp, level, message, merged context, optional OTel IDs, optional error payload
- `LOG_LEVELS` numeric ordering map for level comparison
- `isLogLevel(value)` type guard
- `shouldLog(current, candidate)` — returns `true` when `candidate >= current`
- `buildRecord(...)` — constructs a `LogRecord`, omitting undefined optional fields
- Zod schema `LogRecordSchema` with `z.datetime()` validation for `ts`
## Usage: Basic Example
```ts
import { buildRecord } from "@suaveplan/logger/record";
const record = buildRecord(
"info",
"User signed in",
{ userId: "u-123" },
new Date().toISOString(),
);
// { ts: "...", level: "info", msg: "User signed in", ctx: { userId: "u-123" } }
```
## Usage: Advanced Example
```ts
import { buildRecord, shouldLog } from "@suaveplan/logger/record";
import { LogRecordSchema } from "@suaveplan/logger/record.schemas";
// Only build the record if the level passes the filter
if (shouldLog("warn", "error")) {
const record = buildRecord(
"error",
"Payment failed",
{ orderId: "o-42" },
clock.now().toISOString(),
{ trace_id: "abc123ef", span_id: "def456ab" },
error.toJSON(),
);
// Validate before sink dispatch (defensive — normally guaranteed by pipeline)
const parsed = LogRecordSchema.safeParse(record);
if (!parsed.success) throw new Error("Invariant: invalid LogRecord");
}
```
## API Reference
### `LOG_LEVELS: Record<LogLevel, number>`
Numeric weights: `{ debug: 0, info: 1, warn: 2, error: 3 }`.
### `isLogLevel(value): value is LogLevel`
Type guard returning `true` for `"debug" | "info" | "warn" | "error"`.
### `shouldLog(current, candidate): boolean`
Returns `true` when `LOG_LEVELS[candidate] >= LOG_LEVELS[current]`.
### `buildRecord(level, msg, ctx, ts, traceCtx?, errorPayload?): LogRecord`
| Parameter | Type | Description |
|-----------|------|-------------|
| `level` | `LogLevel` | Severity |
| `msg` | `string` | Human-readable message |
| `ctx` | `Record<string, unknown>` | Merged metadata/tags |
| `ts` | `string` | ISO-8601 timestamp |
| `traceCtx` | `{ trace_id?, span_id? }?` | OTel context |
| `errorPayload` | `unknown?` | Serialised error payload |
Omits `trace_id`, `span_id`, and `error` keys entirely when their values are `undefined`.
### `LogRecordSchema` (from `log-record.schemas.ts`)
Zod schema validating `LogRecord`. Fields: `ts` (`z.string().datetime()`), `level` (`LogLevelSchema`), `msg` (`z.string()`), `ctx` (`z.record(z.unknown())`), optional `trace_id`, `span_id`, `error`.
## Implementation Notes
`buildRecord` deliberately omits keys with `undefined` values rather than setting them to `undefined` — this keeps NDJSON payloads lean and avoids confusing consumers that test `"key" in record`. The Zod schema defaults `ctx` to `{}` so consumers can omit it when there is no additional context.

View File

@@ -0,0 +1,91 @@
# redaction
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The redaction module provides declarative, irreversible sanitisation of log context objects before they reach any sink. It supports two rule kinds: field-path rules (dot-notation with `*` wildcard) and pattern rules (regex applied to leaf key names). It also automatically redacts any value wrapped in `Sensitive<T>` from `@suaveplan/error`. Redaction always operates on a deep clone — the original context is never mutated.
The invariant is strict: once a field matches a rule or is `Sensitive`, its value becomes the label `"[REDACTED]"` and can never be recovered from the log record. This ensures sensitive data (passwords, tokens, PII) never reaches log storage or downstream consumers.
## Features
- `PathRedactionRule` — matches exact dot-notation paths; supports `*` wildcard at any segment
- `PatternRedactionRule` — applies a `RegExp` to every leaf key name
- Automatic detection of `Sensitive<T>` wrappers via `isSensitive()` from `@suaveplan/error`
- Deep cloning before redaction — original object untouched
- Configurable replacement label (default `"[REDACTED]"`) and max traversal depth (default `10`)
- `RedactionEngine` class for reuse across many records; `applyRedaction()` functional one-shot helper
- Property-based tests (fast-check) verify the redaction invariant across arbitrary inputs
## Usage: Basic Example
```ts
import { applyRedaction } from "@suaveplan/logger/redaction";
const clean = applyRedaction(
{ userId: "u-1", password: "hunter2", metadata: { token: "abc" } },
{
rules: [
{ kind: "path", path: "password" },
{ kind: "path", path: "metadata.token" },
],
},
);
// { userId: "u-1", password: "[REDACTED]", metadata: { token: "[REDACTED]" } }
```
## Usage: Advanced Example
```ts
import { RedactionEngine } from "@suaveplan/logger/redaction";
import { markSensitive } from "@suaveplan/error/redact";
const engine = new RedactionEngine({
rules: [
{ kind: "pattern", pattern: /password|secret|token/i },
{ kind: "path", path: "*.credentials" },
],
redactedLabel: "***",
maxDepth: 5,
});
// Sensitive<T> is auto-detected without a rule:
const ctx = { ssn: markSensitive("123-45-6789"), name: "Alice" };
const clean = engine.redact(ctx);
// { ssn: "***", name: "Alice" }
```
## API Reference
### `class RedactionEngine`
**Constructor**: `new RedactionEngine(options?: RedactionOptions)`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `rules` | `RedactionRule[]` | `[]` | Path and pattern rules |
| `redactedLabel` | `string` | `"[REDACTED]"` | Replacement value |
| `maxDepth` | `number` | `10` | Maximum recursion depth |
#### `.redact(ctx): Record<string, unknown>`
Returns a deep-cloned copy of `ctx` with all matching fields replaced by the label.
### `applyRedaction(ctx, options?): Record<string, unknown>`
Functional one-shot wrapper — creates a `RedactionEngine` and calls `.redact()`.
### `type RedactionRule`
```ts
type RedactionRule = PathRedactionRule | PatternRedactionRule;
interface PathRedactionRule { kind: "path"; path: string; }
interface PatternRedactionRule { kind: "pattern"; pattern: RegExp; }
```
## Implementation Notes
Path matching splits both the rule path and the candidate key path on `.`, then compares segments one-by-one, treating `*` as a wildcard. Paths of different lengths never match. The wildcard only covers a single segment — `a.*.c` matches `a.b.c` but not `a.b.d.c`. Arrays are traversed; each element uses a `[i]`-suffixed path that path rules will never match (arrays are safe by default — only Sensitive wrappers or pattern rules can redact array elements).

View File

@@ -0,0 +1,89 @@
# sampling
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The sampling module provides pluggable policies for deciding which log records to emit and which to drop. Reducing log volume is essential in high-throughput services where emitting every debug record would overwhelm storage. Three sampler implementations cover the common cases: `HeadSampler` for deterministic trace-ID-based rate limiting, `TailSampler` for buffered post-hoc decisions (useful for keeping error traces in full while dropping healthy ones), and `RuleSampler` for predicate-based routing.
The sampling decision happens before redaction and sink dispatch, so dropped records never touch sensitive-field handling or I/O.
## Features
- `HeadSampler` — deterministic FNV-1a hash of `trace_id` against a `rate ∈ [0,1]`; records without a `trace_id` are always kept
- `TailSampler` — buffers records per `trace_id`; flushes the buffer to a user-supplied `decide()` callback when it reaches `bufferSize`
- `RuleSampler` — ordered predicate list; first matching rule wins; configurable fallback
- `SampleAllSampler` / `SampleNoneSampler` — trivial always-keep / always-drop sentinels
- `createSampler(policy)` factory from the `SamplingPolicy` discriminated union
- Property-based tests verify `HeadSampler` determinism across 500 trace IDs
## Usage: Basic Example
```ts
import { HeadSampler } from "@suaveplan/logger/sampling";
const sampler = new HeadSampler({ rate: 0.1 }); // keep 10% of traces
const decision = sampler.decide(record); // "keep" or "drop"
```
## Usage: Advanced Example
```ts
import { createSampler } from "@suaveplan/logger/sampling";
import type { SamplingPolicy } from "@suaveplan/logger/sampling";
// Keep all errors; drop 90% of info; drop everything else
const policy: SamplingPolicy = {
kind: "rule",
options: {
rules: [
{ match: (r) => r.level === "error", decision: "keep" },
{ match: (r) => r.level === "info", decision: Math.random() < 0.1 ? "keep" : "drop" },
],
fallback: "drop",
},
};
const sampler = createSampler(policy);
```
## API Reference
### `class HeadSampler implements Sampler`
**Constructor**: `new HeadSampler({ rate: number })`
Deterministic: same `trace_id` always yields the same decision. Records without `trace_id` are always kept.
**Throws** `RangeError` if `rate < 0 || rate > 1`.
### `class TailSampler implements Sampler`
**Constructor**: `new TailSampler({ bufferSize?, decide })`
| Option | Default | Description |
|--------|---------|-------------|
| `bufferSize` | 100 | Maximum records to buffer per trace before forcing a decision |
| `decide` | — | `(records: LogRecord[]) => SamplingDecision` |
#### `.flush(traceId?): SamplingDecision`
Forces a decision on the buffer for `traceId` (or all buffers if omitted).
### `class RuleSampler implements Sampler`
**Constructor**: `new RuleSampler({ rules, fallback? })`
Applies `rules` in order; returns `fallback` (default `"keep"`) when no rule matches.
### `createSampler(policy: SamplingPolicy): Sampler`
Factory that instantiates the appropriate sampler from the `SamplingPolicy` union.
### `type SamplingPolicy`
Discriminated union: `{ kind: "all" } | { kind: "none" } | { kind: "head"; options } | { kind: "tail"; options } | { kind: "rule"; options }`.
## Implementation Notes
`HeadSampler` uses FNV-1a 32-bit hashing for determinism — the same trace ID always maps to the same fraction of the `[0, 0xFFFFFFFF]` range. This ensures all log records from a single trace are either kept or dropped together, which is the key invariant of head-based sampling. The distribution is uniform enough for practical sampling rates but not cryptographically secure — use only for sampling decisions, not security.

View File

@@ -0,0 +1,97 @@
# buffered-sink
## Purpose
`BufferedSink` is an in-memory queuing wrapper around any `Sink` implementation. It decouples the hot path of log record production from the potentially slower I/O performed by the underlying sink, allowing burst writes to be absorbed without blocking callers. The buffer is bounded by a configurable `maxQueueSize`, and when that limit is reached the sink applies one of two overflow policies: silently drop the excess record (`"drop"`) or drain the head record inline before pushing the new one (`"block"`). The `"block"` policy never parks writers in an external waiter list; it always makes forward progress by awaiting `inner.write` of the oldest record before accepting the new one. This eliminates the deadlock hazard of the original "wait until someone calls `flush()` externally" design.
This class is designed for high-throughput server processes where log writes must not stall request handling, and for test harnesses where controlling when records reach the underlying sink is essential for deterministic assertions.
## Features
- Wraps any `Sink` and delegates `write`, `flush`, and `close` calls
- Bounded in-memory queue with configurable `maxQueueSize` (default 1000)
- Two overflow policies: `"drop"` silently discards excess records; `"block"` suspends the caller via a `Promise` until queue capacity frees up
- `dropped` property tracks cumulative records discarded under the `"drop"` policy
- `flush(signal?)` drains the queue sequentially, forwarding each record to the inner sink; throws `LoggerError(LOGGER.FLUSH)` if the provided `AbortSignal` fires mid-drain
- `close()` flushes all pending records then delegates to `inner.close()` if implemented
- Re-entrant flush guard — concurrent `flush()` calls short-circuit rather than double-draining
- `"block"` policy drains inline on overflow — no external `flush()` is required for `write()` to make forward progress, eliminating the deadlock hazard of waiter-list designs
- Constructor rejects non-positive `maxQueueSize` with `LoggerError(LOGGER.CONFIG)`
## Usage: Basic Example
```ts
import { BufferedSink } from "@suaveplan/logger/sink";
import { JsonSink } from "@suaveplan/logger/sink";
const json = new JsonSink({ writer: (line) => process.stdout.write(line + "\n") });
const buffer = new BufferedSink({
sink: json,
maxQueueSize: 500,
overflowPolicy: "drop",
});
// Write a record — enqueued immediately, not yet flushed to stdout
await buffer.write(record);
// Drain all pending records to the underlying JsonSink
await buffer.flush();
```
## Usage: Advanced Example
```ts
import { BufferedSink } from "@suaveplan/logger/sink";
import { JsonSink } from "@suaveplan/logger/sink";
const sink = new BufferedSink({
sink: new JsonSink({ writer: appendToFile }),
maxQueueSize: 2000,
overflowPolicy: "block", // back-pressure: callers await capacity
});
// Graceful shutdown with a 5-second deadline
process.on("SIGTERM", async () => {
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(), 5_000);
try {
await sink.flush(ac.signal);
} catch {
// AbortSignal fired — some records may remain in the queue
console.error(`Dropped ${sink.dropped} records during shutdown`);
} finally {
clearTimeout(timer);
await sink.close();
}
});
```
## API Reference
### `class BufferedSink implements Sink`
**Constructor**: `new BufferedSink(options: BufferedSinkOptions)`
| Option | Type | Default | Description |
|---|---|---|---|
| `sink` | `Sink` | required | The underlying sink that receives flushed records |
| `maxQueueSize` | `number` | `1000` | Maximum queued records before overflow policy triggers |
| `overflowPolicy` | `"drop" \| "block"` | `"drop"` | Action when queue is full |
**Properties**
- `dropped: number` — Cumulative count of records discarded under the `"drop"` policy. Does not reset between flushes.
**Methods**
- `write(record: LogRecord): Promise<void>` — Enqueue a record. Under `"drop"` policy, records beyond `maxQueueSize` are silently counted and discarded. Under `"block"` policy, when the queue is at capacity `write()` first awaits `inner.write` of the head record, then pushes; this ensures forward progress without requiring an external `flush()` and rules out the deadlock that a parked-writer design would expose if no one ever flushed.
- `flush(signal?: AbortSignal): Promise<void>` — Drain the entire queue by forwarding records to the inner sink in FIFO order. Throws `LoggerError(LOGGER.FLUSH)` if the signal is aborted. Throws `LoggerError(LOGGER.SINK)` if the inner sink's `write` rejects. Re-entrant calls return immediately.
- `close(): Promise<void>` — Flush then close the inner sink (if it implements `close()`).
## Implementation Notes
`BufferedSink` uses a simple array queue (`shift()` for FIFO) rather than a ring buffer. For typical logging workloads (hundreds of records per second) the GC pressure is negligible. The `"block"` policy makes forward progress inline (drains the head before pushing) so concurrent overflowing writers serialise on `inner.write` rather than queuing in a waiter list — back-pressure is expressed naturally through awaited promises. The design favours simplicity over micro-optimisation since the bottleneck is always the inner sink's I/O, not the queue operations.
There is no background flush timer — `BufferedSink` is purely demand-driven. If you need periodic auto-flush, start your own `setInterval` that calls `flush()` and clears it on shutdown.

View File

@@ -0,0 +1,89 @@
# console-sink
## Purpose
`ConsoleSink` is the human-readable terminal output sink for `@suaveplan/logger`. It formats each `LogRecord` into a single coloured, prefixed line and dispatches it to the appropriate `console` method (`console.debug`, `console.info`, `console.warn`, or `console.error`) so that log output appears at the correct severity level in browser DevTools and Node.js terminals alike.
The sink is intentionally synchronous — `write` returns `void`, not `Promise<void>` — because `console.*` is itself synchronous and any async wrapper would introduce misleading latency for a purely I/O-synchronous operation.
## Features
- ANSI colour codes per log level: cyan for `debug`, green for `info`, yellow for `warn`, red for `error`
- Optional ISO timestamp prefix, dimmed for contrast in colour mode
- Optional `#tag` rendering when the log record carries `ctx.__tags`
- Optional compact JSON metadata block printed below the message line
- All formatting options can be toggled at construction time or by mutating public fields after construction
- Delegates to the correct `console` method for each log level, preserving browser DevTools severity grouping
- No external dependencies beyond the `Sink` interface and `LogRecord` types
## Usage: Basic Example
```ts
import { ConsoleSink } from "@suaveplan/logger/sink";
const sink = new ConsoleSink(); // colors + timestamp enabled by default
sink.write({
ts: new Date().toISOString(),
level: "info",
msg: "Server started",
ctx: {},
});
// Output: [2024-01-01T00:00:00.000Z] [INFO] Server started
```
## Usage: Advanced Example
```ts
import { ConsoleSink } from "@suaveplan/logger/sink";
import { ConsoleLogger } from "@suaveplan/logger/console";
// Plain text for CI environments (no ANSI codes in log files)
const sink = new ConsoleSink({
colors: false,
timestamp: true,
includeMetadata: true,
includeTags: true,
});
const logger = new ConsoleLogger({ sink });
logger.info("Request completed", {
__tags: ["http", "perf"],
duration: 42,
status: 200,
});
// Output:
// [2024-01-01T00:00:00.000Z] [INFO] [#http #perf] Request completed
// {
// "__tags": ["http", "perf"],
// "duration": 42,
// "status": 200
// }
```
## API Reference
### `class ConsoleSink implements Sink`
**Constructor**: `new ConsoleSink(options?: ConsoleSinkOptions)`
All options are also exposed as mutable public fields so they can be toggled after construction (e.g., to disable colours when a terminal is detected as non-interactive).
| Option / Field | Type | Default | Description |
|---|---|---|---|
| `colors` | `boolean` | `true` | Wrap level and timestamp tokens in ANSI colour escapes |
| `timestamp` | `boolean` | `true` | Prefix each line with the record's `ts` value |
| `includeMetadata` | `boolean` | `false` | Append `ctx` as a formatted JSON block below the message |
| `includeTags` | `boolean` | `true` | Inline `ctx.__tags` values as `#tag` tokens |
**Methods**
- `write(record: LogRecord): void` — Format and print the record synchronously. Never throws; `console.*` errors (which are vanishingly rare) surface as uncaught exceptions in the host environment.
## Implementation Notes
ANSI escape codes are defined as a small static map keyed by `LogLevel` plus `"reset"` and `"dim"` sentinels. The formatter builds a `parts: string[]` array and joins once at the end to minimise string concatenation allocations per record.
The sink checks `console[method]` at call time rather than caching the reference at construction, which means it respects any `console` mocking that test frameworks install. This is deliberate — `ConsoleSink` is commonly used in test environments where `console.error` is wrapped to assert that no unexpected errors are logged.
Colour mode does not auto-detect `process.stdout.isTTY`; that decision is left to the caller (typically the factory or the logger bootstrap) so the sink itself remains environment-agnostic and testable without process environment manipulation.

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.

View File

@@ -0,0 +1,90 @@
# sink
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The sink layer defines the `Sink` interface — the pluggable output primitive for `@suaveplan/logger` — and provides three concrete implementations: `ConsoleSink` for human-readable terminal output during development, `JsonSink` for line-delimited JSON (NDJSON) suitable for log aggregators, and `BufferedSink` for asynchronous, bounded-queue writes with configurable overflow policy and `AbortSignal`-aware flush. The `MultiSink` utility routes a single record to multiple sinks in parallel.
By decoupling the logger from any specific I/O implementation, the same `AbstractLogger` can write to a terminal, a file, a remote endpoint, or an in-memory buffer without code changes — only the sink configuration differs.
## Features
- `Sink` interface: `write(record)`, optional `flush(signal?)`, optional `close()`
- `ConsoleSink` — ANSI colour per level, ISO timestamp prefix, optional tag and metadata formatting
- `JsonSink` — serialises `LogRecord` to a single JSON line, passes to a pluggable `writer` callback (default: `process.stdout.write`)
- `BufferedSink` — bounded in-memory queue; `"drop"` policy silently discards overflow; `"block"` policy awaits drain; `flush(AbortSignal?)` drains with cancellation support
- `MultiSink` — fan-out to multiple sinks concurrently
- `isSink()` type guard; `flushSink()` / `closeSink()` helpers that no-op for sinks lacking those methods
## Usage: Basic Example
```ts
import { JsonSink } from "@suaveplan/logger/sink";
const sink = new JsonSink({ writer: (line) => process.stdout.write(line + "\n") });
sink.write(record); // {"ts":"...","level":"info","msg":"hello","ctx":{}}
```
## Usage: Advanced Example
```ts
import { BufferedSink } from "@suaveplan/logger/sink";
import { JsonSink } from "@suaveplan/logger/sink";
const json = new JsonSink({ writer: appendToFile });
const buf = new BufferedSink({
sink: json,
maxQueueSize: 500,
overflowPolicy: "drop",
});
// In a long-running process, flush on shutdown:
process.on("SIGTERM", async () => {
const ac = new AbortController();
setTimeout(() => ac.abort(), 5_000); // 5 s timeout
await buf.flush(ac.signal);
await buf.close();
});
```
## API Reference
### `interface Sink`
```ts
interface Sink {
write(record: LogRecord): void | Promise<void>;
flush?(signal?: AbortSignal): Promise<void>;
close?(): Promise<void>;
}
```
### `class ConsoleSink implements Sink`
Options: `{ colors?, timestamp?, includeMetadata?, includeTags? }`.
### `class JsonSink implements Sink`
Options: `{ writer? }`. Throws `LoggerError(LOGGER.SERIALIZE)` if `JSON.stringify` fails.
### `class BufferedSink implements Sink`
Options: `{ sink, maxQueueSize?, overflowPolicy? }`. Property `.dropped` counts overflow-dropped records.
### `class MultiSink implements Sink`
**Constructor**: `new MultiSink(sinks: Sink[])`. Writes and flushes all sinks concurrently.
### `isSink(value): value is Sink`
Type guard.
### `flushSink(sink, signal?) / closeSink(sink)`
Helpers that call `sink.flush(signal)` / `sink.close()` only if the method exists.
## Implementation Notes
`BufferedSink` does not start a background interval — it flushes lazily on `flush()` or `close()`. This keeps the class dependency-free (no `Clock` needed) and avoids surprises in test environments. The `"block"` overflow policy uses a simple Promise queue to suspend `write()` callers until the next flush iteration frees capacity.

View File

@@ -0,0 +1,100 @@
# storage-logger
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`StorageLogger` persists log batches to any key-value store that implements the `StorageInterface` contract (set/get/list/delete with `Uint8Array` values). It extends `AbstractLogger` so all enterprise features (redaction, sampling, OTel, clock injection, metrics) run before any storage write. Log entries are buffered in memory and flushed either automatically on a configurable interval, when the buffer reaches `maxBatchSize`, or manually via `flush()`. Retrieved logs can be filtered by date range and limited in count.
## Features
- Buffered writes: accumulate entries in-memory, flush as a single JSON batch keyed by timestamp
- Auto-flush on interval (configurable, default 5 s) or manual `flush(signal?)`
- `maxBatchSize` threshold triggers an immediate async flush
- `getLogs({ limit?, startDate?, endDate?, signal? })` retrieves and deserialises all stored batches
- `clearLogs(olderThan?)` removes batches (optionally filtered by timestamp)
- `destroy()` stops the auto-flush timer and flushes remaining buffer
- Injectable `StorageInterface` — works with any KV store (Cloudflare KV, IndexedDB shims, in-memory)
## Usage: Basic Example
```ts
import { StorageLogger } from "@suaveplan/logger/storage";
const logger = new StorageLogger(cloudflareKV, { level: "info" });
logger.info("Request received", { path: "/api/users" });
// Automatically flushed every 5 s
```
## Usage: Advanced Example
```ts
import { StorageLogger } from "@suaveplan/logger/storage";
const logger = new StorageLogger(storage, {
level: "debug",
storagePrefix: "app:logs",
maxBatchSize: 50,
flushInterval: 10_000,
autoFlush: true,
});
// Manual flush with abort support:
const ac = new AbortController();
setTimeout(() => ac.abort(), 3_000);
await logger.flush(ac.signal);
// Retrieve last 100 error logs in last hour:
const recent = await logger.getLogs({
limit: 100,
startDate: new Date(Date.now() - 3_600_000),
});
// Teardown:
await logger.destroy();
```
## API Reference
### `class StorageLogger<TMetadata> extends AbstractLogger<TMetadata>`
**Constructor**: `new StorageLogger(storage, config?)`
| Config | Type | Default | Description |
|--------|------|---------|-------------|
| `storagePrefix` | `string` | `"logs"` | Key prefix for stored batches |
| `maxBatchSize` | `number` | `100` | Triggers immediate flush when reached |
| `flushInterval` | `number` | `5000` | Auto-flush interval in ms |
| `autoFlush` | `boolean` | `true` | Enable interval-based flushing |
#### `flush(signal?: AbortSignal): Promise<void>`
Writes the current buffer to storage. Aborts if `signal` is already aborted before the write.
#### `getLogs(options?): Promise<LogEntry<TMetadata>[]>`
Options: `{ limit?, startDate?, endDate?, signal? }`. Returns entries sorted newest-first.
#### `clearLogs(olderThan?: Date): Promise<void>`
Deletes all stored batches, or only those older than `olderThan`.
#### `destroy(): Promise<void>`
Clears the auto-flush timer and flushes remaining buffer.
### `interface StorageInterface`
```ts
interface StorageInterface {
set(key: string, value: Uint8Array): Promise<void>;
get(key: string): Promise<Uint8Array | null>;
list(options?: { prefix?: string }): Promise<{ keys: string[] }>;
delete(key: string): Promise<void>;
}
```
## Implementation Notes
Each flush writes a JSON array of `LogEntry` objects under the key `${prefix}:${ISO-timestamp}:${uuid}`. The UUID prevents key collisions when multiple instances flush at the same millisecond. `extractTimestamp` reverse-engineers the timestamp by finding the last `:` separator, which is robust because the ISO timestamp itself contains colons. On flush failure, the buffered entries are re-queued at the front of the buffer so no records are silently lost.