npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bymax-one/nest-logger

v1.0.0

Published

Structured JSON logging for NestJS based on Pino 10, with optional OpenTelemetry correlation.

Readme


✨ Overview

@bymax-one/nest-logger replaces ad-hoc console.log and legacy Winston setups with a production-grade structured logging pipeline. Every log entry is a JSON object carrying a logKey (MODULE_ACTION_RESULT convention), requestId, tenantId, and — when OpenTelemetry is active — the correlated traceId/spanId.

Why Pino? At ~750,000 logs/sec (vs Winston's ~110,000), Pino consumes 3× less CPU and half the RSS under load. The difference is measurable in production billing/payments backends where the logger is on the hot path for every request.

The library has zero direct dependencies — all packages arrive as peer dependencies, so you control exact versions and the supply chain surface stays minimal.


🔥 Features

  • Structured JSON — every entry has level, time, service, logKey, msg, and arbitrary metadata fields
  • PII redaction by default — 113 paths covering passwords, tokens, generic secrets, PCI DSS card data, MFA secrets, CPF/CNPJ/RG (LGPD), and common HTTP headers — powered by fast-redact
  • OpenTelemetry correlation — optional @opentelemetry/api peer; injects traceId/spanId/traceFlags into every log via a Pino mixin when an active span is detected
  • AsyncLocalStorage contextrequestId, tenantId, userId flow automatically through the request lifecycle without prop drilling
  • HTTP logging interceptor — auto-logs all HTTP requests/responses with URL normalization (UUIDs and numeric IDs replaced by :id placeholder)
  • Exception filter — captures NestJS HttpException and unexpected errors with structured output
  • Pluggable destinations — implement ILogDestination to ship logs to Loki, Postgres, rolling files, or any sink
  • NestJS LoggerService bridge — drop-in replacement; all NestJS internal logs flow through Pino
  • MODULE_ACTION_RESULT log key convention — enforced by an exported regex for CI validation
  • Pretty-print in dev — auto-enabled when NODE_ENV !== 'production' (requires optional pino-pretty)
  • Entry size guard — truncates oversized entries (default 64 KB) with a structured warning instead of silently dropping them
  • Zero-downtime destination lifecycleonInit() / onShutdown() hooks; a failing destination is removed without affecting others

📦 Subpath Exports

The package ships two entry points:

| Import path | Contents | Deps | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------- | | @bymax-one/nest-logger | Full server API: BymaxLoggerModule, PinoLoggerService, LogContextService, destinations, decorators, interceptors | NestJS + Pino peer deps | | @bymax-one/nest-logger/shared | Zero-dependency utilities: LOG_KEYS_CONVENTION_REGEX, RESERVED_LOG_KEYS, types (LogLevel, LogEntry, ServiceMetadata) | None |

The /shared subpath is safe to import in isomorphic code, test helpers, CLI scripts, or shared packages that must not pull in NestJS.


📥 Installation

# Install the library
pnpm add @bymax-one/nest-logger

# Required peer dependencies
pnpm add pino reflect-metadata

# Optional — pretty dev output
pnpm add -D pino-pretty

# Optional — OpenTelemetry correlation
pnpm add @opentelemetry/api @opentelemetry/sdk-node

🚀 Quick Start

Step 1 — Register the module

// app.module.ts
import { Module } from '@nestjs/common'
import { BymaxLoggerModule } from '@bymax-one/nest-logger'

@Module({
  imports: [
    BymaxLoggerModule.forRoot({
      service: { name: 'my-app', version: '1.0.0' },
      level: 'info',
      http: { isEnabled: true }
    })
  ]
})
export class AppModule {}

isPretty defaults to true when NODE_ENV !== 'production' — no extra config needed in development.

Step 2 — Async configuration with ConfigService

// app.module.ts
import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { BymaxLoggerModule } from '@bymax-one/nest-logger'

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    BymaxLoggerModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (cfg: ConfigService) => ({
        service: {
          name: cfg.getOrThrow<string>('OTEL_SERVICE_NAME'),
          version: cfg.getOrThrow<string>('RELEASE_SHA')
        },
        level: cfg.get('LOG_LEVEL') ?? 'info',
        http: { isEnabled: true }
      })
    })
  ]
})
export class AppModule {}

