Перейти к содержимому
Поиск
Разработка

Pino: логирование в Node.js — мастерская и 8 практических опытов

Структурированные логи Node.js на практике: восемь настоящих опытов Pino, редактор событий, requestId, ошибки, скрытие полей и JSONL. Исходники, Windows/Linux и запуск на VPS.

Александр Некрасов
Pino — структурированные логи Node.js, уровни и контекст запроса

Pino для Node.js: уровни, requestId, ошибки, redaction, JSONL и журнал HTTP-запросов. Восемь настоящих опытов, живой редактор событий и исходники для Windows, Linux и VPS.

Что такое Pino и что получит ваш Node.js-проект

Когда приложение обслуживает несколько пользователей одновременно, строка «ошибка запроса» почти ничего не объясняет. Нужны маршрут, идентификатор операции, код результата и время выполнения. Pino — библиотека структурированного логирования для Node.js: каждое событие можно записать как JSON, связать с контекстом запроса и затем найти по конкретным полям.

Это полезно для API, фоновых заданий, импорта файлов, уведомлений и интеграций. Вместо поиска случайных фрагментов текста вы можете выбрать ошибки одного requestId, увидеть попытки конкретного задания и отличить ожидаемый отказ от сбоя внешнего сервиса. При этом логирование не должно превращаться в бесконтрольную запись всего запроса и содержимого базы.

В статье используется Pino 10.4.0. Подготовлены восемь настоящих Node.js-опытов, браузерный редактор событий, локальный HTTP-сервер и исходники с командами для Windows и Linux. Можно посмотреть результат, скачать JSONL и код, а затем повторить те же операции у себя. Исходники библиотеки и её лицензия доступны в официальном репозитории Pino.

Где применять: API, очереди, импорты и интеграции

В API журнал помогает ответить, какой запрос завершился ошибкой, сколько времени занял и какие этапы прошёл. В очереди задач важны jobId, номер попытки и результат. В импорте — имя операции, количество обработанных строк и причина остановки. В интеграции — безопасный идентификатор обращения, статус внешнего сервиса и выбранная стратегия повтора.

Например, пользователь сообщает, что отчёт не появился. По requestId можно найти создание задания, по jobId — его исполнение, а затем увидеть timeout или успешно сформированный файл. Такая цепочка полезнее одной общей строки «что-то пошло не так». Но для неё нужно заранее согласовать поля и передавать контекст между компонентами приложения.

Pino отвечает за создание и вывод событий. Поиск по месяцу истории, хранение на другом сервере, графики и уведомления об ошибках требуют дополнительных инструментов. Для небольшого VPS достаточно stdout и journald; для нескольких сервисов может понадобиться централизованный сбор. Не начинайте с большой инфраструктуры, если пока не определено, какие события реально помогают сопровождать ваш продукт.

Pino и console.log: понятное сравнение

Console подходит для быстрой локальной проверки значения. Можно вручную вывести JSON и через console.log, но пороги, контекст, сериализацию ошибок и единый формат придётся согласовывать самостоятельно. Pino предоставляет для этого общие правила, чтобы разные части приложения не создавали несовместимые записи.

Задача console.log / console.error Pino
Быстро увидеть значение Очень удобно в разработке Можно, но требуется подключение
Единые поля события Формат задаёт каждый вызов JSON и общая конфигурация
Порог подробности Нужна собственная обвязка level и методы уровней
Контекст запроса Передавать вручную Child logger
Известные секретные поля Скрывать самостоятельно Redaction по заданным путям
Доставка и хранение Настраивает окружение Тоже требуют отдельного решения

Выбор не сводится к обещанию «будет быстрее во столько-то раз». В этой мастерской производительность production не измеряется. На результат влияют объём записи, сериализаторы, носитель, транспорт и нагрузка приложения. Практическая причина внедрения здесь — управляемый формат и возможность уверенно связать события одной операции. Для PHP аналогичную роль мы разбирали в статье о Monolog.

