$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогНаблюдаемость с нуля: correlation-id, структурные логи и единый формат событий

Наблюдаемость с нуля: correlation-id, структурные логи и единый формат событий

$ sudo teach IT
·25 июля 2026 г.·9 мин·60
Наблюдаемость с нуля: correlation-id, структурные логи и единый формат событий

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

Содержание
Почему логи перестают помогать: три системные причиныЛоги как строки: сложно анализироватьНет корреляции: вы видите части, но не связьНет единого формата событий: вы вынуждены угадыватьМинимальный стандарт логирования для API: состав, правила, форматЧто логируем: события как контрактЧто добавляем в каждый лог-сообщение: поля контекстаcorrelation-id: что это и чем отличается от traceIdЕдиный формат событий: один JSON в строкеcorrelation-id сквозь API и сервисы: как внедрить без магииИсточник correlation-id: заголовок запросаГенерация ID и контекст потокаПрокидывание correlation-id при вызовах других сервисовСтруктурные логи: как сделать формат устойчивымПочему «структурно» ≠ «просто JSON»Схема ошибки: один стандарт на весь доменУровни логирования: прагматичный подходКак читать логи как граф запросов: методикаШаг 1: выберите correlationId и посмотрите все событияШаг 2: восстановите причинность по времениШаг 3: проверьте согласованность форматаЧек-лист: что логировать, а что нетЧто логировать обязательноЧто логировать выборочно

Наблюдаемость (observability) часто обещают как магию: «смотри логи — и всё станет понятно». На практике же команды упираются в три типичные проблемы: логи превращаются в поток текста без структуры, запросы теряются между сервисами, а корреляция «кто вызвал кого» восстанавливается вручную по разным признакам. В итоге система не становится прозрачнее — она становится сложнее.

В этой статье соберём минимальный, но практичный стандарт логирования для API: как ввести correlation-id, как перейти от текстовых сообщений к структурным логам и как договориться об едином формате событий, чтобы логи читались как граф запросов между сервисами. Параллельно разберём, что логировать стоит, а что — нет: это ключ к качеству и к экономике хранения/индексации.

Материал будет полезен, если вы только начинаете наблюдаемость, а также если уже «что-то логируете», но не чувствуете контроля над цепочками вызовов.


Почему логи перестают помогать: три системные причины

Логи как строки: сложно анализировать

Большинство проблем начинается с формы: logger.info("user login failed: " + err) — это строка. При этом в системе почти всегда есть атрибуты, которые нужны для фильтрации и анализа: userId, ip, errorCode, route, latency, serviceName. Когда они не выделены как поля, вы теряете возможность эффективно запросить/агрегировать данные в поиске или в аналитике.

Нет корреляции: вы видите части, но не связь

В микросервисах один запрос превращается в серию вызовов: API → сервис авторизации → сервис заказов → БД и так далее. Без коррелятора вы будете каждый раз восстанавливать цепочку вручную, подбирая похожие времена и IP. Это не «наблюдаемость», это расследование.

Нет единого формата событий: вы вынуждены угадывать

Если разные сервисы логируют по-разному — разные поля, разные названия, разные уровни логирования — то единый дашборд превращается в набор хака. Даже простая выгрузка «все ошибки по userId» превращается в ручную работу.


Минимальный стандарт логирования для API: состав, правила, формат

Цель — не построить идеальный observability stack, а добиться результата «в один вечер»: чтобы любой запрос можно было отследить по логам как цепочку событий, и чтобы это не требовало знаний конкретной реализации.

Что логируем: события как контракт

С точки зрения операционной поддержки полезно мыслить не «логировать всё», а «фиксировать события жизненного цикла запроса и бизнеса». Минимальный набор для API:

  1. HTTP вход: старт обработки запроса (метаданные запроса).
  2. HTTP выход: завершение с кодом ответа, временем, итоговым статусом.
  3. Бизнес-ошибка: ожидаемые ошибки домена (например, «карта просрочена», «недостаточно прав»).
  4. Техническая ошибка: неожиданные исключения (таймауты, падения, ошибки интеграций).
  5. Внешние вызовы (если у вас микросервисы или интеграции): запрос к соседнему сервису/БД/очереди с результатом и latency.
  6. Решения/ветвления (по желанию, но крайне полезно): например, «выбрали стратегию A вместо B» или «фича включена».

Ключевой принцип: каждое событие должно быть машиночитаемым и связанным с контекстом запроса.

Что добавляем в каждый лог-сообщение: поля контекста