Step 3 — Inject the logger in a service

// payments.service.ts
import { Injectable } from '@nestjs/common'
import { InjectLogger, PinoLoggerService } from '@bymax-one/nest-logger'

@Injectable()
export class PaymentsService {
  constructor(
    @InjectLogger(PaymentsService.name)
    private readonly logger: PinoLoggerService
  ) {}

  async refund(paymentId: string, amount: number, requestedBy: string) {
    this.logger.info('PAYMENT_REFUND_REQUESTED', 'Refund requested', requestedBy, {
      paymentId,
      amount
    })

    try {
      const result = await this.stripe.refunds.create({ payment_intent: paymentId, amount })
      this.logger.info('PAYMENT_REFUND_SUCCESS', 'Refund processed', requestedBy, {
        paymentId,
        stripeRefundId: result.id
      })
      return result
    } catch (err) {
      this.logger.errorStructured(
        'PAYMENT_REFUND_FAILED',
        err instanceof Error ? err : new Error(String(err)),
        requestedBy,
        { paymentId, amount }
      )
      throw err
    }
  }
}

Output (JSON, production):

{
  "level": 30,
  "time": "2026-05-28T10:12:44.512Z",
  "service": { "name": "my-app", "version": "abc123" },
  "logKey": "PAYMENT_REFUND_SUCCESS",
  "msg": "Refund processed",
  "context": "PaymentsService",
  "requestId": "r_7f3a9b",
  "tenantId": "t_acme",
  "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
  "spanId": "00f067aa0ba902b7",
  "paymentId": "pi_xyz",
  "stripeRefundId": "re_abc"
}

Output (pretty-print, development):

[10:12:44.512] INFO (my-app): PAYMENT_REFUND_SUCCESS
    requestId: "r_7f3a9b"
    tenantId: "t_acme"
    paymentId: "pi_xyz"
    stripeRefundId: "re_abc"

Step 4 — HTTP logging (automatic)

Enable http.isEnabled: true in the module options. The HttpLoggingInterceptor is registered globally and emits:

| Log key | When | | --------------------------- | ------------------------------------- | | HTTP_REQUEST_START | Request received | | HTTP_REQUEST_SUCCESS | 2xx response | | HTTP_REQUEST_REDIRECT | 3xx response | | HTTP_REQUEST_CLIENT_ERROR | 4xx response | | HTTP_REQUEST_SERVER_ERROR | 5xx response | | HTTP_EXCEPTION_HANDLED | HttpException caught by the filter | | HTTP_EXCEPTION_UNHANDLED | Unexpected error caught by the filter |

URLs are automatically normalized — /users/550e8400-e29b-41d4-a716-446655440000 becomes /users/:id so Loki/Grafana cardinality stays bounded.

Step 5 — OpenTelemetry correlation

Initialize the OTel SDK before importing NestJS — this is critical:

// main.ts (top of file — before any NestJS import)
import { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
import { resourceFromAttributes } from '@opentelemetry/resources'
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'

const sdk = new NodeSDK({
  resource: resourceFromAttributes({
    [ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME ?? 'my-app',
    [ATTR_SERVICE_VERSION]: process.env.RELEASE_SHA ?? 'dev',
    'deployment.environment': process.env.NODE_ENV ?? 'development'
  }),
  traceExporter: new OTLPTraceExporter({ url: process.env.OTLP_TRACE_ENDPOINT }),
  instrumentations: [
    getNodeAutoInstrumentations({
      '@opentelemetry/instrumentation-fs': { isEnabled: false } // noisy
    })
  ]
})

sdk.start()

process.on('SIGTERM', () => {
  void sdk.shutdown().finally(() => process.exit(0))
})

// NestJS imports come AFTER sdk.start()
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { bufferLogs: true })
  await app.listen(3000)
}