Мини-мастерская: результаты Node.js и живой редактор

В верхней части выберите один из восьми опытов. Вы увидите исходник и результат настоящего запуска Pino в Node.js: пороги, child logger, redaction, Error, сериализатор, mixin, два потока и файловую запись. Фильтр «Показать записи от уровня» меняет только отображение уже полученного результата; он не перезапускает Node и не меняет исходные события.

Нижний редактор вызывает настоящий pino/browser. Измените порог логгера, уровень события, сообщение и JSON, затем добавьте запись. Так можно непосредственно увидеть, почему debug не проходит при пороге info. События остаются в памяти вкладки. Серверные destination, multistream, transport и redaction в этом редакторе не выполняются; не вводите реальные пароли и токены.

Для воспроизводимой записи Node-опытов отключены timestamp, PID и hostname, а стек учебной ошибки задан явно. Скачиваемые короткие примеры используют обычные настройки Pino: при запуске у вас эти поля и пути могут отличаться. Команда verify в проекте сравнивает новую запись тех же опытов с опубликованными данными. Это проверяемый результат, а не изображение терминала.

Первая запись и устройство JSONL

Типичный вызов передаёт объект с полями первым аргументом и короткое сообщение вторым. Поля пригодны для фильтрации, сообщение помогает человеку быстро понять событие. Например, orderId и status лучше хранить отдельно, чем прятать их в длинной строке с произвольной пунктуацией.

import pino from 'pino';

const log = pino({level: 'info'});
log.info({orderId: 42, status: 'ready'}, 'Заказ подготовлен');

В Node обычный вывод Pino представляет собой JSONL: один JSON-объект на строку. Это не единый массив с квадратными скобками вокруг всего файла. Такой формат удобно дописывать и читать потоком, не загружая весь журнал в память. Переносы строк внутри строкового поля кодируются, поэтому сообщение с несколькими строками не должно ломать границы записей.

Не добавляйте в тот же stdout произвольные console.log рядом с JSONL, если downstream ожидает только JSON. Служебные сообщения CLI лучше направлять в stderr. При сохранении вывода используйте node src/cli.mjs напрямую: команда npm run может добавить свой заголовок запуска. Формат аргументов и служебных полей описан в API Pino.

Опыт 1. Уровни и порог подробности

В Pino используются trace=10, debug=20, info=30, warn=40, error=50 и fatal=60. Порог info пропускает info и более серьёзные уровни, но не debug и trace. В первом опыте вызываются debug, info и warn, а в результате остаются две записи. Это фильтрация при создании события, поэтому пропущенную диагностическую строку нельзя получить последующим фильтром просмотра.

const log = pino({level: 'info'});
log.debug({cache: 'miss'}, 'Диагностика');
log.info({orderId: 42}, 'Заказ принят');
log.warn({retryIn: 5}, 'Повтор позже');

Договоритесь о значении уровней в проекте. Info — обычные значимые этапы, warn — ситуация, которую приложение пока обработало, error — неуспешная операция. Debug полезен для подробностей во время диагностики. Если каждое ожидаемое отсутствие записи становится error, важные сбои потеряются среди шума; если всё записывается как info, сложнее выделять проблемы.

Fatal обозначает критическое событие, но сам вызов log.fatal не заменяет политику завершения процесса. Значение silent отключает вывод и является порогом, а не отдельным методом события. В редакторе выберите silent и отправьте fatal: запись не появится. Для временного debug на сервере заранее определите, кто его включает, на какой срок и какой объём данных допустим.

Опыт 2. Child logger и requestId

Второй опыт создаёт дочерний логгер для одного запроса. Его записи получают одинаковые requestId и route без повторения этих полей в каждом вызове. Начало и завершение можно связать, даже если между ними в общий поток попали события десятков других запросов.

const log = pino();
const requestLog = log.child({requestId: 'req-42', route: '/orders'});
requestLog.info('Запрос начат');
requestLog.info({statusCode: 200}, 'Запрос завершён');