Чтобы «логи читались как граф запросов», вводим набор полей, которые в идеале присутствуют во всех сервисах:

  • timestamp — время (обычно ISO-8601 в UTC).
  • level — уровень (debug/info/warn/error).
  • service — имя сервиса.
  • env — окружение (prod/stage/dev).
  • traceId или correlationId — идентификатор корреляции.
  • spanId (если используете tracing) — необязательно для старта, но полезно.
  • event — тип события (например, http.request_started, http.request_finished).
  • method, path — для HTTP.
  • statusCode — для ответа.
  • durationMs — длительность обработки.
  • requestId (если у вас есть отдельный ID от сервера/балансировщика) — опционально.
  • userId / clientId — если это бизнес-атрибут и безопасно.
  • error — структурированный объект с code, message, type (и без утечек).
  • tags — массив/объект для произвольных меток (редко и аккуратно).

Важный нюанс: не пытайтесь логировать слишком много. В наблюдаемости лучше иметь стабильный минимальный набор и жёсткие правила приватности, чем «всё подряд», а потом не иметь возможности искать.

correlation-id: что это и чем отличается от traceId

correlation-id (иногда в командах его называют traceId) — это идентификатор, который вы прокидываете сквозь границы сервисов. Его главная задача: по одному значению вы можете восстановить путь запроса.

Если вы позже перейдёте к полноценному distributed tracing (OpenTelemetry), traceId и spanId станут частью стандарта. Но correlation-id можно внедрить быстрее: он не требует немедленно полного tracing backend.

Единый формат событий: один JSON в строке

Практика: лог-сообщение должно быть одним JSON-объектом в строке. Так проще интегрироваться с лог-агрегатором (ELK/OpenSearch, Loki, Datadog logs и т.д.) и проще проверять схемы.

Пример «целевого» JSON-формата события:

code
{
  "timestamp": "2026-07-25T10:12:34.567Z",
  "level": "info",
  "env": "prod",
  "service": "api-gateway",
  "correlationId": "5c2f1b2a6f9e4c8f",
  "event": "http.request_finished",
  "method": "POST",
  "path": "/v1/orders",
  "statusCode": 201,
  "durationMs": 87,
  "clientIp": "203.0.113.10",
  "userId": "u_193847"
}

Обратите внимание: поля типизированы (числа, строки, объекты), а не «всё в message».


correlation-id сквозь API и сервисы: как внедрить без магии

Источник correlation-id: заголовок запроса

Обычно корреляцию задают через HTTP-заголовок. Часто используют X-Correlation-Id, иногда — traceparent из W3C Trace Context.

Для минимума начните с собственного заголовка:

  • При входе в API: если клиент прислал X-Correlation-Id, используйте его.
  • Если нет: сгенерируйте новый.
  • При вызове других сервисов: прокиньте заголовок дальше.

Критично: выбирайте один заголовок и придерживайтесь его во всей системе.

Генерация ID и контекст потока

В Node.js, Java и Go есть разные способы хранить контекст запроса (AsyncLocalStorage, ThreadLocal, context.Context). Суть одна: correlation-id должен быть доступен в логгере в любой точке обработки запроса.

Ниже пример для Node.js (минимально, но рабоче).

Пример: корреляция в Express + структурные логи

code
import express from "express";
import crypto from "crypto";

const app = express();

// Простейшая генерация correlation-id
function newCorrelationId() {
  return crypto.randomBytes(8).toString("hex"); // 16 hex chars
}

// Ассинхронный контекст
import { AsyncLocalStorage } from "async_hooks";
const als = new AsyncLocalStorage();

function logger(eventObj) {
  const ctx = als.getStore() || {};
  // Гарантируем наличие correlationId в каждом лог-сообщении
  const correlationId = eventObj.correlationId || ctx.correlationId;

  const logLine = {
    timestamp: new Date().toISOString(),
    level: eventObj.level ?? "info",
    service: process.env.SERVICE_NAME ?? "api",
    env: process.env.NODE_ENV ?? "dev",
    correlationId,
    ...eventObj,
  };

  // Один JSON в строку
  process.stdout.write(JSON.stringify(logLine) + "\n");
}

app.use((req, res, next) => {
  const correlationId = req.header("X-Correlation-Id") || newCorrelationId();
  const store = { correlationId };

  const started = Date.now();
  logger({
    level: "info",
    event: "http.request_started",
    method: req.method,
    path: req.originalUrl,
    // Берём только то, что безопасно логировать
    clientIp: req.ip,
  });

  als.run(store, () => {
    res.on("finish", () => {
      logger({
        level: res.statusCode >= 500 ? "error" : "info",
        event: "http.request_finished",
        method: req.method,
        path: req.originalUrl,
        statusCode: res.statusCode,
        durationMs: Date.now() - started,
        // Можно добавить userId из auth-мидлвари (если туда сохраняете в req)
        userId: req.user?.id,
      });
    });

    next();
  });
});