void bootstrap()

Once active, every log entry automatically carries traceId, spanId, and traceFlags. Click the traceId in Grafana to jump directly to the correlated span in Tempo or Honeycomb.

Step 6 — Context propagation with LogContextService

// request-id.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common'
import { LogContextService } from '@bymax-one/nest-logger'
import { randomUUID } from 'node:crypto'

@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
  constructor(private readonly logContext: LogContextService) {}

  use(req: Request, res: Response, next: () => void) {
    const requestId = req.headers['x-request-id'] ?? `r_${randomUUID()}`
    this.logContext.run({ requestId }, next)
  }
}

Any log emitted inside the run() scope — regardless of nesting depth — automatically includes requestId with no prop drilling.


⚙️ Configuration

Full options reference for BymaxLoggerModule.forRoot(options):

Top-level options

| Option | Type | Default | Description | | ---------------------------- | -------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------ | | service.name | string | Required | Service name emitted in every log entry | | service.version | string | Required | Release version/SHA emitted in every log entry | | level | LogLevel | 'info' | Minimum log level. One of fatal \| error \| warn \| info \| debug \| trace | | isPretty | boolean | !isProduction | ⚠️ Reserved — not auto-wired. For pretty output add new PrettyDevDestination() to destinations (needs pino-pretty) | | redactPaths | string[] | [] | Additional fast-redact paths merged with the defaults | | shouldDisableDefaultRedact | boolean | false | Skip the 113 default PII paths. ⚠️ Emits a bootstrap warning — document why | | redactCensor | string \| function | '[REDACTED]' | Replacement value for redacted fields. A function receives the raw value | | maxEntrySizeBytes | number | 65536 | Entries larger than this are replaced by a structured LOGGER_ENTRY_TRUNCATED warning | | destinations | ILogDestination[] | [] | Additional sinks (Loki, Postgres, rolling file, …) alongside default stdout |

http options

| Option | Type | Default | Description | | ------------------------------ | ---------- | ------------------------------- | ------------------------------------------------------------------------------------- | | http.isEnabled | boolean | false | Register HttpLoggingInterceptor (and, on forRoot, HttpExceptionFilter) globally | | http.shouldCaptureExceptions | boolean | true | Capture unhandled HTTP exceptions and emit HTTP_EXCEPTION_UNHANDLED | | http.shouldGenerateRequestId | boolean | true | Generate a requestId when the inbound request header is absent | | http.excludePaths | RegExp[] | [/^\/health$/, /^\/metrics$/] | Paths that bypass HTTP logging. Use anchored, linear-time regexes (ReDoS-safe) | | http.tenantIdHeader | string | 'x-tenant-id' | Request header carrying the tenant identifier |

otel options

| Option | Type | Default | Description | | ----------------------------------- | ----------------------------- | ------------- | ------------------------------------------------------------------------ | | otel.shouldAutoInjectTraceContext | boolean | true | Detect @opentelemetry/api and inject traceId/spanId via Pino mixin | | otel.fieldFormat | 'camelCase' \| 'snake_case' | 'camelCase' | Field names in log entries: traceId/spanId vs trace_id/span_id |


🔐 PII Redaction

Default paths

The library ships 113 redact paths compiled at initialization into a single fast-redact function (< 3% throughput impact). These cover:

| Category | Fields | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | Passwords | password, passwordHash, passwordConfirm, newPassword, oldPassword | | Tokens | token, accessToken, refreshToken, idToken, apiKey, apiSecret | | MFA | mfaSecret, mfaRecoveryCodes, totpSecret | | Generic secrets | secret, clientSecret, signingSecret, privateKey | | Payment / PCI DSS | cardNumber, cardCvv, cvv, cvc, cardExpiry | | Personal documents (LGPD) | cpf, cnpj, rg | | Conservative PII | email | | HTTP headers (absolute) | req.headers.authorization, req.headers.cookie, req.headers["x-api-key"], req.headers["x-auth-token"], res.headers["set-cookie"] |

