4.8 KiB
OpenSpec Change: logger-silent-failure-fix
- Change ID: logger-silent-failure-fix
- Package:
@suaveplan/logger - Status: PROPOSED
- Date: 2026-04-27
Summary
Close two silent-failure bugs in @suaveplan/logger and tighten
openspec/specs/logging/spec.md to cover substrate behaviours
already shipped by the package: MultiSink fan-out semantics,
EventLogger, StorageLogger, enable/disable, resetMetrics,
and the Sensitive<T> redaction envelope. Pin every behavioural
fix and every previously-undocumented surface behind a
*.spec-conformance.test.ts so the 5-gate publish flow catches
drift.
Current state
@suaveplan/logger is fully implemented and ships 171 passing
tests at 100% coverage, but two paths fail silently:
-
Sink dispatch swallows everything.
dispatchToSinks(src/abstract/abstract-logger.ts) catches every sink throw and promise rejection and increments a singledroppedByBackpressure++counter. Spec defines aLOGGER.SINKerror code, but the dispatch path never raises it — onlyBufferedSink.flushdoes. A serializer crash inside a sink is misclassified as backpressure with no error surfaced anywhere. -
BufferedSinkblock-policy deadlock. Underpolicy: "block",write()parks writers in a privateflushWaiterslist. Drain only happens when an external caller invokesflush(). If nobody ever does, blocked writers hang forever — there is no internal timer, no inline drain, and no construction-time guard.
Additionally, requirements LG-11 / LG-12 / LG-13 don't reflect
the new failure-mode taxonomy and the spec lacks requirements
for MultiSink, the concrete loggers, lifecycle methods, and
Sensitive<T>.
What changes
- Modify Requirement: LG-11 Metrics — add
droppedBySinkError: numberto theLoggerMetricsshape. - Modify Requirement: LG-12 OTel instrumentation — extend
the
logger.dropped_totalcounter'sreasonattribute set to include"sink_error". - Modify Requirement: LG-13 Structured errors — codify
that
LOGGER.SINKis raised by the dispatch path (not justBufferedSink.flush) and routed to a configurablefallbackSink. - ADD Requirement: LG-21 Sink dispatch isolation —
per-sink try/catch; one bad sink never halts the others;
each failure produces a
LoggerError(LOGGER.SINK)written tofallbackSink(default: a sink that callsconsole.error). - ADD Requirement: LG-22 BufferedSink no-deadlock contract
— under
policy: "block",write()drains the head record inline before pushing. No parked-writer list, no externalflush()required for forward progress. - ADD Requirement: LG-23 MultiSink fan-out semantics —
parallel dispatch via
Promise.all; per-sink errors isolated and reported to the parent logger'sfallbackSink; all sinks attempted regardless of order. - ADD Requirement: LG-24 Generic Logger surface —
Logger<TMetadata>is generic over metadata shape; defaults toRecord<string, unknown>for back-compat with the original LG-02ctx: Record<string, unknown>clause. - ADD Requirement: LG-25 Concrete loggers —
EventLoggerandStorageLoggerare documented exports with their minimal contracts. - ADD Requirement: LG-26 Lifecycle controls —
enable(),disable(),isEnabled(),resetMetrics()are part of the supported public surface. - ADD Requirement: LG-27 Sensitive envelope —
RedactionEnginerecognisesSensitive-tagged values and redacts them automatically (LG-07 already names this; the new requirement pins the verification path). - Add
src/conformance.spec-conformance.test.tspinning LG-21, LG-22, and LG-23 so future drift exits the 5-gate non-zero. - Add
**Verified by:**lines to every requirement (existing + new) in the folded spec.
Why now
Both bugs are silent — the only signal a misbehaving sink emits today is a generic counter increment with no error text, no stack, no fallback record. The block-policy deadlock is undetectable at construction time. Both ship green through the existing 5-gate. Tightening the contract behind conformance tests is the smallest possible change that prevents recurrence.
Impact
@suaveplan/logger—LoggerMetricsgainsdroppedBySinkError.AbstractLoggerConfiggainsfallbackSink?: Sink. Two existing assertions inabstract-logger.test.tsand one inabstract-logger.spec-conformance.test.tsflip fromdroppedByBackpressure/reason: "backpressure"todroppedBySinkError/reason: "sink_error". Coverage stays at 100%.- Downstream consumers — none affected; new metric and config
field are additive.
LOGGER.SINKerror code already exists. - Spec readers — get an accurate picture of the dispatch contract, the fallback sink, and the concrete loggers.