6.6 KiB
Spec Delta — logging
MODIFIED Requirements
LG-11 — Metrics
AbstractLogger MUST expose getMetrics() returning:
{
totalLogs: number;
logsByLevel: Record<LogLevel, number>;
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_totalcounter (attributes:level) - Record a
logger.record_size_byteshistogram (attributes:level) - Increment
logger.dropped_totalwhen a record is dropped, with areasonattribute drawn from the closed set"sampling" | "backpressure" | "sink_error".sink_errorMUST 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:
- Iterate sinks in registration order.
- Wrap each
sink.write(record)in an isolated try/catch (sync) and.catch(...)(async). A throw / rejection in sinkiMUST NOT prevent sinksi+1, …, n-1from being attempted. - On any sink failure: increment
droppedBySinkError, firelogger.dropped_total{reason:"sink_error"}, build aLoggerError(LOGGER.SINK)(withcauseset to the originalError, if any), and write a fallbackLogRecordtoconfig.fallbackSink. The fallback record MUST carry the serialisedLoggerErrorunderrecord.errorand include the failing sink's index underrecord.ctx.__sink_index. - The default
fallbackSinkMUST write viaconsole.error. dispatchToSinksMUST NOT throw out of thelog()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<TMetadata extends Record<string, unknown> = Record<string, unknown>>. Concrete loggers (ConsoleLogger,
StorageLogger, EventLogger) and AbstractLogger MUST honour
the same generic. The default parameter preserves the original
LG-02 ctx: Record<string, unknown> 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<T> 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.