Головна / Статті / Структуроване логування в Node.js: перетворення хаосу у налаштуванні дебаггінгу в продакшені на порядок

Структуроване логування в Node.js: перетворення хаосу у налаштуванні дебаггінгу в продакшені на порядок

Дізнайтеся, чому console.log не працює у продакшн-додатках Node.js, та як структуроване логування, рівні логів та ідентифікатори кореляції допомагають швидко вирішувати складні проблеми.

1912 слів

Клієнт скаржиться, що у нього зазнала невдачі процедура оплати. Панель моніторингу виявляє нерозкриту помилку: TypeError: Cannot read properties of undefined (reading 'id').

Ви завантажуєте файл, на який вказує стек-трейс, і проблемна рядок виглядає абсолютно звичайним:

const customerId = session.user.id;

То чому в той момент session.user був undefined?

Ви намагаєтесь відтворити цю проблему локально. Ви уходите в систему, проходите через процес оплати, і нічого не ламається. Ви перевіряєте базу даних і бачите, що кожен тестовий запис має повний об’єкт користувача. Без можливості побачити, які саме дані потрапили до цього рядка коду в продакшені, вам доводиться лише здогадуватися.

Таке розслідування може забрати цілі дні робочого часу інженерів. Але якщо ви налаштували логування з урахуванням цілей та структури, ту саму помилку часто вдається виправити протягом кількох хвилин.

Чому налагодження Node.js у продакшні є складним

Під час розробки локально у вас є інтерактивні засоби налагодження, миттєва відповідь терміналу, і зазвичай є лише один користувач, який використовує додаток — це ви самі. Якщо якийсь маршрут видає помилку, ви просто запускаєте його знову з іншими даними, поки причина не стане зрозумілою.

