lenec ru

← все посты

strace для отладки: находим баги в проде за 5 минут

13K

Выбор логгера в Node.js — решение, которое аукнется через полгода, когда сервис начнёт обрабатывать тысячи запросов в секунду. В 2026 году два главных кандидата — Pino и Winston. Оба поддерживают structured logging, оба зрелые. Но разница в производительности и философии колоссальная. Разберёмся на бенчмарках и реальных конфигурациях, чтобы вы могли сделать осознанный выбор для своего проекта.

Зачем structured logging

Plaintext-логи вида User 42 logged in at 10:03 невозможно нормально парсить в ELK, Loki или Datadog. Structured logging — это JSON-строки с фиксированными полями: timestamp, level, msg, traceId. Любой агрегатор разбирает их без regex-костылей, а поиск по конкретному userId или traceId занимает миллисекунды вместо минут.

Преимущества structured logging на практике:

  • Корреляция запросов через traceId — один ID связывает логи от gateway до базы данных.
  • Алерты по конкретным полям: level == "error" AND service == "payments".
  • Метрики из логов: подсчёт ошибок, percentile latency — без отдельного инструментирования.
  • Фильтрация в реальном времени: показать только логи конкретного пользователя в проде.

Оба логгера умеют structured logging из коробки, но подходы разные:

  • Pino — JSON-first. Каждая строка — валидный JSON. Форматирование для человека — отдельный процесс (pino-pretty).
  • Winston — transport-first. Формат настраивается через цепочку format-функций, можно миксовать JSON и printf.

Бенчмарк: throughput в req/s

Стенд: Node.js 22.4, Fastify 5.x, 4 vCPU (c6i.xlarge), autocannon -c 100 -d 10. Каждый запрос пишет одну info-строку с 5 полями. Тест повторён 5 раз, берём медиану.

┌──────────┬────────────┬─────────────┬───────────┐
│ Logger   │ Req/s      │ Latency p99 │ Mem (RSS) │
├──────────┼────────────┼─────────────┼───────────┤
│ Pino     │ 48 200     │ 4.1 ms      │ 78 MB     │
│ Winston  │ 31 400     │ 7.3 ms      │ 112 MB    │
│ No logs  │ 52 100     │ 3.2 ms      │ 62 MB     │
└──────────┴────────────┴─────────────┴───────────┘

Pino быстрее на 53% по throughput. Причина — минимальная сериализация через fast-json-stringify и запись напрямую в stdout без промежуточных буферов. Winston проходит через цепочку transform-стримов, что добавляет аллокации и нагрузку на GC.

Важный нюанс: разрыв увеличивается при росте количества полей в лог-записи. Если вы логируете 15-20 полей на запрос (headers, body, response), Pino выигрывает уже в 2-2.5 раза.

Настройка Pino с транспортами

Pino 9.x вынес транспорты в worker_threads — основной event loop не блокируется даже при записи в файл или отправке в удалённый коллектор.

import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL ?? 'info',
  formatters: {
    level(label) {
      return { level: label };
    },
  },
  timestamp: pino.stdTimeFunctions.isoTime,
  transport: {
    targets: [
      {
        target: 'pino-pretty',
        options: { colorize: true },
        level: 'debug',
      },
      {
        target: 'pino/file',
        options: { destination: '/var/log/app/app.log', mkdir: true },
        level: 'info',
      },
      {
        target: 'pino-loki',
        options: {
          host: 'http://loki:3100',
          batching: true,
          interval: 5,
          labels: { app: 'my-service' },
        },
        level: 'warn',
      },
    ],
  },
});

// Дочерний логгер с контекстом запроса
export function createRequestLogger(traceId: string) {
  return logger.child({ traceId });
}

Ключевые моменты:

  • transport.targets — массив, каждый target работает в отдельном worker_thread.
  • pino-pretty только для dev (level: debug), в проде — чистый JSON в stdout.
  • child() — дешёвая операция без копирования, создаёт логгер с дополнительными полями.
  • formatters.level — заменяет числовой level на строковый (info вместо 30).

Настройка Winston с форматами

Winston 3.x строится на format pipeline — цепочке функций, трансформирующих log-entry перед записью. Это даёт максимальную гибкость, но за счёт производительности.

import winston from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file';

const { combine, timestamp, json, errors, printf, colorize } = winston.format;

// Кастомный формат для dev-окружения
const devFormat = printf(({ level, message, timestamp, ...meta }) => {
  const metaStr = Object.keys(meta).length ? JSON.stringify(meta) : '';
  return `${timestamp} [${level}] ${message} ${metaStr}`;
});

const logger = winston.createLogger({
  level: 'info',
  format: combine(
    errors({ stack: true }),
    timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
  ),
  transports: [
    // Dev — человекочитаемый вывод
    new winston.transports.Console({
      level: 'debug',
      format: combine(colorize(), devFormat),
    }),
    // Prod — JSON + ротация файлов
    new DailyRotateFile({
      filename: '/var/log/app/app-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      maxSize: '100m',
      maxFiles: '14d',
      format: json(),
    }),
  ],
});

// Дочерний логгер с контекстом
export const requestLogger = (traceId: string) =>
  logger.child({ traceId });

Сильные стороны Winston:

  • Огромная экосистема транспортов: Elasticsearch, MongoDB, Syslog, Slack, Telegram.
  • Гибкий format pipeline — фильтрация, редактирование, маскирование PII прямо в цепочке.
  • winston-daily-rotate-file — встроенная ротация без внешнего logrotate.
  • Exception и rejection handling из коробки — ловит uncaughtException и unhandledRejection.

Подводные камни

Pino:

  • Без pino-pretty JSON нечитаем в терминале — новичков это отпугивает.
  • Транспорты в worker_threads потребляют дополнительные ~15 MB RAM на каждый target.
  • Ошибки в transport-воркере не всплывают в основной процесс без явного обработчика on('error').
  • Сериализация circular-объектов требует отдельного плагина (pino-safe-stringify).

Winston:

  • Синхронная обработка format-цепочки в event loop — при сложных трансформациях latency растёт нелинейно.
  • transports.File без ротации может забить диск за считанные часы при высокой нагрузке.
  • Некоторые community-транспорты заброшены и не совместимы с Node 22+.
  • Дефолтный Console-транспорт медленнее, чем прямой process.stdout.write.

Когда что выбрать

Простое правило для принятия решения:

  • Pino — high-load API, микросервисы на Fastify/Koa, serverless (каждая миллисекунда = деньги). Логи уходят в централизованный коллектор (Loki, Datadog, CloudWatch), человекочитаемость в проде не нужна.
  • Winston — монолит с десятком интеграций, нужна ротация файлов на сервере, маскирование PII в format-цепочке, алерты в Slack/Telegram прямо из логгера. Или если команда привыкла к Winston и переезд экономически не оправдан.

Многие проекты начинают с Winston (проще для понимания, больше примеров на Stack Overflow), а переезжают на Pino когда упираются в throughput. Миграция несложная — API похожи, child() есть у обоих, structured-формат одинаковый. Главное — не откладывать structured logging на потом. JSON с первого дня экономит десятки часов дебага в будущем.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.