Every field is listed at wildcard depths 1–4 (*.field, *.*.field, *.*.*.field, *.*.*.*.field) because fast-redact's * matches a single level only — not recursive.

Extending the defaults

BymaxLoggerModule.forRoot({
  service: { name: 'my-app', version: '1.0.0' },
  redactPaths: [
    '*.internalSecret', // depth-1 wildcard
    'body.creditCard.*', // all fields inside a subobject
    'payload.user.taxId' // exact path
  ]
})

The extra paths are merged with the defaults — never replacing them.

Disabling defaults (not recommended)

BymaxLoggerModule.forRoot({
  service: { name: 'my-app', version: '1.0.0' },
  shouldDisableDefaultRedact: true, // ⚠️ emits LOGGER_BOOTSTRAP_WARNING
  redactPaths: ['*.password'] // you own the full list
})

A LOGGER_BOOTSTRAP_WARNING entry is emitted on startup so security reviews can audit when PII protection was intentionally reduced.


🔑 Log Key Convention

All structured log calls must use the MODULE_ACTION_RESULT format:

USER_LOGIN_SUCCESS         AUTH_REGISTER_FAILED
PAYMENT_REFUND_PROCESSED   WEBHOOK_STRIPE_RECEIVED
HTTP_REQUEST_CLIENT_ERROR  METHOD_SLOW_EXECUTION

The regex is exported from the /shared entry for CI validation:

import { LOG_KEYS_CONVENTION_REGEX } from '@bymax-one/nest-logger/shared'

function assertValidLogKey(key: string) {
  if (!LOG_KEYS_CONVENTION_REGEX.test(key)) {
    throw new Error(`Invalid log key: "${key}". Expected MODULE_ACTION_RESULT format.`)
  }
}

Reserved keys

The following keys are used internally by the library — do not reuse them in application code:

LOGGER_BOOTSTRAP_OK · LOGGER_BOOTSTRAP_WARNING · LOGGER_SHUTDOWN_OK · HTTP_REQUEST_START · HTTP_REQUEST_SUCCESS · HTTP_REQUEST_REDIRECT · HTTP_REQUEST_CLIENT_ERROR · HTTP_REQUEST_SERVER_ERROR · HTTP_REQUEST_COMPLETED · HTTP_EXCEPTION_HANDLED · HTTP_EXCEPTION_UNHANDLED · METHOD_EXECUTION · METHOD_SLOW_EXECUTION · LOGGER_DESTINATION_INIT_FAILED · LOGGER_DESTINATION_WRITE_FAILED · LOGGER_ENTRY_TRUNCATED

All reserved keys are exported as the RESERVED_LOG_KEYS constant from @bymax-one/nest-logger/shared.


🧩 Custom Destinations

Implement ILogDestination to ship logs to any sink:

import type { ILogDestination } from '@bymax-one/nest-logger'
import type { LogEntry } from '@bymax-one/nest-logger/shared'

export class LokiDestination implements ILogDestination {
  readonly name = 'loki'
  readonly minLevel = 'info' as const

  private readonly url: string
  private readonly headers: Record<string, string>
  private readonly labels: Record<string, string>
  private buffer: LogEntry[] = []
  private flushTimer?: NodeJS.Timeout

  constructor(opts: {
    url: string
    username: string
    password: string
    labels: Record<string, string>
  }) {
    this.url = `${opts.url}/loki/api/v1/push`
    this.labels = opts.labels
    const credentials = Buffer.from(`${opts.username}:${opts.password}`).toString('base64')
    this.headers = {
      'Content-Type': 'application/json',
      Authorization: `Basic ${credentials}`
    }
  }

  async onInit(): Promise<void> {
    this.flushTimer = setInterval(() => void this.flush(), 5_000)
  }

