Логирование в Node.js: console.log, Winston или Pino
console.log отъедает CPU под нагрузкой и не даёт структуры для поиска. Разбираем, чем платят Winston, Pino и Bunyan, и когда какой выбрать.

На проде под нагрузкой синхронный console.log становится узким местом: запись в stdout блокирует event loop, а неструктурированные строки невозможно нормально искать в Elasticsearch или Loki. При этом менять всё разом на тяжёлый логгер тоже цена — лишние зависимости, свой формат, кривая обучения. Ниже — четыре подхода к логированию в Node.js (проверялось на Node.js 20.x LTS), у каждого своя цена в производительности, читаемости и интеграции с инфраструктурой.
Задача и почему console.log не тянет
Логгер на боевом сервисе решает три задачи одновременно: писать быстро и не блокировать обработку запросов, давать структуру для машинного поиска и уметь разделять уровни (debug/info/warn/error), чтобы в проде не сыпался мусор из отладки.
console.log проваливает все три. Он пишет в stdout синхронно, когда вывод идёт в файл или пайп1. Формат — произвольная строка, поэтому grep по «userId» работает, а фильтр «все ошибки за последние 5 минут по конкретному пользователю» — нет. Уровней нет вообще: вы сами городите if (DEBUG).
Правило простое: если логи кто-то читает глазами в терминале на своей машине — console.log хватает. Как только логи уезжают в сборщик и по ним строят алерты — нужна структура и асинхронность.
Четыре подхода
1. console.log
Встроен, ноль зависимостей. Годится для скриптов, локальной отладки и очень маленьких сервисов, где логи никто не агрегирует.
Цена: синхронная запись блокирует event loop при выводе в файл; нет уровней; нет структуры; при высокой частоте логов заметно проседает пропускная способность.
2. Pino
Делает ставку на скорость. Пишет JSON, сериализацию выносит максимально дёшево, а тяжёлое форматирование (человекочитаемый вывод) отдаёт отдельному процессу-транспорту, чтобы не грузить основной поток.
// pino 9.x
import pino from 'pino'
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
})
// структурированный лог: первым идёт объект, вторым — сообщение
logger.info({ userId: 42, route: '/checkout' }, 'заказ оформлен')
logger.error({ err: new Error('таймаут БД') }, 'сбой при записи заказа')
По умолчанию Pino отдаёт компактный JSON в stdout — это то, что нужно для контейнеров: сборщик логов подхватывает stdout сам. Для читаемого вывода локально ставят pino-pretty и подключают через transport, а не парсят строки вручную.
3. Winston
Самый гибкий по транспортам: файлы с ротацией, HTTP, консоль, сторонние сервисы — всё через единый интерфейс transports и format. За гибкость платите скоростью и объёмом настройки.
// winston 3.x
import winston from 'winston'
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'app.log' }),
],
})
logger.info('заказ оформлен', { userId: 42, route: '/checkout' })
Обратите внимание: в Winston метаданные идут вторым аргументом, в Pino — первым. Это частый источник путаницы при миграции между ними.
4. Bunyan
Исторически один из первых структурированных JSON-логгеров, идеолог формата, который во многом повторил Pino. Активность разработки заметно ниже, чем у Pino и Winston. Если проект не на нём с самого начала — начинать новый на Bunyan сейчас смысла мало, поэтому подробно код не приводим.
Сравнение
| Критерий | console.log | Pino | Winston |
|---|---|---|---|
| Формат | строка | JSON | настраиваемый (JSON и др.) |
| Уровни логов | нет | да | да, настраиваемые |
| Скорость | низкая при выводе в файл | высокая | ниже Pino |
| Транспорты | только stdout/stderr | через отдельный процесс | много встроенных |
| Зависимости | ноль | лёгкая | тяжелее |
| Кривая настройки | нулевая | низкая | средняя |
Цифры производительности сильно зависят от объёма сообщения, уровня и того, куда идёт вывод. Не берите чужие бенчмарки как истину: замерьте на своей нагрузке — прогоните типичный поток логов через autocannon или простой скрипт, который пишет фиксированное число сообщений, и сравните время и загрузку CPU. Условия замера (Node.js версия, размер объекта, вывод в pipe или в файл) фиксируйте рядом с результатом.
Когда что выбирать
- console.log — CLI-утилиты, разовые скрипты, прототип на выходные. Логи читаются глазами, не агрегируются.
- Pino — сервисы под нагрузкой, контейнеры, где логи пишутся в stdout и собираются платформой. Приоритет — скорость и структура.
- Winston — нужен один логгер, который сам раскидывает записи по нескольким назначениям (файл + внешний сервис + консоль) с разным форматом, и производительность не критична.
Критерий короткий: если вы деплоите в контейнер и лог уходит в stdout — берите Pino, пусть транспортом занимается платформа. Если приложение само отвечает за доставку логов в несколько мест — Winston окупает свою тяжесть.
Что ломается на практике
- Логирование объекта ошибки как строки.
logger.info('ошибка: ' + err)теряет stack trace. Передавайте ошибку отдельным полем: в Pino —{ err }с сериализатором ошибок, в Winston — через соответствующий формат. Иначе в проде увидите[object Object]или голое сообщение без стека. - Логи с секретами. Если писать весь объект запроса, в лог утекут токены и пароли. Настройте маскирование полей (в Pino — опция
redact) и не логируйте тело запросов целиком. - Слишком подробный уровень в проде.
debugна боевом сервисе под нагрузкой генерирует гигабайты в час и роняет диск. Уровень задавайте через переменную окружения, дефолт —info. - Синхронный вывод в файл. Прямая запись логов в файл из процесса блокирует event loop. В контейнере пишите в stdout, доставку и ротацию отдавайте инфраструктуре.
1 Event loop — цикл обработки событий Node.js, через который проходят все асинхронные операции. Синхронная запись в файл останавливает его на время записи, и в этот момент сервис не обрабатывает другие запросы.
Что делать дальше
Выберите логгер под сценарий доставки, а не по популярности. Дальше настройте: единый уровень через переменную окружения, маскирование чувствительных полей, единый формат JSON для всех сервисов и корреляционный ID запроса, чтобы связывать логи одного обращения. Мерьте влияние на пропускную способность на своей нагрузке до и после — под нагрузкой логгер способен съесть заметную долю CPU, и это видно только в замере, а не в документации.
Поиск источников: Реальные статьи и книги со ссылками, библиография по ГОСТ
Спросить за 15 ₽Источники: Pino — официальная документация, Winston — репозиторий на GitHub
Частые вопросы
А если у меня старая версия — Winston 2 или Pino 6?
Логи не пишутся в файл, что проверить?
Можно ли смешивать console.log и логгер в одном проекте?
Насколько Pino реально быстрее Winston?
Что делать с ротацией логов?
Как не залогировать пароли и токены?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.