В HTTP-примере идентификатор генерируется сервером и возвращается в заголовке x-request-id. Пользователь может сообщить его поддержке, не отправляя всё содержимое запроса. Внутри сервиса тот же child logger передаётся в функции обработки. Для фонового задания полезно сохранить связь между исходным запросом и jobId, но не копировать весь пользовательский объект в каждую строку.

Не принимайте без проверки чужие поля в bindings: пользовательский объект может конфликтовать со служебными level, time или msg. В мастерской введённые JSON-поля вложены в payload, а requestId задаётся отдельно. Для дочерних логгеров и возможных совпадений имён есть отдельная документация child loggers.

Опыт 3. Redaction: скрываем известные пути

В третьем опыте заранее перечислены token, user.password и user.email. Pino заменяет их на [HIDDEN], сохраняя orderId. Это удобно, когда структура события известна и некоторые поля нужно исключить из диагностического вывода. Можно также выбрать удаление поля, если важно не сохранять даже признак его наличия.

const log = pino({
  redact: {
    paths: ['token', 'user.password', 'user.email'],
    censor: '[HIDDEN]'
  }
});
log.info({token: 'demo-token', user: {
  password: 'demo-password', email: 'reader@example.test'
}, orderId: 42}, 'Заказ принят');

Redaction работает с заданными путями и учитывает регистр. Она не ищет автоматически все секреты внутри произвольного текста msg. Если разработчик собрал строку «пароль: …» вручную, правило user.password её не исправит. Поэтому основная стратегия — не писать лишние данные; маскирование служит дополнительной защитой известных полей, а не разрешением логировать всё.

Пути маскирования задавайте в доверенной конфигурации приложения, не принимайте их напрямую из запроса пользователя. Проверяйте и обычную запись, и сериализованные исключения: message и stack внешней библиотеки могут содержать параметры обращения. Подробности синтаксиса находятся в официальном разделе redaction.

Опыт 4. Ошибка как объект: тип, сообщение и стек

Если записать только err.message, теряются тип ошибки и место возникновения. В четвёртом опыте Error передаётся под ключом err, а рядом указывается операция loadCatalog. В результате есть структурированный объект err и короткое человеческое msg. Это помогает отличить причину от пояснения уровня приложения.

try {
  throw new Error('Учебный API недоступен');
} catch (err) {
  log.error({err, operation: 'loadCatalog'}, 'Каталог не получен');
}

В сохранённой записи мастерской стек специально приведён к стабильному учебному виду, чтобы результат не зависел от пути на компьютере автора. При скачивании и выполнении примера вы получите настоящий стек текущего запуска. Это различие не меняет структуру данных: type, message и stack остаются отдельными свойствами.

Логирование исключения не означает, что оно обработано. После записи решите, какой результат вернуть пользователю, нужен ли повтор и может ли процесс продолжать работу. Не отправляйте внутренний stack в HTTP-ответ обычному посетителю. В нашем сервере клиент получает короткий error и requestId, а технические детали остаются в журнале процесса.

Опыт 5. Сериализатор: разрешённые поля вместо всего объекта

У заказа могут быть служебные заметки, персональные данные, вложения и большие коллекции. Для сопровождения операции часто достаточно id, status и totalMinor. Сериализатор пятого опыта выбирает именно эти поля и не переносит privateNote в журнал. Это уменьшает объём и делает формат устойчивее к изменениям внутреннего объекта.

const log = pino({
  serializers: {
    order: value => ({
      id: value.id,
      status: value.status,
      totalMinor: value.totalMinor
    })
  }
});
log.info({order: {
  id: 42, status: 'paid', totalMinor: '12500', privateNote: 'demo-only'
}}, 'Изменение заказа');

Здесь totalMinor — строковое учебное значение в минимальных единицах, а не вычисление денежных сумм внутри логгера. Журнал фиксирует результат приложения, но не должен незаметно менять бизнес-данные. При появлении нового поля в заказе оно не попадёт в запись автоматически: разработчик сначала решит, действительно ли оно нужно для диагностики.

