›Теперь у меня новый блестящий блог. Прочтите эту статью с последними обновлениями там https://blog.goncharov.page/nodejs-logging-made-right

Что вас больше всего беспокоит, когда вы думаете о входе в NodeJS? Если вы спросите меня, я скажу отсутствие отраслевых стандартов для создания идентификаторов трассировки. В этой статье мы рассмотрим, как мы можем создать эти идентификаторы трассировки (это означает, что мы собираемся кратко изучить, как работает локальное хранилище продолжения, также известное как CLS), и углубимся в то, как мы можем использовать Прокси, чтобы заставить его работать с ЛЮБЫМ регистратор.

Почему вообще проблема иметь идентификатор трассировки для каждого запроса в NodeJS?

Ну, на платформах, которые используют многопоточность и порождают новый поток для каждого запроса. Существует вещь, называемая локальное хранилище потока, также известное как TLS, которая позволяет хранить любые произвольные данные, доступные для чего-либо в потоке. Если у вас есть собственный API для этого, довольно просто создать случайный идентификатор для каждого запроса, поместить его в TLS и использовать в своем контроллере или службе позже. Так в чем же дело с NodeJS? Как вы знаете, NodeJS - это однопоточная платформа (это уже не совсем так, поскольку у нас теперь есть рабочие, но это не меняет общей картины), что делает TLS устаревшим. Вместо работы с разными потоками NodeJS запускает разные обратные вызовы в одном потоке (есть отличная серия статей о циклах событий в NodeJS, если вам интересно), а NodeJS предоставляет нам способ однозначно идентифицировать эти обратные вызовы и отслеживать их отношения с друг с другом.

В былые времена (v0.11.11) у нас был addAsyncListener, который позволял нам отслеживать асинхронные события. На его основе Форрест Норвелл построил первую реализацию продолжения локального хранилища a.k.a. CLS. Мы не собираемся описывать эту реализацию CLS из-за того, что мы, как разработчики, уже лишились этого API в версии 0.12.

До NodeJS 8 у нас не было официального способа подключения к асинхронной обработке событий NodeJS. И, наконец, NodeJS 8 предоставил нам силу, которую мы потеряли через async_hooks (если вы хотите лучше понять async_hooks, посмотрите эту статью). Это подводит нас к современной реализации CLS на основе async_hooks - cls-hooked.

Обзор CLS

Вот упрощенная схема работы CLS:

Давайте разберемся по порядку:

  1. Скажем, у нас есть типичный веб-сервер. Сначала нам нужно создать пространство имен CLS. Один раз за все время существования нашего приложения.
  2. Во-вторых, нам нужно настроить промежуточное ПО для создания нового контекста CLS для каждого запроса. Для простоты предположим, что это промежуточное ПО - это просто обратный вызов, который вызывается при получении нового запроса.
  3. Поэтому, когда поступает новый запрос, мы вызываем эту функцию обратного вызова.
  4. Внутри этой функции мы создаем новый контекст CLS (один из способов - использовать вызов API run).
  5. На этом этапе CLS помещает новый контекст в карту контекстов по текущему идентификатору выполнения.
  6. Каждое пространство имен CLS имеет свойство active. На этом этапе CLS присваивает active контексту.
  7. Внутри контекста мы вызываем асинхронный ресурс, например, запрашиваем данные из базы данных. Мы передаем обратный вызов вызову, который будет запущен после завершения запроса к базе данных.
  8. Асинхронный хук init запускается для новой асинхронной операции. Он добавляет текущий контекст на карту контекстов по асинхронному идентификатору (считайте его идентификатором новой асинхронной операции). Теперь на этой карте мы имеем два идентификатора, указывающих на один и тот же контекст.
  9. Поскольку у нас больше нет логики внутри нашего первого обратного вызова, он фактически завершает нашу первую асинхронную операцию.
  10. Асинхронный хук after запускается для первого обратного вызова. Он устанавливает активный контекст в пространстве имен на undefined (это не всегда верно, поскольку у нас может быть несколько вложенных контекстов, но в простейшем случае это правда).
  11. Хук destroy запускается при первой операции. Он удаляет контекст из нашей карты контекстов по его асинхронному идентификатору (он совпадает с текущим идентификатором выполнения нашего первого обратного вызова).
  12. Запрос к базе данных завершен, и вот-вот будет запущен наш второй обратный вызов.
  13. На этом этапе в игру вступает асинхронный хук до. Его текущий идентификатор выполнения совпадает с асинхронным идентификатором второй операции (запроса к базе данных). Он устанавливает свойство active пространства имен в контекст, найденный по его текущему идентификатору выполнения. Это контекст, который мы создали раньше.
  14. Теперь мы запускаем наш второй обратный вызов. Запустите бизнес-логику внутри. Внутри этой функции мы можем получить любое значение по ключу из CLS, и она вернет все, что найдет по ключу в контексте, который мы создали ранее.
  15. Предполагая, что это конец обработки запроса, возвращаемого нашей функцией.
  16. Асинхронный хук after запускается для второго обратного вызова. Он устанавливает активный контекст в пространстве имен на undefined.
  17. destroy запускается для второй асинхронной операции. Он удаляет наш контекст из карты контекстов по его асинхронному идентификатору, оставляя его абсолютно пустым.
  18. Поскольку у нас больше нет ссылок на объект контекста, наш сборщик мусора освобождает связанную с ним память.

Это упрощенная версия того, что происходит под капотом, но она охватывает все основные этапы. Если вы хотите копнуть глубже, вы можете взглянуть на исходный код. Менее 500 строк.

Создание идентификаторов трассировки

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

Для экспресс это промежуточное ПО могло выглядеть так:

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

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

Добавим в наш winston.

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

Объединение прокси и CLS

Прокси - это объект, который обертывает наш исходный объект, позволяя нам переопределить его поведение в определенных ситуациях. Список таких ситуаций (фактически они называются ловушками) ограничен, и вы можете просмотреть весь набор здесь, но нас интересует только ловушка get. Это дает нам возможность перехватывать доступ к свойствам. Это означает, что если у нас есть объект const a = { prop: 1 } и обернуть его в прокси, с get ловушкой мы могли бы вернуть все, что захотим, для a.prop.

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

В этом случае наш прокси может выглядеть так:

Наше промежуточное ПО трансформируется во что-то вроде этого:

И мы могли бы использовать логгер так:

Cls-proxify

На основе вышеизложенной идеи была создана небольшая библиотека под названием cls-proxify. Имеет встроенную интеграцию с express, koa и fastify. Он применяет не только get ловушку к исходному объекту, но и многие другие. Итак, существует бесконечное количество возможных применений. Вы можете проксировать вызовы функций, создание классов, почти все что угодно! Вы ограничены только своим воображением! Взгляните на живые демонстрации использования его с pino и fastify, pino и express.

Надеюсь, вы нашли что-то полезное для своего проекта. Не стесняйтесь сообщать мне свой отзыв! Я, безусловно, ценю любую критику и вопросы.

Следите за новыми статьями, подписавшись на меня в Twitter или LinkedIn! Подпишитесь на мою рассылку новостей или RSS. Напишите мне электронное письмо, если у вас возникнут вопросы.