// Эмуляция обработки
app.post("/v1/orders", async (req, res) => {
  try {
    // Пример: бизнес-ошибка
    if (!req.body?.itemId) {
      logger({
        level: "warn",
        event: "business.error",
        error: {
          code: "ORDER_ITEM_MISSING",
          message: "itemId is required",
        },
      });
      return res.status(400).json({ error: "ORDER_ITEM_MISSING" });
    }

    // Пример: успешный ответ
    res.status(201).json({ ok: true });
  } catch (e) {
    logger({
      level: "error",
      event: "technical.error",
      error: {
        code: "UNHANDLED_EXCEPTION",
        message: e.message,
        type: e.name,
      },
    });
    res.status(500).json({ error: "INTERNAL_ERROR" });
  }
});

app.listen(3000, () => console.log("listening on 3000"));

Что важно в коде:

  • correlation-id хранится в контексте запроса (AsyncLocalStorage), чтобы логгер всегда мог его подмешать.
  • мы логируем start и finish события с durationMs.
  • ошибки логируются структурировано, с кодом, типом и сообщением (без стека наружу в production, если нет необходимости).

Прокидывание correlation-id при вызовах других сервисов

Дальше — внешние вызовы. Для API → сервис заказов → сервис платежей нужно пробросить тот же X-Correlation-Id.

Пример (условный fetch):

code
async function callOrdersService({ correlationId, payload }) {
  const response = await fetch("http://orders/v1/internal/orders", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Correlation-Id": correlationId,
    },
    body: JSON.stringify(payload),
  });

  return response;
}

// Использование внутри хэндлера:
app.post("/v1/payments", async (req, res) => {
  const ctx = als.getStore();
  const correlationId = ctx.correlationId;

  const t0 = Date.now();
  logger({ level: "info", event: "external.call_started", target: "orders", url: "/v1/internal/orders" });

  try {
    const r = await callOrdersService({ correlationId, payload: req.body });
    logger({
      level: "info",
      event: "external.call_finished",
      target: "orders",
      statusCode: r.status,
      durationMs: Date.now() - t0,
    });

    if (!r.ok) {
      return res.status(502).json({ error: "ORDERS_SERVICE_ERROR" });
    }

    res.status(200).json({ ok: true });
  } catch (e) {
    logger({
      level: "error",
      event: "external.call_failed",
      target: "orders",
      durationMs: Date.now() - t0,
      error: { code: "NETWORK_ERROR", message: e.message, type: e.name },
    });
    res.status(502).json({ error: "BAD_GATEWAY" });
  }
});

Смысл: вы видите граф не только на уровне HTTP API, но и на уровне интеграций.


Структурные логи: как сделать формат устойчивым

Почему «структурно» ≠ «просто JSON»

Люди часто делают первый шаг: logger.info({ msg: "..." , userId }). Но если в разных местах поля называются по-разному (user_id vs userId), или местами логгер добавляет разные структуры ошибок — вы теряете преимущества.

Устойчивость достигается договорённостью:

  • единые имена полей;
  • единые типы значений;
  • единые event-категории;
  • единая форма ошибок.

Схема ошибки: один стандарт на весь домен

Рекомендуемый минимальный формат:

  • error.code — короткий, стабильный идентификатор (удобен для статистики).
  • error.message — человекочитаемое сообщение (не обязательно отличающееся от code).
  • error.type — имя класса/категории (опционально).
  • error.details — объект с дополнительными данными (опционально и безопасно).

Пример:

code
{
  "event": "technical.error",
  "error": {
    "code": "TIMEOUT_UPSTREAM",
    "message": "orders request timed out after 5000ms",
    "type": "TimeoutError"
  }
}

Важно: не включайте stack trace в каждый прод лог в проде. Он раздувает стоимость и мешает машинной обработке. Стек уместен точечно (sampling или только при определённых условиях).

Уровни логирования: прагматичный подход

  • info: вход/выход запросов, важные состояния без ошибок (start/finish, external call finished).
  • warn: ожидаемые проблемы, которые не должны ломать сервис целиком (валидация, бизнес-отказ, деградация).
  • error: ошибки, которые требуют вмешательства или указывают на сбои (технические исключения, 5xx, failed external call).

debug оставьте для локальной диагностики или sampling. В проде debug часто становится «пылесосом» и дорогой помойкой.


Как читать логи как граф запросов: методика

Когда у вас есть correlationId, чтение становится более инженерным: вы не ищете «по тексту», вы строите маршрут.

Шаг 1: выберите correlationId и посмотрите все события