Сериализатор должен быть небольшим и предсказуемым. Сетевой запрос или тяжёлое вычисление в нём превращает запись лога в дополнительную работу на пути каждого события. Также проверяйте неожиданные входные значения: если функция безусловно читает свойства null, ошибка логирования может скрыть исходную проблему. Контракт конкретного примера предполагает корректный объект заказа.

Опыт 6. Mixin и контекст фоновой задачи

Mixin может добавлять поля в каждую запись логгера. В шестом опыте это jobId и attempt для одного запуска импорта. Две строки показывают прогресс обработки и используют одинаковый контекст. Читателю не нужно извлекать идентификатор из сообщения, а обработчик журнала может искать по точным полям.

const job = {id: 'import-17', attempt: 2};
const log = pino({
  mixin: () => ({jobId: job.id, attempt: job.attempt})
});
log.info({imported: 3}, 'Пакет импортирован');
log.info({imported: 7}, 'Импорт завершён');

У этой техники есть важная граница: общий изменяемый объект currentJob для всех параллельных запросов способен смешать контексты. В примере job принадлежит одному запуску. Для обычного requestId чаще проще использовать child logger и передавать его явно. Более сложная асинхронная привязка требует отдельного проектирования и проверки конкурентных операций.

Если задача повторяется, сохраняйте постоянный jobId и отдельный номер попытки. Тогда видно, завершилась ли работа после первого отказа или породила несколько независимых действий. Для очередей это хорошо сочетается с BullMQ и его повторами. Само наличие строки в логе не заменяет сохранённый статус задания в очереди.

Опыт 7. Два потока и осознанное дублирование ошибок

Multistream направляет события нескольким получателям. В опыте обычные записи идут в общий поток, а error дополнительно попадает в аварийный. Поэтому одна ошибка намеренно присутствует в двух результатах. Это не ошибка библиотеки, а выбранная схема маршрутизации, которую нужно учитывать при подсчёте числа событий.

const streams = pino.multistream([
  {level: 'info', stream: process.stdout},
  {level: 'error', stream: pino.destination({dest: 'errors.jsonl', sync: true})}
]);
const log = pino({level: 'info'}, streams);
log.info('Сервис готов');
log.error({code: 'DEMO_TIMEOUT'}, 'Учебный timeout');

Порог самого логгера применяется до порогов получателей. Если logger.level равен info, поток с уровнем debug не получит уже отброшенную диагностику. Начните с минимального нужного уровня у логгера, затем определите требования каждой стороны. Иначе конфигурация выглядит правильной, а часть ожидаемых событий отсутствует.

В сохранённой сцене потоки собираются в памяти, чтобы их можно было сравнить на странице. Скачиваемый пример показывает stdout и отдельный файл. Для постоянного сервиса может быть проще оставить один stdout и разделять события в системе сбора. Многократная запись на диск не нужна только ради красивой демонстрации.

Опыт 8. Настоящий JSONL-файл и чтение обратно

Последний опыт создаёт временный каталог, записывает две строки через pino.destination, завершает запись, читает файл и разбирает каждую строку JSON. После проверки временный файл удаляется самим опытом. Так подтверждается не только вызов метода log.info, но и читаемый результат файлового назначения.

const destination = pino.destination({dest: 'app.jsonl', sync: true});
const log = pino({}, destination);
log.info({taskId: 'report-9'}, 'Отчёт начат');
log.info({taskId: 'report-9', rows: 120}, 'Отчёт готов');
destination.flushSync();
destination.end();

Sync выбран для короткого воспроизводимого опыта. Он не является универсальной рекомендацией для высоконагруженного API. Асинхронная запись и транспорты требуют понимания буферов, завершения процесса и поведения при ошибке назначения. Наличие лога в памяти ещё не гарантирует его сохранение на диск или доставку внешнему приёмнику.

