153 lines
6.6 KiB
Markdown
153 lines
6.6 KiB
Markdown
# Spec Delta — logging
|
|
|
|
## MODIFIED Requirements
|
|
|
|
### LG-11 — Metrics
|
|
|
|
`AbstractLogger` MUST expose `getMetrics()` returning:
|
|
|
|
```ts
|
|
{
|
|
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_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<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`.
|