Типичный workflow:

  1. Найдите один проблемный запрос: например, 500 на /v1/orders.
  2. Скопируйте correlationId из логов http.request_finished (или из ответа/headers, если вы возвращаете его клиенту).
  3. Запросите все события с этим correlationId в лог-агрегаторе.

В идеале вы увидите последовательность:

  • http.request_started в API
  • external.call_started → external.call_finished к соседнему сервису
  • technical.error или business.error
  • http.request_finished с 500/4xx

Шаг 2: восстановите причинность по времени

Даже без полноценного trace вы сможете увидеть:

  • где выросла latency (разница durationMs).
  • где произошла остановка (последнее событие перед ошибкой).
  • повторения (например, несколько внешних вызовов в цикле).

Шаг 3: проверьте согласованность формата

Иногда вы думаете, что «корреляция не работает», но на деле:

  • один сервис логирует под другим именем поля (correlationID vs correlationId);
  • один сервис не прокидывает заголовок;
  • лог-агрегатор парсит JSON неправильно.

Самопроверка: пройдитесь по документации формата в нескольких сервисах и проверьте, что поля действительно извлекаются как отдельные параметры.


Чек-лист: что логировать, а что нет

Этот раздел важен не меньше, чем техническая реализация. Логирование — это расходы и риск. Стабильный стандарт должен заранее отвечать на вопросы «как не утонуть» и «как не утечь».

Что логировать обязательно

  1. HTTP start/finish для каждого входящего запроса в API:
    • method, path, статус, durationMs, correlationId.
  2. Внешние вызовы:
    • target сервис/зависимость, статус результата, durationMs.
  3. Ошибки:
    • бизнес-ошибки с error.code (для аналитики отказов);
    • технические с error.code и категорией (для инцидентов).
  4. События, влияющие на поведение:
    • выбор стратегии, фича-флаг, режим, версию контракта (по делу, не всегда).

Что логировать выборочно

  1. Параметры запроса (body/query):
    • только безопасные поля;
    • либо редактирование/маскирование (PII, токены, номера карт).
  2. userId/clientId:
    • если это помогает найти влияние на конкретных пользователей и нет приватности проблем.
  3. Полезные подсказки для расследования:
    • например, orderId при ошибке создания

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

Базовый курс для обычных людей: что такое защищённое соединение, как оно устроено, что оно умеет и чего не умеет, как выбрать сервис и настроить его на своих устройствах.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

Практический курс по безопасности сети: как собрать собственный сервис защищённого доступа - от пустого сервера до подписки с автовыбором - и, что важнее, как потом не уронить его своими руками.

Перейти к курсу

Другие статьи

Работа с Gantt-логикой задач: как проектировать дедлайны, ретраи и ограничения в асинхронных сервисах
работа

Работа с Gantt-логикой задач: как проектировать дедлайны, ретраи и ограничения в асинхронных сервисах

Покажем практический шаблон управления временем выполнения: дедлайн на весь запрос, отмена вложенных задач, ретраи с backoff и защита от каскадных зависаний при сетевых ошибках.

17 сентября 2026 г.
1010
Мониторинг ошибок в Python: Sentry (или аналоги) + трассировка запросов от клиента до базы
мониторинг

Мониторинг ошибок в Python: Sentry (или аналоги) + трассировка запросов от клиента до базы

Как настроить сборку исключений и контекста (user_id, correlation-id, параметры запроса) так, чтобы ошибки были воспроизводимыми и сопоставимыми между релизами. Разберём, какие события логировать, а какие лучше не спамить, и как привязать отчёты к конкрет

5 августа 2026 г.
620
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1090
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Почему логи перестают помогать: три системные причиныЛоги как строки: сложно анализироватьНет корреляции: вы видите части, но не связьНет единого формата событий: вы вынуждены угадыватьМинимальный стандарт логирования для API: состав, правила, форматЧто логируем: события как контрактЧто добавляем в каждый лог-сообщение: поля контекстаcorrelation-id: что это и чем отличается от traceIdЕдиный формат событий: один JSON в строкеcorrelation-id сквозь API и сервисы: как внедрить без магииИсточник correlation-id: заголовок запросаГенерация ID и контекст потокаПрокидывание correlation-id при вызовах других сервисовСтруктурные логи: как сделать формат устойчивымПочему «структурно» ≠ «просто JSON»Схема ошибки: один стандарт на весь доменУровни логирования: прагматичный подходКак читать логи как граф запросов: методикаШаг 1: выберите correlationId и посмотрите все событияШаг 2: восстановите причинность по времениШаг 3: проверьте согласованность форматаЧек-лист: что логировать, а что нетЧто логировать обязательноЧто логировать выборочно