Скачивание JSONL из мастерской отдаёт полный результат выбранной сцены, даже если экранный фильтр скрывает часть уровней. Для multistream отдельный аварийный поток показан дополнительно в раскрывающемся блоке. Это позволяет сравнить исходный набор с отображением и не принять пустой фильтр за отсутствие события.

Живой редактор: что выполняет pino/browser

Браузерная версия предназначена для клиентского окружения и имеет другую границу возможностей. В редакторе используется browser.asObject и собственный write: созданный объект попадает в массив на странице. Child logger добавляет requestId, а уровень действительно фильтруется библиотекой до записи. Это не имитация JSON, вручную собранного из выбранного пункта меню.

import pino from 'pino/browser.js';
const rows = [];
const log = pino({
  level: 'warn',
  browser: {asObject: true, write: record => rows.push(record)}
}).child({requestId: 'browser-demo'});
log.info('Не появится');
log.error({payload: {orderId: 42}}, 'Появится');

Встроенное ограничение — сто записей и JSON-объект до 16 КБ. Данные выводятся текстом, очищаются отдельной кнопкой и могут быть скачаны. Вложенный payload не может заменить level, time и requestId логгера. При этом он не маскируется автоматически: вводите только учебные значения. Отличия Node и браузера перечислены в Browser API.

HTTP-пример: связать запрос, ответ и время выполнения

В исходниках есть отдельный настоящий сервер src/server.mjs на порту 8284. Он создаёт requestId через randomUUID, возвращает его в заголовке ответа и использует child logger. Событие finish записывает statusCode и durationMs. Если соединение закрыто до завершения ответа, появляется предупреждение о прерывании со стороны клиента.

const requestId = randomUUID();
const started = performance.now();
const log = logger.child({requestId, route: '/orders'});
res.setHeader('x-request-id', requestId);
res.on('finish', () => log.info({
  statusCode: res.statusCode,
  durationMs: Math.round((performance.now() - started) * 100) / 100
}, 'Запрос завершён'));

В примере есть /health, /orders и /fail. Последний намеренно возвращает 503, чтобы можно было увидеть структурированное исключение. Неизвестный путь даёт 404, а метод кроме GET — 405. Некорректный URL запроса возвращает 400 с invalid_request_target; сервер продолжает обслуживать следующие запросы. Сервер не принимает идентификатор клиента как доверенный и не пишет query string, тело, Authorization или Cookie. Для неизвестного маршрута логируется unmatched вместо произвольной строки URL; исходная строка некорректного адреса тоже не попадает в журнал.

Это локальный демонстрационный API без пользовательской базы и авторизации, а не готовый публичный кабинет. Мастерская на сайте к нему не обращается. В рабочем Fastify многие части этой интеграции уже предусмотрены фреймворком; их применение разобрано в статье о Fastify.

Windows: установка и запуск из PowerShell

Установите Node.js 24 LTS, распакуйте проект и откройте терминал в его каталоге. Для примеров закреплены Pino 10.4.0 и pino-pretty 13.2.0. Команда npm ci устанавливает версии из lock-файла. Build собирает браузерную часть и записывает результаты восьми опытов, а verify повторяет их и сравнивает данные.

Set-Location C:\Projects\pino-project
node --version
npm.cmd ci --ignore-scripts
npm.cmd run build
npm.cmd test
npm.cmd run verify
npm.cmd run examples
npm.cmd run serve

Откройте http://127.0.0.1:8184/. Для терминального представления выполните npm.cmd run pretty. Если PowerShell блокирует npm.ps1, вариант npm.cmd позволяет запустить установленную команду без изменения общей политики выполнения. Сервер останавливается Ctrl+C; мастерская и HTTP API запускаются разными командами и используют разные порты.

Чтобы проверить настоящий API, откройте второй терминал в папке проекта, задайте $env:LOG_LEVEL = “info” и выполните npm.cmd run api. Из ещё одного терминала вызовите curl.exe -i http://127.0.0.1:8284/orders. Вызов curl.exe указан явно, чтобы не попасть на одноимённый псевдоним Windows PowerShell.