У продакшні немає жодних з цих зручностей:

  • Асинхронна обробка: Node.js обробляє багато операцій одночасно за допомогою свого циклу подій. Поки один запит чекає на виконання запиту до бази даних, інший вже виконує свою логіку. Вивод з консолі від одночасних запитів перемішується, що робить його нерозбірливим.
  • Тимчасові дані: Конкретний набір даних, який надіслав користувач, зберігається в пам’яті лише на короткий час. Як тільки некерована помилка призводить до зупинки процесу або сервер відповідає кодом 500, ці дані стають недоступними для відновлення.
  • Короткі треки стеку: Трек стеку в JavaScript повідомляє вам де виникла помилка під час виконання, але майже ніколи не пояснює чому. Він не розкриває, який користувач здійснив виклик, які опції були обрані чи що насправді повернула верхньоступенева служба.
  • Без навмисного логування ваша працююча програма поводиться як запечатана чорна скринька. Ефективні практики логування виступають у ролі панелі керування, яка показує, що відбувається всередині неї.

    Пастка консольного логування

    Більшість розробників спочатку вдаються до console.log() під час дебагування додатків Node.js. Він поставляється разом із середовищем виконання, не потребує налаштувань та записує дані безпосередньо у stdout.

    Але покладання на нього у продакшені створює три серйозні проблеми.

    1. Він генерує неструктурований текст

    Візьмемо такий рядок:

    console.log("Payment processed for user: " + userId + " amount: " + amount);
    

    Як тільки платформа для агрегації журналів, така як Datadog, CloudWatch чи Grafana Loki, отримує цей рядок, він зберігається як звичайний рядок без індексації. Не існує ефективного способу здійснити пошук „усіх оплат понад 1000“, оскільки платформі довелося б використовувати дорогий регулярний вираз для обробки тексту лише для вилучення значень.

    2. Відсутній контекст виконання

    Database timeout on query
    

    Тут немає жодної інформації про те, який маршрут спричинив запит, який користувач брав участь чи коли надійшов запит.

    3. Це може погіршити продуктивність циклу подій

    За певних умов — зокрема під час запису в файли або передачі даних через трубку на деяких системах — console.log() виконується синхронно. Коли навантаження високе, часті виклики console.log() призводять до блокування циклу подій Node.js, що підвищує затримки в усьому додатку.

    Три основи журналінгу промислового рівня

    Щоб журнали справді були корисними для відлову помилок, необхідно дотримуватися трьох основних принципів: структурований вихід, послідовні рівні серйозності та ідентифікатори кореляції.

    1. Структурований журналінг (JSON)

    Замість запису звичайних рядків ваш додаток повинен виводити кожну запис у журнал у вигляді структурованого об’єкта JSON. Ключі JSON можуть автоматично індексуватися платформами для журналінгу.

    {
      "timestamp": "2026-03-24T14:32:01.412Z",
      "level": "error",
      "message": "Payment processing failed",
      "userId": "usr_9912",
      "orderId": "ord_5521",
      "attempt": 3,
      "error": "Gateway timeout"
    }
    

    За таким форматом немає необхідності здогадуватися, що сталося при виникненні проблеми — достатньо лише знайти orderId: "ord_5521", щоб миттєво отримати всі записи журналу, пов’язані з цим замовленням.

    2. Значущі рівні журналу

    Не кожен запис потребує однакової уваги. Використання послідовних рівнів журналу допомагає контролювати обсяг інформації у продакшені:

    • DEBUG: Детальні діагностичні дані, призначені для розробки, такі як повні записи даних або внутрішні рішення щодо розгалужень. Їх зазвичай пригнічують у продакшені.
    • INFO: Звичайні оперативні повідомлення, які підтверджують правильну роботу додатку, наприклад Server started on port 3000 або User account created.
  • ПОПЕРЕДЖЕННЯ: Ситуації, з яких система відновилася самостійно, але які можуть свідчити про посилення проблеми — наприклад, невдалий доступ до кешу, що змушує систему використовувати базу даних, або спроба клієнта звернутися до застарілого кінцевого пункту.
  • ПОШКДА: Невдачі, які завадили завершенню операції та потребують перевірки людиною, такі як невдала оплата або неперетворена відмова обіцянки.
  • 3. ID кореляції (відстеження запитів)

    Оскільки Node.js виконує операції асинхронно, потрібен спосіб для відстеження окремого запиту під час його проходження через контролери, сервіси, виклики бази даних та зовнішні запити API.

    ID кореляції — який зазвичай називають requestId або traceId — це унікальний ідентифікатор, створений під час першого надходження HTTP-запиту. Цей ідентифікатор додається до кожної рядка журналу, який створюється під час обробки цього запиту.

    Практична реалізація: структуроване журналювання в Express

    Тепер давайте застосуємо це на практиці, створивши готову до використання систему журналювання за допомогою Pino — дуже швидкого JSON-журналіста для Node.js — у поєднанні з вбудованим API AsyncLocalStorage для обробки ID кореляції.

    Крок 1: Налаштування сховища контексту та журналіста

    Node.js постачається з AsyncLocalStorage, який доступний через основний модуль node:async_hooks. Уявіть його як еквівалент локального зберігання для потоку, яке існує в багатопотокових мовах: він зберігає фрагмент контексту під час асинхронних викликів, не змушуючи вас вручну передавати параметри потоку через кожну функцію.

    // logger.js
    import pino from 'pino';
    import { AsyncLocalStorage } from 'node:async_hooks';
    
    export const asyncLocalStorage = new AsyncLocalStorage();const baseLogger = pino({
      level: process.env.LOG_LEVEL || 'info',
      timestamp: pino.stdTimeFunctions.isoTime,
    });// A proxy that injects the requestId into every log entry automatically
    export const logger = new Proxy(baseLogger, {
      get(target, property) {
        const store = asyncLocalStorage.getStore();
        const childLogger = store?.requestId
          ? target.child({ requestId: store.requestId })
          : target;    return childLogger[property];
      }
    });
    

    Крок 2: Реалізація мідлвейру Express

    Далі напишіть мідлвейр, який присвоює ідентифікатор кожному надходженню запиту та виконує решту етапів життєвого циклу запиту в контексті AsyncLocalStorage.

    // middleware.js
    import { randomUUID } from 'node:crypto';
    import { asyncLocalStorage, logger } from './logger.js';
    
    export function requestContextMiddleware(req, res, next) {
      // Use existing header if forwarded by a load balancer, or create a new UUID
      const requestId = req.headers['x-request-id'] || randomUUID();
    
      res.setHeader('x-request-id', requestId);  asyncLocalStorage.run({ requestId }, () => {
        const startTime = Date.now();    logger.info({
          method: req.method,
          url: req.url,
          ip: req.ip
        }, 'Incoming request');    res.on('finish', () => {
          const durationMs = Date.now() - startTime;
    
          const logData = {
            statusCode: res.statusCode,
            durationMs
          };      if (res.statusCode >= 500) {
            logger.error(logData, 'Request completed with server error');
          } else {
            logger.info(logData, 'Request completed');
          }
        });    next();
      });
    }
    

    Крок 3: Використання логгера в бізнес-логіці

    У своїх контролерах чи функціях сервісу просто імпортуйте спільний логгер. Немає потреби вручну передавати req чи requestId як параметри функції.

    // orderService.js
    import { logger } from './logger.js';
    
    export async function processOrder(user, cart) {
      logger.info({ cartItemsCount: cart.items.length }, 'Validating cart inventory');  if (!user || !user.id) {
        logger.warn({ userState: user }, 'Attempted checkout without a valid user ID');
        throw new Error('User identification is missing');
      }  // Continue checkout logic...
    }
    

    Якщо щось зазнає невдачі всередині processOrder, запис у журналі виглядає ось так:

    {
      "time": "2026-03-24T14:40:12.102Z",
      "level": 40,
      "requestId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "userState": null,
      "msg": "Attempted checkout without a valid user ID"
    }
    

    Зверніть увагу, як requestId автоматично з’являється в цьому записі. Маючи це значення, ви можете шукати у своїх журналах та відтворювати повну послідовність подій, пов’язаних із цим запитом, від початку до кінця.

    Аналіз 5-хвилинного рішення

    Давайте знову розглянемо початкову проблему: session.user виявляється значенням undefined під час процесу оформлення замовлення.

    Без структурованого контексту (сценарій кошмару)

    • Стек-трейс вказує на рядок const customerId = session.user.id.
    • Ви перевіряєте базу даних та бачите, що у всіх тестових облікових записах сесії є цілком дійсними.
  • Ви витрачаєте 45 хвилин на перевірку різних теорій: гостьовий вихід, закінчені сесії, поведінка, специфічна для мобільних пристроїв.
  • Ви додаєте тимчасові виклики console.log() у продакшн, сподіваючись, що баг знову з’явиться, щоб ви могли його зафіксувати в реальному часі.
  • Проблема залишається нерозв’язаною протягом кількох днів.
  • Зі структурованим контекстом (сценарій на 5 хвилин)

    • Ви берете requestId зі звіту клієнта про баг або з попередження про помилку 500.
    • Ви шукаєте у своєму збиральнику журналів requestId: "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d".
    • Ви отримуєте три записи журналу, пов’язані з цим ID, та читаєте їх по черзі. У першому вказується, що запит надійшов на маршрут оплати. У другому зазначається, що сесія належить анонімному, неавтентифікованому відвідувачеві, а не увійшовому клієнту. У третьому підтверджується, що логіка оплати відхилила спробу саме через відсутність дійсного ID користувача.
    • Якщо прочитати ці три записи разом, основна причина стає очевидною відразу: користувачі-відвідувачі потрапляють безпосередньо на маршрут оплати з автентифікацією, обходячи заплановане перенаправлення для відвідувачів.
    • Ви можете виправити перевірку автентифікації приблизно за п’ять хвилин.

    Рішення знайшлося швидко не тому, що код став простішим, а тому, що запущена програма вже документувала свій власний шлях виконання.

    Поширені помилки у журналуванні, яких слід уникати

    Навіть досвідчені команди по-різному погіршують якість своїх журналів подій:

    • Запис секретних даних (PII): Ніколи не записуйте у журнали паролі, токени автентифікації, повні номери карт чи інші особисті дані. Користуйтеся вбудованими функціями маскування — наприклад, опцією redact у Pino — щоб автоматично видаляти такі поля, як password чи authorization, ще до того, як інформація буде серіалізована.
    • Зберігання всього у продакшні: Надмірне використання інформації рівня DEBUG у живій системі під реальним трафіком марнує процесорні ресурси та збільшує витрати на зберігання журналів. Для стандартних продакшн-середовищ використовуйте рівень INFO чи WARN, а рівень деталізації підвищуйте лише тимчасово, коли це дійсно необхідно.
  • Видалення деталей помилки: Уникайте ситуацій, коли ви ловите помилку та записуєте лише звичайне рядкове повідомлення, адже це призводить до втрати самого об’єкта помилки та його стек-трейсу. Натомість завжди передавайте повний об’єкт помилки під час виклику функції логування, наприклад logger.error({ err }, "Не вдалося зберегти дані"), щоб нічого не загубилося.
  • Вбудовування можливостей спостереження у ваш код

    Застосування правильних практик логування змінює підхід до розуміння продакшн-систем. Замість того, щоб сприймати додаток як щось, від чого просто сподіваєшся, що воно буде працювати правильно, ви починаєте розглядати його як живий процес, який зобов’язаний надати пояснення кожного разу, коли щось йде не так.

    Щоб досягти цього, не потрібно багато зусиль на початковому етапі: сформатуйте журнали у форматі JSON, позначте кожен надходження запиту ідентифікатором взаємозв’язку та фіксуйте відповідні змінні щоразу, коли виникає помилка. Наступного разу, коли у продакшені виникне несподівана проблема, саме ця підготовка дозволить вам присвятити час її вирішенню, а не намаганням здогадатися, що сталося.

    Пов’язана література

  • Знаходження справжнього вузького місця у повільному кінцевому пункті Node.js — Дізнайтеся про систематичний метод відстеження затримок на бекенді по шляху запиту — від коду Node.js до запитів до бази даних — за допомогою вимірювання часу та команди EXPLAIN ANALYZE.
  • Шаблони структурованого журналювання для ефективної налагодки API Node.js — Дізнайтеся, як замінити розрізнені виклики console.log на структуроване журналювання, ідентифікатори запитів, рівні журналів та показники часу для швидшої налагодки API Node.js.
  • Створення API для Node.js з підтримкою Observable: журналювання, метрики та відстеження — Дізнайтеся, як структуроване журналювання, відстеження запитів, метрики та систематичне надсилання сповіщень допомагають значно полегшити виправлення помилок у продакшн-API для Node.js.