Логирование запросов с помощью middleware в Node.js
Когда что-то ломается в вашем API в 2 часа ночи, первое, к чему вы обращаетесь — это логи. Если они отсутствуют, неполные или утонули в шуме, отладка превращается в угадывание. Логирование HTTP-запросов в Node.js — одна из тех основ, которую легко сделать неправильно и дорого игнорировать.
В этой статье рассматривается, как работает middleware для логирования запросов в Express, когда использовать проверенные библиотеки вроде Morgan или Pino, и как выглядит production-ready логирование на практике.
Ключевые выводы
- Middleware для логирования в Express перехватывает запросы на раннем этапе цепочки и слушает событие
finishобъектаres, чтобы захватить данные ответа, такие как коды статуса и длительность. - Morgan обеспечивает быстрый, человекочитаемый вывод логов доступа, подходящий для разработки, в то время как Pino предлагает быстрый, структурированный JSON-вывод, созданный для production-окружений.
- Используйте
AsyncLocalStorageв современном Node.js для распространения correlation ID через асинхронные операции без необходимости вручную передавать их через каждый вызов функции. - Никогда не логируйте чувствительные данные, такие как заголовки
Authorization, cookies или тела запросов по умолчанию — используйте встроенную редакцию или выполняйте санитизацию вручную.
Как middleware для логирования вписывается в жизненный цикл запроса Express
В Express middleware — это функция с сигнатурой (req, res, next). Она перехватывает каждый запрос до того, как он достигнет вашего обработчика маршрута. Middleware для логирования находится в начале этой цепочки, записывая, что пришло и что ушло.
[Клиент] → [Logging Middleware] → [Auth Middleware] → [Обработчик маршрута] → [Ответ]
Ключевой момент: вы не можете залогировать полный ответ — код статуса, длительность — пока ответ не завершён. Вот почему middleware для логирования слушает событие finish объекта res, а не логирует немедленно.
import crypto from 'node:crypto'
// Express middleware для логирования запросов
const logRequests = (req, res, next) => {
const start = Date.now()
res.on('finish', () => {
logger.info({
method: req.method,
url: req.url,
status: res.statusCode,
duration: Date.now() - start,
requestId: req.headers['x-request-id'] ?? crypto.randomUUID(),
})
})
next()
}
app.use(logRequests)
Обратите внимание, что crypto.randomUUID() требует импорта модуля node:crypto (или использования глобального crypto, доступного в Node.js 19+). Также объект logger здесь является заполнителем — вы должны заменить его на экземпляр вашей реальной библиотеки логирования (например, Pino или console).
Этот паттерн работает с любым HTTP-сервером Node.js, не только с Express.
Традиционное логирование доступа с Morgan
Morgan — классическое middleware для логирования запросов в Express. Две строки — и у вас есть логи доступа в стиле Apache:
import morgan from 'morgan'
app.use(morgan('combined'))
// Пример вывода:
// ::1 - - [01/Jan/2025:00:00:00 +0000] "GET /api/users HTTP/1.1" 200 1234
Morgan подходит для разработки и простых развёртываний. Его вывод читаем человеком, но не легко парсится машиной — что становится проблемой, когда вы отправляете логи в Datadog, Loki или любую систему структурированных логов.
Структурированное логирование в Node.js с Pino
Для production структурированное логирование означает вывод в JSON. Каждая строка лога становится записью, по которой можно делать запросы. Pino — стандартный выбор здесь — он значительно быстрее Winston или Bunyan, с минимальными накладными расходами.
Пакет pino-http встраивается как middleware для Express:
import express from 'express'
import pinoHttp from 'pino-http'
const app = express()
app.use(pinoHttp({
level: process.env.LOG_LEVEL ?? 'info',
redact: ['req.headers.authorization', 'req.headers.cookie'],
}))
app.get('/', (req, res) => {
req.log.info('handling root request')
res.send('ok')
})
Pino пишет в stdout. Ваша инфраструктура (Docker, systemd, log shipper) обрабатывает маршрутизацию этих строк туда, куда им нужно попасть.
Discover how at OpenReplay.com.
Correlation ID и контекст запроса
Когда вы отслеживаете запрос через множество асинхронных операций, вам нужен согласованный ID запроса, прикреплённый к каждой строке лога. В современном Node.js используйте для этого AsyncLocalStorage — не устаревший модуль domain или низкоуровневые async hooks.
import { AsyncLocalStorage } from 'node:async_hooks'
import crypto from 'node:crypto'
export const requestContext = new AsyncLocalStorage()
app.use((req, res, next) => {
const requestId = req.headers['x-request-id'] ?? crypto.randomUUID()
requestContext.run({ requestId }, next)
})
Любой вызов логгера внутри асинхронного контекста этого запроса теперь может получить requestId без необходимости вручную передавать его через каждую функцию. Вот как вы можете его получить:
// В любом downstream-модуле
import { requestContext } from './context.js'
function doWork() {
const { requestId } = requestContext.getStore()
logger.info({ requestId, msg: 'doing work' })
}
Ответственное логирование
Несколько правил, которые важны в production:
- Никогда не логируйте заголовки
Authorization, cookies или API-токены. Используйте опциюredactв Pino или выполняйте санитизацию вручную перед записью. - Будьте осторожны с IP-адресами клиентов за прокси.
req.socket.remoteAddressдаст вам IP прокси. Если ваше приложение находится за обратным прокси, правильно настройте параметрtrust proxyв Express и осторожно обрабатывайте заголовкиX-Forwarded-For. - Не логируйте тела запросов по умолчанию. Они могут быть большими, бинарными или содержать персональные данные. Логируйте их выборочно, с ограничениями по размеру.
Выбор между Morgan и Pino
| Morgan | Pino (pino-http) | |
|---|---|---|
| Формат вывода | Текст (стиль Apache) | JSON (структурированный) |
| Производительность | Хорошая | Отличная |
| Редакция логов | Ручная | Встроенная |
| Production-ready | Ограниченно | Да |
| Время настройки | ~2 мин | ~5 мин |
Используйте Morgan для локальной разработки, если предпочитаете читаемый вывод. Используйте Pino для всего, что отправляется в production.
Заключение
Хорошее middleware для логирования незаметно, пока оно вам не понадобится — и тогда оно становится всем. Начните с pino-http, выводите структурированный JSON в stdout и позвольте вашей инфраструктуре обрабатывать маршрутизацию и хранение. Объедините это с correlation ID через AsyncLocalStorage, чтобы вы могли отследить любой запрос от начала до конца. Держите чувствительные данные вне ваших логов с первого дня, и у вас будет фундамент observability, который масштабируется вместе с вашим приложением.
Часто задаваемые вопросы
Да, вы можете запускать оба одновременно. Распространённый паттерн — использование Morgan в разработке для читаемого вывода в консоль и Pino в production для структурированных JSON-логов. Используйте переменную окружения для условного применения одного или другого, чтобы избежать дублирования записей логов в любом отдельном окружении.
Pino по дизайну пишет JSON в stdout. Используйте log shipper, такой как Fluent Bit, Filebeat или Datadog Agent, для сбора вывода stdout из вашего контейнера или процесса и пересылки его на вашу платформу логирования. Это позволяет отделить код вашего приложения от любого конкретного назначения логов.
AsyncLocalStorage автоматически распространяет контекст через всю цепочку асинхронных вызовов без изменения сигнатур функций. Ручная передача ID запроса через каждую функцию подвержена ошибкам и загромождает ваш код. AsyncLocalStorage стабилен в Node.js 16 и выше и является рекомендуемым подходом для контекста, ограниченного запросом.
С Pino накладные расходы обычно незначительны, потому что он использует быструю сериализацию и эффективно записывает логи в stdout. Morgan также лёгкий для большинства нагрузок. Паттерн с прослушиванием события `finish` означает, что логирование выполняется после того, как ответ был передан операционной системе, поэтому обычно это не влияет на задержку для клиента. Избегайте синхронных записей в файл или тяжёлой сериализации в вашем пути логирования.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.