Windows: сохранить JSONL без изменения кодировки

При перенаправлении вывода старый Windows PowerShell может преобразовать кодировку. Для учебного файла можно передать stdout Node напрямую через cmd, а затем прочитать его как UTF-8. Команда ниже создаёт каталог runtime и перезаписывает только указанный demo.jsonl. Для важных журналов выбирайте отдельный процесс хранения и ротации.

New-Item -ItemType Directory -Force runtime | Out-Null
cmd.exe /d /c "node src/cli.mjs > runtime\demo.jsonl"
Get-Content -Encoding utf8 runtime/demo.jsonl |
  ForEach-Object { $_ | ConvertFrom-Json } |
  Where-Object level -ge 50

Выборка работает по полю level, а не по наличию слова error в сообщении. Для одного опыта передайте его имя: node src/cli.mjs redact или node src/cli.mjs child. Если журнал содержит только JSONL, каждую строку можно разобрать независимо. Если в файл попали заголовки npm или произвольный console.log, сначала исправьте источник смешанного формата.

Linux: команды, фильтрация и удалённый просмотр

На Linux используется тот же исходный проект. Установите подходящую Node 24, проверьте версию и распакуйте архив в отдельный каталог. Не меняйте runtime действующего приложения только ради знакомства с библиотекой. Локальная мастерская слушает 127.0.0.1 и не требует открывать её порт в Интернет.

unzip pino-project.zip -d "$HOME/projects"
cd "$HOME/projects/pino-project"
npm ci --ignore-scripts
npm run build
npm test
npm run verify
mkdir -p runtime
node src/cli.mjs > runtime/demo.jsonl
jq 'select(.level >= 50)' runtime/demo.jsonl
npm run serve

Jq здесь используется только для чтения JSONL; это отдельная утилита, не зависимость проекта. На Ubuntu/Debian она доступна через apt. Если приложение работает на VPS, для просмотра мастерской используйте SSH-туннель ssh -N -L 8184:127.0.0.1:8184 user@your-server и откройте адрес в локальном браузере. Такой же OpenSSH есть в современных Windows.

HTTP-пример запускается командой LOG_LEVEL=info npm run api. Проверки /orders и /fail покажут успешную и неуспешную операции. Путь /fail возвращает ожидаемый учебный 503, поэтому его результат не надо «исправлять» отключением логирования. Уровень error в журнале здесь соответствует намеренно воспроизведённому сбою.

VPS: отдельный systemd-сервис и stdout

Для постоянного запуска Linux-приложения удобен отдельный systemd unit. В архиве есть deploy/aen-pino-demo.service: Node запускается напрямую, JSONL направляется в journald, сервис работает от отдельного пользователя и слушает только loopback. Pino-pretty в production-цепочку не включён: исходный формат остаётся пригодным для разбора.

[Service]
User=aen-pino
Group=aen-pino
WorkingDirectory=/opt/aen-pino
ExecStart=/usr/bin/node /opt/aen-pino/src/server.mjs
Environment=LOG_LEVEL=info
Environment=PORT=8284
StandardOutput=journal
StandardError=journal
Restart=on-failure
RestartSec=5

Полный unit также задаёт ограничение времени остановки и базовые ограничения процесса. Проверьте фактический путь command -v node: установка через NVM в домашнем каталоге не подходит к указанному /usr/bin/node и ProtectHome=true без адаптации. README содержит подготовку отдельного пользователя, установку зависимостей, проверку unit и остановку именно учебного сервиса.

sudo systemd-analyze verify /etc/systemd/system/aen-pino-demo.service
sudo systemctl daemon-reload
sudo systemctl start aen-pino-demo.service
sudo systemctl status aen-pino-demo.service --no-pager
curl -i http://127.0.0.1:8284/orders

