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

Зачем это вообще нужно? Ответ очевиден - упростить поиск и устранение неисправностей и мониторинг наших приложений. Давайте посмотрим на пример. Есть приложение, которое содержит набор сервисов (шлюз, статьи, расчеты цен, налоги и скидки).

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

Реализовать собственное приложение

В этой статье я собираюсь реализовать две службы, которые работают вместе (ссылка на репозиторий github с кодом доступна в конце статьи). Одна услуга - экспресс, вторая - коа. Каждый из них использует следующие библиотеки:

  • Winston - библиотека логирования
  • Cls-hooked - обертка асинхронных хуков
  • Morgan - промежуточное ПО для регистратора HTTP-запросов

Экспресс-служба предоставит товар с базовой ценой, но окончательная цена будет рассчитана второй службой.

Хранение корреляционных идентификаторов

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

Есть два подхода к хранению идентификатора корреляции в Node.JS:

  1. Создайте экземпляр регистратора для каждого запроса с указанным идентификатором корреляции и передайте этот регистратор для каждой функции во время потока выполнения. Лично мне не нравится такой подход к ведению журнала, потому что он требует от разработчика множества явных действий, а также, если требуется внедрить в существующий код, количество изменений будет огромным.
  2. Используйте асинхронные хуки. Это своего рода переменная ThreadLocal для NodeJS. Недостатком этого подхода является то, что асинхронные перехватчики не являются стабильной функциональностью, и ваш идентификатор корреляции может быть потерян (лично у нас есть редкие случаи потери идентификаторов корреляции в нашем продукте). Вот хорошая статья, описывающая, как работают асинхронные хуки https://medium.com/the-node-js-collection/async-hooks-in-node-js-illustrated-b7ce1344111f

В этой статье будет реализован подход на основе асинхронных хуков.

Итак, давайте создадим абстракцию для идентификатора корреляции

Абстракция предоставляет следующие функции, которые потребуются позже:

  • withId - выполняет указанную функцию с идентификатором корреляции. Если идентификатор отсутствует, создается новый идентификатор.
  • getId - предоставляет текущий идентификатор корреляции.
  • bindEmitter - связывает EventEmitter с пространством имен идентификатора корреляции. Подробнее. Кстати, самая популярная причина потери идентификатора корреляции - это то, что эмиттеры событий не связаны контекстом.
  • bind - связывает функцию с пространством имен идентификатора корреляции. Подробнее.

Регистратор

Ведение журнала реализовано через библиотеку Winston. Единственное, что здесь нужно сделать, - это добавить соответствующие средства форматирования для добавления и печати идентификатора корреляции.

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

Реальный сценарий. У нас был потерян идентификатор корреляции для запросов, которые были прерваны сервером Node.JS из-за тайм-аута. Записи были найдены при проверке журнала с фильтрацией по nocorrelation значению идентификатора корреляции.

Приложение Koa - Сервис расчета цен

Давайте реализуем наше первое приложение, которое отвечает за расчет цен. Он берет базовую цену и страну и увеличивает цену, применяя налоговые ставки (нет цели создать приложение для электронной коммерции, поэтому примеры просто показывают, как работает идентификатор корреляции).

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

  • Он должен связывать источники запросов, ответов и событий сокета с контекстом идентификатора корреляции, чтобы любые инициированные события были связаны в текущем контексте идентификатора корреляции.
  • Он должен выполнять поток запросов в пределах значения идентификатора корреляции.
  • Кроме того, необходимо повторно привязать __onFinished функцию. Koa использует __onFinished под капотом, когда начинает обрабатывать запрос, а morgan ссылается на него позже. Если мы пропустим повторную привязку, то запись журнала ответов будет без идентификатора корреляции.

Теперь мы готовы внедрить наш сервис.

Экспресс-заявка - Сервис статей

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

  1. внедрить промежуточное программное обеспечение экспресс-обработки для выполнения обработки запроса в контексте идентификатора корреляции
  2. предоставить масштабируемый способ вызова других сервисов с текущим идентификатором корреляции

Промежуточное ПО Express похоже на Koa, но без повторной привязки __onFinished, потому что оно не используется внутренними компонентами Express.

Пришло время создать код для передачи идентификатора корреляции при обращении к другим службам. Есть два подхода:

  1. Наивный. В этом случае мы можем добавить дополнительный заголовок при каждом вызове API.
  2. Украшаем существующий объект, отвечающий за отправку запросов. Большинство библиотек имеют такие возможности. В настоящее время я собираюсь использовать request-promise библиотеку, которая поддерживает функцию defaults, которая создает новый экземпляр request-promise с использованием моих значений по умолчанию. Также имейте в виду, что при определении заголовка по умолчанию для идентификатора корреляции необходимо использовать функцию-получатель, а не просто строку для значения. Потому что во время каждого звонка необходимо отправлять фактическое значение. Если используется чистая строка, все запросы будут иметь одинаковый идентификатор корреляции.

Теперь мы готовы внедрить наш сервис.

Результат

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

Также я обработал тысячи запросов с разными задержками. Результатом такого выполнения является журнал, в котором можно выяснить последовательность обработки запроса. Более того, если вы используете такой инструмент, как Kibana, он дает вам дополнительные возможности за счет возможностей фильтрации инструментов. Вы можете найти образец журнала ниже:

Исходный код доступен на GitHub https://github.com/evgeni-k/correlation-id-sample.

Мы разрабатываем платформу визуализации данных. Вы можете попробовать нашу платформу на https://vizydrop.com или https://trello.vizydrop.com.