  async onShutdown(): Promise<void> {
    clearInterval(this.flushTimer)
    await this.flush()
  }

  write(entry: LogEntry): void {
    this.buffer.push(entry)
    if (this.buffer.length >= 100) void this.flush()
  }

  private async flush(): Promise<void> {
    if (this.buffer.length === 0) return
    const batch = this.buffer.splice(0)
    const body = JSON.stringify({
      streams: [
        {
          stream: this.labels,
          values: batch.map((e) => [String(BigInt(e.time) * 1_000_000n), JSON.stringify(e)])
        }
      ]
    })
    await fetch(this.url, { method: 'POST', headers: this.headers, body })
  }
}

Then pass it via destinations:

BymaxLoggerModule.forRootAsync({
  inject: [ConfigService],
  useFactory: (cfg: ConfigService) => ({
    service: { name: cfg.getOrThrow('OTEL_SERVICE_NAME'), version: cfg.getOrThrow('RELEASE_SHA') },
    level: 'info',
    destinations: [
      new LokiDestination({
        url: cfg.getOrThrow('LOKI_URL'),
        username: cfg.getOrThrow('LOKI_USER'),
        password: cfg.getOrThrow('LOKI_PASSWORD'),
        labels: { service: cfg.getOrThrow('OTEL_SERVICE_NAME') }
      })
    ]
  })
})

Postgres destination (Prisma)

import type { ILogDestination } from '@bymax-one/nest-logger'
import type { LogEntry } from '@bymax-one/nest-logger/shared'
import type { PrismaClient } from '@prisma/client'

export class PrismaLogDestination implements ILogDestination {
  readonly name = 'prisma-postgres'
  readonly minLevel = 'warn' as const // only persist warnings and above

  constructor(private readonly prisma: PrismaClient) {}

  write(entry: LogEntry): void {
    void this.prisma.applicationLog.create({
      data: {
        level: entry.level,
        logKey: entry.logKey,
        message: entry.msg,
        payload: entry,
        createdAt: new Date(entry.time)
      }
    })
  }
}

Rolling file destination (pino-roll)

import { createStream } from 'pino-roll'
import type { ILogDestination } from '@bymax-one/nest-logger'
import type { LogEntry } from '@bymax-one/nest-logger/shared'

export class RollingFileDestination implements ILogDestination {
  readonly name = 'rolling-file'
  private stream?: Awaited<ReturnType<typeof createStream>>

  async onInit(): Promise<void> {
    this.stream = await createStream('logs/app.log', {
      frequency: 'daily',
      mkdir: true,
      size: '50m'
    })
  }

  async onShutdown(): Promise<void> {
    await new Promise<void>((resolve) => this.stream?.end(resolve))
  }

  write(entry: LogEntry): void {
    this.stream?.write(JSON.stringify(entry) + '\n')
  }
}

🏗️ Architecture

HTTP Request
    │
    ▼
HttpLoggingInterceptor          ← emits HTTP_REQUEST_START
    │
    ▼
RequestIdMiddleware             ← opens AsyncLocalStorage scope
    │                              { requestId, tenantId, userId }
    ▼
Application Service
    │
    └── PinoLoggerService.info(logKey, msg, context, metadata)
              │
              ▼
         Pino logger
              │
         composedMixin()         ← runs per-log (O(1))
              ├── ALS store      → { requestId, tenantId, userId }
              └── OTel span      → { traceId, spanId, traceFlags }
              │
              ▼
         fast-redact             ← compiled redact function (113 paths)
              │
              ▼
    ┌─────────────────────────────┐
    │  DefaultStdoutDestination   │  ← always active
    │  LokiDestination            │  ← optional
    │  PrismaLogDestination       │  ← optional
    │  RollingFileDestination     │  ← optional
    └─────────────────────────────┘