Размещение мастерской и размещение API — разные задачи. Готовый public можно раздавать через существующий HTTPS-сайт без Node-процесса. Если реальному API нужен внешний доступ, включайте его в действующую схему авторизации и reverse proxy; учебный /fail не следует выставлять как рабочий endpoint. Для серверной части сайта есть материалы о Caddy и Nginx.

Как читать Pino в journald и не перепутать уровни

Journald добавляет собственную оболочку: unit, время, процесс и другие поля. Опция -o cat показывает содержимое MESSAGE. При этом в журнале службы могут присутствовать сообщения systemd, которые не являются JSON Pino. Для обработки смешанного потока удобно сначала попытаться разобрать каждую строку и пропустить неподходящие.

sudo journalctl -u aen-pino-demo.service --since today --no-pager -o cat |
  jq -R 'fromjson? | select(.level >= 50)'

Число level внутри JSON не становится автоматически приоритетом journald. Поэтому journalctl -p err не равнозначен выборке Pino error. Используйте фильтр по данным Pino либо настройте явное преобразование в системе сбора. Такой нюанс помогает избежать ситуации, когда журнал содержит ошибки, но привычная команда их не показывает.

Ограничивайте временной диапазон и unit, особенно на большом сервере. Для проверки последних запросов обычно достаточно нескольких минут. Доступ к журналу следует выдавать по рабочей необходимости: даже аккуратно подготовленные события могут содержать технические сведения о приложении. Глобальная конфигурация journald в исходниках не меняется.

Хранение, ротация и доставка — отдельная часть системы

Если бесконечно дописывать файл, однажды закончится место. До production определите срок хранения, допустимый объём, права чтения и поведение при недоступности получателя. Для systemd-примера это политика journald; для отдельного файла — согласованная ротация и повторное открытие назначения. Не размещайте журналы в каталоге public рядом со статикой.

Переименование файла не всегда переключает уже открытый дескриптор процесса на новый путь. Некоторые схемы используют reopen после ротации, другие — копирование и усечение с собственными ограничениями. Не копируйте случайный logrotate-фрагмент без понимания того, как пишет ваш процесс. Официальный раздел помощи Pino разбирает ротацию, переоткрытие и фильтрацию.

Для внешнего сборщика нужны свои проверки доставки, повторов и буферизации. Успешный log.info не доказывает, что событие уже доступно в удалённом интерфейсе поиска. В проекте нет подключения к платному провайдеру, а такие гарантии не заявляются. Начните с наблюдаемого локального пути вывода и добавляйте следующий уровень после проверки его поведения.

Pino-pretty для разработки и transports для интеграций

Читать длинные JSON-строки в терминале не всегда удобно. Pino-pretty преобразует вывод в более наглядное представление для разработчика. В проекте есть npm run pretty: опыты остаются теми же, меняется только способ показа. Исходный JSONL полезнее сохранять отдельно, чтобы не потерять поля и формат машинной обработки.

npm run pretty
# One selected scenario with the installed formatter:
node src/cli.mjs child | node node_modules/pino-pretty/bin.js

В рабочей системе транспорт может направлять события в нужное назначение или выполнять обработку вне основного потока. Это расширение требует отдельной настройки жизненного цикла и порогов. Важно, чтобы завершение процесса не обрывало ожидаемую доставку, а медленный получатель не создавал неограниченный рост буфера. Начальные восемь опытов намеренно обходятся простыми контролируемыми назначениями.

Не смешивайте красивый терминальный вывод с контрактом хранения. Сначала определите, кто будет читать данные: человек, парсер, агент сбора или несколько получателей. Затем выбирайте преобразование. Официальные материалы: pino-pretty и transports Pino.

Какие поля помогают расследовать сбой

Полезная запись отвечает на конкретный вопрос. Для HTTP это requestId, route, statusCode и durationMs. Для задачи — jobId, attempt и результат. Для импорта — количество обработанных строк и безопасный идентификатор источника. Стабильные имена важнее большого объёма: три названия одного requestId в разных сервисах затрудняют поиск.

