Это перевод статьи «A Complete Guide to Pino Logging in Node.js», опубликованной 14 апреля 2023-го года Better Stack Team. Но сперва предисловие переводчика. Почему именно Pino? Об этом можно посмотреть видео основателя проекта, Маттео Коллина (Matteo Collina), «The Cost of Logging» о цене, которую приходится платить  многопользовательским приложениям за кажущуюся совершенно безобидной функцию ведения логов. Суть его выступления в том, что при выборе медленного инструмента логирования и увеличении количества запросов в секунду, оверхед на запись логов становится неприлично высоким. Каждую секунду приложение может тратить большую часть времени только на запись логов. Чтобы справиться с этой проблемой, и был создан Pino, асинхронный JSON-логгер. В первую очередь именно его асинхронность и позволяет ему быть очень быстрым, создавая минимальный оверхед в системе. Также для максимального ускорения были применены и некоторые другие приёмы, о которых упоминается в видео. Кроме того, этот логгер является встроенным в набирающем популярность веб-фреймворке Fastify, ведущим разработчиком которого является всё тот же Маттео Коллина. Fastify был создан со всё той же целью — минимизировать оверхед для Node.js-приложений. Далее перевод оригинальной статьи от Better Stack Team. Pino — это мощная платформа ведения логов для Node.js, обладающая молниеносной скоростью и широкими возможностями. Фактически именно скорость Pino и завоевала ему место логгера по-умолчанию в open-source веб-сервере Fastify. Также Pino способен очень просто интегрироваться с другими Node.js-фреймворками, что делает его лучшим выбором для всех разработчиков, ищущих надёжное и гибкое решение для ведения логов. В Pino есть все стандартные возможности, которые ожидаются от логгера , такие как настраиваемые уровни логирования, настройки форматирования и всевозможные виды транспортов.  Его гибкость является одной из его самых выдающихся особенностей, так как он может быть легко расширен дополнительными модулями, чтобы соответствовать любым требованиям. Это делает его лучшим выбором для широкого спектра приложений. В этой статье будет описан процесс создания службы логирования для Node.js-приложения с помощью Pino. Будет показано, как задействовать и настроить многие функции логгера, чтобы достичь оптимальной конфигурации для того или иного случая. К концу этого пособия вы будете хорошо подготовлены, чтобы самостоятельно настроить систему логирования в своём Node.js-приложении. Она поможет вам полностью наладить все процессы ведения логов, улучшит общую производительность и надёжность вашего приложения. Прежде, чем продолжить читать статью, убедитесь, что на вашей машине установлены последние версии Node.js и npm. Также подразумевается, что вы уже знакомы с базовыми особенностями логирования в Node.js. Чтобы получить от данного пособия максимум и протестировать все возможности Pino, обсуждаемые далее, создайте новое Node.js-приложение. Для этого выполните следующие команды: После этого установите последнюю версию Pino с помощью команды ниже. Примеры, приведённые в статье, совместимы с Pino версии 8, последней на момент написания статьи. Создайте файл logger.js в корневой папке проекта и наполните его следующим содержимым: Этот фрагмент импортирует пакет pino и экспортирует экземпляр логгера, созданный с помощью функции верхнего уровня pino(). Далее в статье мы рассмотрим все возможные способы настройки Pino, но для начала воспользуемся настройками по-умолчанию и импортируем наш логгер в index.js: После того, как сохраните файл, запустите его командой: Вы должны увидеть следующий вывод: Первое, что можно заметить — вывод структурирован в формате JSON, в доминирующем формате промышленного структурированного логирования. Кроме самого сообщения в записи лога присутствуют следующие поля: Поле level означает уровень серьёзности события. Поле time содержит время в миллисекундах, прошедшее, начиная с 01.01.1970 00:00:00 UTC. Поле hostname означает имя машины, на которой запущен процесс. Поле pid означает ID запущенного процесса. Далее будет показано как эти поля можно изменить, а также добавить новые контекстные поля. Благодаря своим простоте, гибкости и широкой поддержке среди инструментов логирования JSON хорошо подходит для ведения логов в продакшене. Но человеческому глазу сложно воспринимать его напрямую, особенно, если он записан в одну линию. Чтобы сделать логи Pino более удобочитаемыми при разработке (где они зачастую выводятся в консоль), можно применить один из следующих подходов: Jq — это стильная консольная утилита для обработки JSON. Если передать в неё JSON-логи, то результатом станет их цветной форматированный вывод:  Вывод: Если вывод получается слишком громоздким, можно удалить ненужные поля с помощью функции jq del(): Результат: Также можно использовать белый список полей, который может быть полезен ещё и в случае, если нужно поменять поля местами: Вывод: О других возможностях форматирования логов с помощью этого инструмента можно почитать в его документации. Команда разработки Pino создала аналогичный jq инструмент — pino-pretty — для преобразования строк JSON в удобочитаемый человеком обычный текст. Установка pino-pretty: Передача JSON-логов в pino-pretty:  Вы заметите, что логи сменили свой формат, обрели цвет и стали более удобочитаемыми: Если хотите изменить форматирование вывода, обратитесь к соответствующей документации по транспорту pino-pretty. В Pino уровни логирования по-умолчанию это (по возрастанию серьёзности): trace, debug, info, warn, error, и fatal. Для каждого из них существует соответствующий метод логгера: После запуска index.js должен получиться следующий результат: Обратите внимание, что поле level представлено в виде числа, увеличивающегося на 10 каждый раз с повышением уровня серьёзности события. Также вы можете обнаружить, что в списке событий нет записей, сгенерированных функциями debug() и trace() — это потому, что по-умолчанию минимальным уровнем вывода в Pino является info, который подавляет вывод событий более низкого уровня. Обычно минимальный уровень логирования выставляется при создании экземпляра логгера. Лучше всего им управлять через переменную окружения — так вы сможете всегда изменить уровень логирования снаружи, не переписывая каждый раз код приложения: В данном примере, если установлена переменная окружения PINO_LOG_LEVEL, будет использоваться её значение, а иначе уровень логирования по-умолчанию будет установлен в значение info. В примере ниже минимальный уровень логирования устанавливается в error, что значит, что все события ниже этого уровня будут проигнорированы: Примечание переводчика. Для Windows-систем такой способ подходит лишь с оговоркой, что переменная будет установлена отдельным вызовом командой set, а затем будет выполнена команда, использующая эту переменную. Это потому, что в cmd переменная окружения устанавливается только после завершения выполнения строки: Вывод: Изменить минимальный уровень логирования можно в любом месте приложения через объект logger и его свойство level: Это удобно, если необходимо изменить минимальный уровень логирования во время работы приложения, например, через создание специальной конечной точки: Вы не ограничены теми уровнями логирования, которые Pino предоставляет по-умолчанию. Можно легко добавить любые другие уровни, создав объект, содержащий номера приоритетов для каждого из уровней, и присвоив его свойству customLevels логгера. Например, в коде ниже добавляется уровень notice, более серьёзный, нежели info, но менее серьёзный, нежели warn.  После этого вы можете логировать события всех настроенных вами уровней через соответствующие методы, а все уровни, заданные по-умолчанию, будут по-прежнему работать: Результат: Если вы хотите полностью заменить уровни Pino, например, стандартными уровнями Syslog, добавьте опцию useOnlyCustomLevels: В этой главе лишь слегка затрагивается процесс изменения стандартных полей, присутствующих в каждой записи лога. Для более подробных инструкций обратитесь в раздел справки, касающийся опций инициализации Pino. Зададим какому-нибудь уровню логирования строковое имя вместо его числового значения. Сделать это можно с помощью опции formatters: Код выше переименовывает уровни и меняет регистр их новых строковых имён на верхний: Также можно переименовать само название свойства level на что-нибудь возвращаемое произвольной функцией: Вывод: По-умолчанию, временная метка создаётся в формате количества миллисекунд, прошедших с 01.01.1970 00:00:00 UTC (как в выводе функции Date.now()). Вы можете переопределить это поле (timestamp) при создании экземпляра логгера. Мы рекомендуем выводить временные метки в формате ISO-8601: Результат: Также вы можете переименовать свойство time в timestamp с помощью функции, которая возвращает частичное JSON-представление текущего времени (предваряемое запятой) как в примере ниже: Вывод: В Pino существуют ещё два встроенных поля, которые по-умолчанию добавляются к каждой записи лога: ID процесса (pid) и имя хоста (hostname). Вы можете переопределить их с помощью функции bindings в разделе formatters. Например, давайте переименуем hostname в host: Вывод: Вы можете удалить любые встроенные поля из возвращаемого объекта, а также добавить любые поля по вашему желанию — и они будут присутствовать в каждой записи лога. Ниже приведён пример, в котором в каждую запись лога добавляется версия Node.js, используемой для запуска приложения: Вывод: В качестве примеров других глобальных полей, которые могут оказаться полезными в каждой записи лога, можно привести версию приложения, имя операционной системы, элементы конфигурации, хэш коммита гита и так далее. Добавление контекстуальных данных означает вложение в запись лога дополнительной сопутствующей информации, которая описывает контекст или другие детали логируемого события. С помощью этой информации может быть проще диагностировать, отлаживать и наблюдать за вашим приложением в продакшене. Например, если в приложении возникнет ошибка, то скорее всего будет проще определить её основную причину, если она будет включать в себя такие контекстуальные данные как ID запроса, имя конечной точки или ID пользователя, который вызвал запрос. В Pino основным способом добавления контекстуальных данных является определение параметра mergingObject в вызываемом методе логгера: Фрагмент выше породит следующий результат: Бывает также очень полезным вставлять контекстуальные данные во все логи, создаваемые той или иной функцией, модулем или сервисом, чтобы не повторять их при каждом вызове логгера. В Pino это делается при помощи дочерних логгеров (child loggers): Выполните этот код с установленным минимальным уровнем логирования trace: Примечание переводчика. Для Windows-систем команды будут следующими: Результат: Обратите внимание, что свойство userID присутствует только внутри контекста функции getUser(). Использование дочерних логгеров позволяет добавлять контекстуальные данные без добавления их в самой точке логирования. Также это позволяет проще фильтровать и анализировать логи, основываясь на таких данных как ID пользователя, имя функции или других сопутствующих деталях. Логирование ошибок — это важнейшая практика, помогающая выявить и диагностировать проблемы, возникающие в работающем приложении. При перехвате исключения необходимо залогировать все возможные детали произошедшего, включая серьёзность ошибки, описание проблемы и другую сопутствующую информацию. Метод error() логгера может в качестве первого аргумента принять объект ошибки. А сообщение об ошибке в этом случае станет вторым аргументом: Этот пример создаст запись лога, включающую в себя свойство err, содержащее внутри себя тип ошибки, сообщение и полный стек вызова, что может быть полезно при диагностике проблемы: В Pino нет специального механизма для логирования неотловленных исключений или отказов промисов, поэтому вы должны самостоятельно прослушивать события uncaughtException и unhandledRejection и ещё до выхода из приложения логировать эти исключения с уровнем FATAL (после попытки мягкого завершения):  Вы можете использовать менеджер процессов PM2 или службу наподобие Docker, чтобы автоматически перезапускать приложение в случае неотловленного исключения. Чтобы всегда быть в курсе состояния вашего приложения, не забудьте настроить проверки его здоровья с помощью соответствующего приложения мониторинга. Как можно было заметить, по-умолчанию Pino выводит все сообщения в консоль. Но существует также и возможность логирования в файл или другие точки назначения (например, в службу удалённого управления логами). Для этого необходимо использовать транспорты, доступные с 7-й версии Pino. Благодаря тому, что транспорты работают внутри воркеров, основной поток приложения освобождается от операций модификации записей лога или отправки их в удалённые службы, что может значительно снизить задержку при обработке HTTP-запросов. Ниже приведён пример использования встроенного транспорта pino/file, который перенаправляет логи в файл (или файловый дескриптор): Другой способ записать логи в файл (или файловый дескриптор) заключается в использовании pino.destination() API: Обратите внимание, что транспорт pino/file под капотом использует pino.destination(). Основная разница между ними в том, что первый выполняется в отдельном воркере, а второй — в основном потоке. Если логировать только в стандартный вывод или локальные файлы, то использование pino/file может внести некоторый оверхед, так как сперва данные должны быть вынесены из основного потока. Возможно, в таких случаях стоит использовать именно pino.destination(). Pino/file рекомендуется использовать только в случае логирования сразу в несколько точек назначения, например, и в файл и в стороннюю службу управления логами. Кроме того, Pino поддерживает «устаревшие транспорты», которые выполняются в отдельном от Node.js процессе. Для бо́льших подробностей обратитесь к соответствующей документации. Запись логов одновременно в несколько точек назначения очень распространено, и в транспортах Pino начиная с 7-й версии эта возможность также поддерживается. Для этого вам необходимо создать массив targets и поместить все объекты транспортов внутрь него: Выше приведена настройка Pino на логирование одновременно и в стандартный вывод и в файл app.log. Обратите внимание, что функцию formatters.level невозможно использовать при логировании во множественные точки назначения, поэтому она и не была использована в коде выше. Если всё же попытаться её использовать, то произойдёт следующая ошибка:  Чтобы в консоли отобразился красивый отформатированный вывод вместо сырого JSON, вторую точку назначения можно изменить на pino-pretty (который должен быть предварительно установлен): Запуск: Примечание переводчика. Для Windows-систем команда запуска будет следующей: Вывод: Одна из важнейших лучших практик при логировании — это контроль над утечкой в логи конфиденциальной информации. Примерами таких данных могут быть: Финансовые данные, такие как номера банковских карт, пин-коды, номера банковских счетов и т. д. Пароли или другие секретные данные приложения. Любые данные, которые могут помочь идентифицировать личность, такие как адреса электронной почты, имена, телефонные номера, почтовые адреса, идентификационные номера и так далее. Медицинские записи. Данные биометрии и прочее. Запись чувствительной информации в логи может привести к утечкам данных, краже личности, несанкционированному доступу или другим зловредным действиям, которые могут полностью разрушить репутацию вашего бизнеса. Также это может привести к штрафам и другим санкциям в отношении вашего бизнеса от таких органов как GDPR, PCI и HIPPA . Чтобы предотвратить подобные происшествия, необходимо всегда проверять ваши логи с целью предотвращения случайного попадания в них чувствительных данных. Существует несколько практик, которые помогут вам не допустить попадания такой информации в логи. Их все невозможно обсудить в рамках данной статьи, поэтому мы сосредоточимся только на практике редактирования логов — технике выявления и удаления конфиденциальных данных с сохранением информации, необходимой для диагностики или анализа. В Pino для реализации этой функции используется npm-пакет fast-redact. Например, у нас есть объект user со следующей структурой: Если залогировать этот объект как есть, то он раскроет такие чувствительные данные как имя пользователя, его адрес, паспортные данные и телефонный номер:  Вывод: Чтобы этого не произошло, необходимо заранее настроить ваш логгер на удаление секретных полей: Опция redact определяет массив полей, данные которых должны вырезаться при записи логов. Данный пример приведёт к тому, что из логов будут вырезаны данные полей name, address, passport и phone в любом объекте user. Вместо них будет отображена надпись . В результате в логах можно будет прочитать только значение поля id:  Вы можете изменить надпись-заполнитель с помощью следующей настройки: Вывод: И, наконец, можно полностью удалить эти поля, воспользовавшись опцией remove. При логировании предпочтительно снижать количество ненужных данных, чтобы они не занимали дисковое пространство. Результат: Несмотря на то, что это довольно удобный способ снижения риска утечки данных, все усилия могут сойти на нет, если не соблюдать аккуратность. Например, если объект user вложен в какой-нибудь другой объект или помещён на самый верхний уровень, то по указанным полям цензурирующий фильтр уже не сработает, и конфиденциальная информация легко просочится в лог. Чтобы избежать этого, обновим фильтр: Не смотря на то, что на этапе ревью вы должны обеспечить полноценное логирование ваших объектов, вы не можете учесть все возможные варианты утечки чувствительных данных. Поэтому лучше не полагаться на эту технику как на основное решение проблемы конфиденциальности логирования. Редактирование логов должно использоваться как запасное решение, помогающее отловить проблемы, пропущенные на этапе ревью. В иделе, вообще не логируйте те объекты, которые могут привести к утечке данных. Из таких объектов стоит извлекать только не конфиденциальную информацию и передавать её в качестве контекстуальных данных. Это лучший способ снижения риска утечки секретных данных в логи. Pino можно использовать для логирования HTTP-запросов в любом Node.js-приложении вне зависимости от того, каким фреймворком вы пользуетесь. Пользователи Fastify уже могли заметить, что Pino встроен в этот фреймворк. По-умолчанию отключён, поэтому сперва его нужно включить: После этого Pino начнёт логировать все входящие запросы в следующем виде: Если вы используете другой фреймворк, посетите страницу экосистемы Pino, чтобы найти специфичную для него интеграцию. Пример далее показывает как использовать npm-пакет pino-http, чтобы логировать HTTP-запросы в Express: Убедитесь, что файл logger.js настроен на логирование одновременно и в консоль и в файл: Затем установите все необходимые зависимости командой ниже: Запустите сервер на порту 4000 и с помощью curl сделайте GET-запрос на роут /crypto: В консоли сервера вы увидите цветной форматированный вывод логгера по этому HTTP-запросу: А файл server.log будет содержать сырой JSON: Примечание переводчика. Для Windows-систем команда будет следующей: Вывод: Прочитав документацию по API pino-http вы можете настроить форматирование вывода, как вам нужно. Одним из главных преимуществ структурированных логов является возможность их загрузки в централизованную систему логирования, где их можно будет эффективно проиндексировать, проанализировать и организовать по ним поиск. Собрав все логи в едином месте, вы обретёте целостный взгляд на здоровье и производительность вашей системы. Так будет проще выявлять особенности её поведения, замечать в них аномалии и диагностировать проблемы. Централизация логов упрощает работу по соблюдению требований аудита и мониторинга благодаря единому источнику истины. Также она помогает обеспечить надлежащее хранение соответствующих логов и лёгкий к ним доступ при любых нормативных или юридических проверках. Более того, если пользоваться правильными инструментами, централизованное логирование даёт возможность реагировать на события в реальном времени и заранее выявлять проблемы, не дожидаясь, пока они превратятся в катастрофу. Это может значительно снизить время простоя системы и минимизировать негативное влияние проблемных ситуаций на работу вашей организации. Теперь, когда вы настроили Pino в вашем Node.js-приложении, вашим следующим шагом может стать перенаправление логов в систему управления логами. Благодаря этому вы начнёте пожинать плоды структурированного логирования. Logtail — одно из решений, которое управляет логами, анализирует и визуализирует их, а также посылает уведомления в случае обнаружения определённых вами моделей поведения.  Существует несколько способов, с помощью которых можно вывести логи из Node.js-приложения в Logtail. Самый простой из них — использовать соответствующий транспорт Pino: С этой конфигурацией вы централизуете все ваши логи в Logtail и сможете увидеть их в реальном времени на автоматически обновляемой странице в браузере. Вы сможете отфильтровать их по любому из полей и создать автоматические уведомления, которые сообщат вам о критических событиях (таких как единовременный наплыв ошибок). Примечание переводчика. Logtail — это платное решение от Better Stack Team. В качестве  бесплатной альтернативы можно использовать Elastic Stack (ELK), в который они приходят через транспорт pino-elasticsearch или Logstash (часть ELK) через транспорт pino-socket. Также можно использовать транспорты и в другие базы данных и инструменты для аналитики, которые с ними работают. Эта статья представляет из себя исчерпывающий обзор основных возможностей, предоставляемых Pino для логирования в Node.js. Кроме того, в ней показаны различные примеры его настройки. Мы надеемся, что информация, содержащаяся в данном руководстве, оказалась полезной в плане раскрытия тайны логирования с помощью Pino, а также его эффективного использования в ваших Node.js приложениях. В одной статье невозможно передать всю информацию о Pino и его возможностях, поэтому мы очень рекомендуем обратиться к официальной документации за более полной информацией об основных и продвинутых вариантах использования данного логгера. Благодарим за внимание и счастливого вам логирования!