strace для отладки: находим баги в проде за 5 минут
Выбор логгера в 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 с первого дня экономит десятки часов дебага в будущем.