Сообщение должно описывать событие, а не повторять JSON целиком. «Импорт завершён» вместе с imported и skipped помогает сравнивать запуски. «Данные: {…}» заставляет человека изучать внутреннюю структуру без объяснения результата. Для ошибки добавляйте имя операции и код, если он действительно имеет устойчивый смысл в приложении.

Не используйте журнал как единственную базу бизнес-событий, если от неё зависят расчёты или права. Логи могут фильтроваться, теряться при сбое и удаляться по сроку хранения. Аудит действий и операционная диагностика имеют разные требования. Pino поможет сформировать запись, но выбор хранилища и гарантий остаётся архитектурным решением.

Проверки проекта и границы примеров

В проекте девятнадцать тестов: восемь настоящих сцен, фильтрация уровней, наличие контекста, отсутствие трёх учебных секретов, сохранность orderId, структура Error, allowlist сериализатора, mixin, два потока, файловое чтение, браузерный логгер, DOM-контролы и реальный локальный HTTP-сервер. Серверные проверки охватывают 200, 400, 503, 404 и 405, продолжение работы после некорректного URL и отсутствие переданных учебных секретов в журнале. Проверка интерфейса подтверждает, что ошибка загрузки Node-опытов не блокирует независимый живой редактор.

npm test
npm run verify
node src/cli.mjs levels
node src/cli.mjs redact
node src/cli.mjs file

Проверка verify не сравнивает случайные timestamp и PID: эти поля отключены только в воспроизводимых сценах. Рабочий HTTP-сервер использует обычное время Pino. Такое разделение позволяет проверить пример и сохранить смысл реального журнала. Появление нового свойства в записи должно быть осознанным изменением контракта, а не поводом убрать сравнение результата.

Эти проверки не измеряют пропускную способность вашей базы, надёжность внешнего хранилища и поведение физического телефона. Они подтверждают конкретную логику проекта. Перед внедрением добавьте свои сценарии: переполненный диск, недоступный сборщик, корректное завершение процесса и объём debug при реальном потоке запросов.

Частые ошибки и порядок внедрения

Если debug не появился, сначала проверьте порог самого логгера, затем настройки получателя. Если строки одной операции невозможно собрать, проверьте передачу child logger и единое имя requestId. Если файл не разбирается как JSONL, ищите лишние console.log, заголовки npm и перекодирование оболочки. Эти причины обычно полезнее поиска ошибки в функции log.info.

Если секрет всё же оказался в журнале, сравните фактический путь и регистр поля с redaction. Проверьте, не был ли он встроен в строку message или stack. Если поля неожиданно заменяют друг друга, перестаньте разворачивать недоверенный объект на верхний уровень записи. Используйте разрешённую структуру и вложенный payload только там, где он действительно нужен.

Внедряйте постепенно: один HTTP-маршрут или одно фоновое задание, согласованный набор полей, понятный порог, проверяемый вывод и ограниченное хранение. Затем воспроизведите ошибку и найдите её по идентификатору так, как это будет делать поддержка. Если ответ находится быстро и не требует читать личные данные, схема логирования уже приносит практическую пользу.

Исходники и официальные материалы

Весь проект можно скачать и запустить отдельно. В архив входят Node-опыты, CLI, локальный HTTP-сервер, браузерная мастерская, lock-файл, тесты, README и systemd-шаблон. Собственный код распространяется под MIT, лицензии зависимостей приложены. В статье нет скрытого подключения к внешнему сборщику или отправки ваших тестовых записей на сервер.

После мастерской выберите одну реальную проблему сопровождения: поиск неуспешного запроса, повтор задания или сбой импорта. Добавьте необходимые поля, воспроизведите ситуацию и убедитесь, что журнал даёт понятный ответ. Затем расширяйте покрытие событий, сохраняя общий формат и контролируя, какие данные действительно нужны для этой задачи.