Key design decisions:

  • Singleton scope, not Scope.REQUESTAsyncLocalStorage delivers per-request context at zero latency overhead. NestJS request scope adds ~5% latency on the injection graph; unacceptable on a logger that runs every request.
  • One Pino mixin — ALS context and OTel trace context are composed into a single mixin. Merge order is deterministic: ALS fields first, then OTel (OTel wins on name conflicts because an active span is the authoritative trace identity).
  • Zero direct dependencies — the library ships with "dependencies": {}. All packages arrive as peer dependencies so consumers control exact versions.
  • fast-redact compiled at init — 113 paths compile once at module bootstrap into a specialized JS function. No per-log regex matching.

📖 API Reference

PinoLoggerService

Implements NestJS LoggerService. All methods are available via @InjectLogger().

| Method | Signature | Notes | | ----------------- | ---------------------------------- | -------------------------------------- | | info | (logKey, msg, context?, meta?) | Structured info log | | warn | (logKey, msg, context?, meta?) | Structured warn log | | debug | (logKey, msg, context?, meta?) | Structured debug log | | error | (logKey, msg, context?, meta?) | Structured error log (string message) | | fatal | (logKey, msg, context?, meta?) | Structured fatal log | | warnStructured | (logKey, error, context?, meta?) | Warn with Error object (serialized) | | errorStructured | (logKey, error, context?, meta?) | Error with Error object (serialized) | | fatalStructured | (logKey, error, context?, meta?) | Fatal with Error object (serialized) | | log | (msg, context?) | NestJS LoggerService bridge → info | | verbose | (msg, context?) | NestJS bridge → trace |

LogContextService

| Method | Signature | Description | | ---------- | ------------------- | --------------------------------------------------------------------------- | | run | (store, callback) | Opens an ALS scope. Any log inside callback carries store fields | | set | (key, value) | Adds/updates a field in the current scope. Throws if called outside run() | | getStore | () | Returns current scope or undefined outside a run() call |

Decorators

| Decorator | Target | Description | | ------------------------------- | ----------------- | --------------------------------------------------------------------------------------------- | | @InjectLogger(context?) | Constructor param | Injects PinoLoggerService pre-bound to the given context string | | @LogContext(store) | Method | Wraps the method in logContext.run(store, ...) — all downstream logs carry the given fields | | @LogPerformance(thresholdMs?) | Method | Logs METHOD_EXECUTION on completion; METHOD_SLOW_EXECUTION if duration exceeds threshold |

Types (from @bymax-one/nest-logger/shared)

type LogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace'

type ServiceMetadata = { name: string; version: string }

type LogEntry = {
  level: number
  time: number
  service: ServiceMetadata
  logKey: string
  msg: string
  [key: string]: unknown
}

ILogDestination

interface ILogDestination {
  readonly name: string
  readonly minLevel?: LogLevel
  write(entry: LogEntry): void
  onInit?(): Promise<void>
  onShutdown?(): Promise<void>
}

🚨 Error Code Catalog

| Code | Severity | When | Action | | --------------------------------- | ----------------- | -------------------------------------------------------- | --------------------------------------------------------------------- | | LOGGER_INVALID_OPTIONS | Throws at init | service.name or service.version missing | Add the required fields | | LOGGER_INVALID_LEVEL | Throws at init | level is not a valid Pino value | Use 'fatal'\|'error'\|'warn'\|'info'\|'debug'\|'trace' | | LOGGER_PRETTY_UNAVAILABLE | Warn on bootstrap | isPretty: true but pino-pretty not installed | Install pino-pretty or set isPretty: false | | LOGGER_OTEL_API_UNAVAILABLE | Info on bootstrap | OTel mixin active but @opentelemetry/api not installed | Install the package or set otel.shouldAutoInjectTraceContext: false | | LOGGER_DESTINATION_INIT_FAILED | Error (logged) | destination.onInit() rejects | Destination is removed; other destinations continue | | LOGGER_DESTINATION_WRITE_FAILED | Warn (logged) | destination.write() throws | Entry skipped for that destination; others continue | | LOGGER_CONTEXT_OUT_OF_SCOPE | Throws | LogContextService.set() called outside run() | Wrap in logContext.run({ ... }, () => ...) | | LOGGER_ENTRY_TRUNCATED | Warn (meta-log) | Entry exceeds maxEntrySizeBytes | Reduce metadata or raise the limit |


