Skip to content

ADR-0001 — Pino (framework) + Winston/EvoLogger (app-code) coexistence

Status: Accepted Date: 2026-04-16 Author(s): Platform engineering

Context

All five NestJS apps need structured, trace-correlated JSON logs in Loki. The codebase already had ~40 call sites using EvoLogger (a winston-backed facade from @bebkovan/server-core). The OTel ecosystem has a stable pino instrumentation (@opentelemetry/instrumentation-pino) that bridges log records into the OTLP logs SDK for free. Winston's equivalent (@opentelemetry/instrumentation-winston) exists but is less mature and does not bridge records into the OTLP logs pipeline — it only injects trace context fields.

Decision

Run two loggers side by side:

  1. nestjs-pino is the Nest framework logger. It captures HTTP request/response lifecycle, Nest module events, and any constructor(private logger: Logger) injection sites. Records are JSON on stdout, bridged into OTel's logs API by PinoInstrumentation, and exported via OTLP to the collector → Loki.

  2. EvoLogger (winston) continues to back the ~40 existing app-code call sites. WinstonInstrumentation (enabled by default in getNodeAutoInstrumentations) injects trace_id / span_id / trace_flags at the winston transport layer, so records are trace-tagged. These records go to docker stdout and reach Loki via the filelog/docker collector receiver.

Wiring: libs/shared/src/logger/pino-logger.module.ts provides NestLoggerModule.forRoot(). Every app.module.ts imports it before EvoLoggerModule. libs/shared/src/basic/base.main.ts swaps the framework logger: app.useLogger(app.get(Logger)). pre-otel.main.ts disables the auto-pino instrumentation and registers one with custom logKeys (trace_id, span_id, trace_flags).

Alternatives considered

  1. Migrate all ~40 EvoLogger call sites to pino. Rejected: touches 40+ files across every module for zero functional gain. EvoLogger records are already trace-tagged via WinstonInstrumentation and now reach Loki via the filelog receiver. The mass rewrite is pure churn.

  2. Use winston everywhere (drop pino). Rejected: winston has no stable OTLP log bridge. We would lose the automatic request-lifecycle logging that nestjs-pino provides and would need to build custom Nest middleware to get equivalent HTTP access logs.

  3. Use pino everywhere (drop EvoLogger). Same outcome as #1 but removes the @bebkovan/server-core dependency. Possible future state, but not worth the effort today.

Consequences

  • New code should prefer the Nest-injected pino logger (constructor(private logger: Logger) from nestjs-pino). Those records land in Loki via OTLP with full resource attributes.
  • EvoLogger records reach Loki via a separate path (filelog/docker receiver, tagged source=docker_filelog). They lack service.name resource attributes and must be queried with {source="docker_filelog"}.
  • Two logger setups means two mental models. The glossary and observability doc explain the distinction.

References

  • libs/shared/src/logger/pino-logger.module.ts — NestLoggerModule
  • libs/shared/src/basic/pre/pre-otel.main.ts:78-82 — PinoInstrumentation registration
  • libs/shared/src/basic/base.main.ts — framework logger swap
  • docs/observability.md — full two-logger explanation