# Spec Delta — logging ## MODIFIED Requirements ### LG-11 — Metrics `AbstractLogger` MUST expose `getMetrics()` returning: ```ts { totalLogs: number; logsByLevel: Record; droppedBySampling: number; droppedByBackpressure: number; /** Records lost because a sink threw or rejected during dispatch. */ droppedBySinkError: number; lastLogTime?: Date; } ``` `droppedByBackpressure` is reserved for downstream queue saturation (e.g. `BufferedSink` overflow under `policy: "drop"`). Sink throws and rejections during `dispatchToSinks` MUST increment `droppedBySinkError` instead — never `droppedByBackpressure`. **Verified by:** `src/conformance.spec-conformance.test.ts` (sink-error counter scenarios) and `src/abstract/abstract-logger.test.ts`. ### LG-12 — OTel instrumentation For every `log()` call the implementation MUST: - Increment a `logger.records_total` counter (attributes: `level`) - Record a `logger.record_size_bytes` histogram (attributes: `level`) - Increment `logger.dropped_total` when a record is dropped, with a `reason` attribute drawn from the closed set `"sampling" | "backpressure" | "sink_error"`. `sink_error` MUST be fired for sink throws and rejections caught by the dispatch path. All via `@opentelemetry/api` — no-op when no exporter is registered. **Verified by:** `src/abstract/abstract-logger.spec-conformance.test.ts` (records_total + record_size_bytes + dropped_total{reason:"sampling"} + dropped_total{reason:"sink_error"}). ### LG-13 — Structured errors All errors thrown by `@suaveplan/logger` MUST be instances of `LoggerError extends SuaveplanError` with stable codes registered via `defineCodes`: | Code constant | Trigger | |---------------|---------| | `LOGGER.SINK` | A sink's `write()` rejected or threw, OR `BufferedSink.flush()`'s inner `write` rejected. The dispatch path MUST surface this code through `fallbackSink` (it MUST NOT throw out of `log()`). | | `LOGGER.SERIALIZE` | JSON serialisation of a log record failed | | `LOGGER.CONFIG` | Invalid configuration detected at construction time, including `BufferedSink` `maxQueueSize <= 0` | | `LOGGER.FLUSH` | `flush()` aborted via `AbortSignal` | **Verified by:** `src/conformance.spec-conformance.test.ts` (LOGGER.SINK from dispatch + LOGGER.CONFIG from BufferedSink constructor) and `src/sink/sink.test.ts` (LOGGER.FLUSH from BufferedSink.flush). ## ADDED Requirements ### LG-21 — Sink dispatch isolation `AbstractLogger.dispatchToSinks(record)` MUST: 1. Iterate sinks in registration order. 2. Wrap each `sink.write(record)` in an isolated try/catch (sync) and `.catch(...)` (async). A throw / rejection in sink `i` MUST NOT prevent sinks `i+1, …, n-1` from being attempted. 3. On any sink failure: increment `droppedBySinkError`, fire `logger.dropped_total{reason:"sink_error"}`, build a `LoggerError(LOGGER.SINK)` (with `cause` set to the original `Error`, if any), and write a fallback `LogRecord` to `config.fallbackSink`. The fallback record MUST carry the serialised `LoggerError` under `record.error` and include the failing sink's index under `record.ctx.__sink_index`. 4. The default `fallbackSink` MUST write via `console.error`. 5. `dispatchToSinks` MUST NOT throw out of the `log()` call. **Verified by:** `src/conformance.spec-conformance.test.ts` (sync throw, async rejection, two-sink isolation, fallbackSink record shape). ### LG-22 — BufferedSink no-deadlock contract `BufferedSink` under `overflowPolicy: "block"` MUST make forward progress without requiring an external `flush()`. When the queue is already at `maxQueueSize` and `write()` is called, the implementation MUST drain the head record inline (await `inner.write(head)`) before pushing the new record. There MUST NOT be an external waiter list that depends on `flush()` for resolution. The constructor MUST throw `LoggerError(LOGGER.CONFIG)` if `maxQueueSize` is not a positive integer. **Verified by:** `src/conformance.spec-conformance.test.ts` (forward-progress without flush + invalid maxQueueSize) and `src/sink/sink.test.ts`. ### LG-23 — MultiSink fan-out semantics `MultiSink([a, b, …])` MUST dispatch each record to every wrapped sink in parallel via `Promise.all`. When composed under `AbstractLogger`, per-sink failures inherit the LG-21 contract: an inner sink throw / rejection inside the parallel fan-out is reported to the parent logger's `fallbackSink` and does not prevent the other inner sinks from receiving the record. `flush()` and `close()` similarly fan out in parallel and aggregate via `Promise.all`. **Verified by:** `src/conformance.spec-conformance.test.ts` (two-sink parallel dispatch) and `src/sink/sink.test.ts`. ### LG-24 — Generic Logger surface The exported `Logger` interface MUST be generic over metadata shape: `Logger = Record>`. Concrete loggers (`ConsoleLogger`, `StorageLogger`, `EventLogger`) and `AbstractLogger` MUST honour the same generic. The default parameter preserves the original LG-02 `ctx: Record` clause for callers that don't specialise. **Verified by:** `src/conformance.spec-conformance.test.ts` (typed-metadata scenario) and `src/abstract/abstract-logger.test.ts`. ### LG-25 — Concrete loggers The package MUST export the following concrete loggers, each extending `AbstractLogger`: | Class | `writeLog` behaviour | |-------|----------------------| | `ConsoleLogger` | Routes `LogEntry` to `console[level]` (with optional formatting). | | `StorageLogger` | Persists each `LogEntry` to a `StorageLike` adapter. | | `EventLogger` | Publishes each `LogEntry` to an `EventBusLike`. | Each MUST be importable both from the package root and from a dedicated subpath export (`./console`, `./storage`, `./event`). **Verified by:** `src/console/console-logger.test.ts`, `src/storage/storage-logger.test.ts`, `src/event/event-logger.test.ts`. ### LG-26 — Lifecycle controls `AbstractLogger` MUST expose `enable()`, `disable()`, `isEnabled()`, and `resetMetrics()` as part of the public surface. `disable()` suppresses every subsequent `log()` call (including sink dispatch and metrics). `resetMetrics()` returns the metrics struct to its zero state without affecting configuration. **Verified by:** `src/abstract/abstract-logger.test.ts` (enable/disable + resetMetrics scenarios). ### LG-27 — Sensitive<T> envelope `RedactionEngine` MUST recognise values wrapped in the `Sensitive` envelope from `@suaveplan/error/redact` (or the package's own re-export) and replace them with `"[REDACTED]"` in the redacted record. Detection MUST occur regardless of whether the field path matches a configured `RedactionRule`. **Verified by:** `src/redaction/redaction.test.ts`.