🪜 Level Mapping

| Pino | Pino string | NestJS | Typical use | | ---- | ----------- | --------- | ----------------------------------------- | | 60 | fatal | — | Process about to exit | | 50 | error | error | Failure requiring human attention | | 40 | warn | warn | Recoverable anomaly | | 30 | info | log | Significant business events | | 20 | debug | debug | Implementation detail for troubleshooting | | 10 | trace | verbose | Ultra-granular (rarely in prod) |


🧪 Testing & Quality

pnpm test              # unit tests (Jest)
pnpm test:cov          # coverage report
pnpm test:e2e          # end-to-end tests (supertest)
pnpm test:cov:all      # full coverage gate (100% statements/branches/functions/lines)
pnpm mutation          # Stryker mutation testing (95% break gate)
pnpm typecheck         # tsc strict check (all tsconfig variants)
pnpm lint              # ESLint

Coverage gate: 100% on statements, branches, functions, and lines. Mutation: 97.42% current vs the 95% break gate (Stryker thresholds.break: 95); 97.42% is the theoretical maximum — all 10 surviving mutants are documented equivalents. See mutation_testing_results.md.


🧱 Tech Stack

Pino NestJS TypeScript Node.js Jest Stryker pnpm tsup


⚡ Performance

Pino official benchmarks (Node 24, 100k logs):

| Logger | Logs/sec | Avg latency | RSS | CPU | | ---------------------- | ------------ | ----------- | ---------- | ------ | | Pino 10 (JSON) | ~750,000 | 1.3 µs | ~45 MB | 8% | | Pino 10 (pretty) | ~120,000 | 8 µs | ~50 MB | 14% | | Winston 3 (JSON) | ~110,000 | 9 µs | ~85 MB | 22% | | Winston 3 (transports) | ~75,000 | 13 µs | ~95 MB | 28% |

At high throughput (~100k req/s), Pino consumes 3× less CPU and ~2× less RSS than Winston.


🚫 What This Library Does NOT Do

By design, the following are out of scope:

  • OTel SDK initialization — consumer's responsibility in main.ts
  • Custom metrics (OTel Metrics API) — use @opentelemetry/api directly
  • Alerting (PagerDuty, Slack) — configure your OTLP backend (Grafana Alertmanager)
  • Log aggregation UI — Grafana, Datadog, Honeycomb
  • Sentry SDK — install @sentry/node in the consumer if desired
  • Immutable audit logs — future @bymax-one/nest-audit
  • File rotation built-in — use RollingFileDestination with pino-roll
  • pino-http — the HttpLoggingInterceptor is a NestJS-native replacement; install pino-http independently if you prefer it (produces duplicate logs if both are active)

🤝 Contributing

This library is part of the @bymax-one monorepo. Development follows the Bymax coding standards:

  • TypeScript strict (noImplicitAny, exactOptionalPropertyTypes, noUncheckedIndexedAccess)
  • 100% test coverage gate
  • Conventional Commits enforced by commitlint + husky
  • No direct dependencies — peer deps only
  • All boolean identifiers prefixed with is / has / should / can
  • All log keys in MODULE_ACTION_RESULT format
# Clone the repository
git clone https://github.com/bymaxone/nest-logger.git
cd nest-logger

# Install dependencies
pnpm install

# Run tests
pnpm test

# Build
pnpm build

# Type check
pnpm typecheck

🔒 Security Policy

If you discover a security vulnerability, please do not open a public issue. Instead, email us at [email protected] with details. We take security seriously and will respond promptly.


📄 License

MIT © Bymax One