Рубрики
Без рубрики

Использование Async Hooks для обработки контекста запроса в Node.js

Node.js Async Hooks предоставляет API для отслеживания времени жизни асинхронных ресурсов в приложении Node. Давайте используем Async Hooks для отслеживания HTTP-запросов.

Вступление

Async Hooks – это основной модуль в Node.js, который предоставляет API для отслеживания времени жизни асинхронных ресурсов в приложении Node. Асинхронный ресурс можно представить как объект, имеющий связанный с ним обратный вызов.

Примеры включают, но не ограничиваются ими: Promises, Timeouts, TCPWrap, UDP и т.д. Весь список асинхронных ресурсов, которые мы можем отслеживать с помощью этого API, можно найти здесь.

Функция Async Hooks была представлена в 2017 году в Node.js версии 8 и до сих пор является экспериментальной. Это означает, что в будущих выпусках API могут быть внесены обратно несовместимые изменения. Тем не менее, в настоящее время он не считается пригодным для производства.

В этой статье мы более подробно рассмотрим Async Hooks – что это такое, почему они важны, где мы можем их использовать и как мы можем использовать их для конкретного случая использования, то есть обработки контекста запроса в приложении Node.js и Express.

Что такое Async Hooks?

Как было сказано ранее, класс Async Hooks – это основной модуль Node.js, который предоставляет API для отслеживания асинхронных ресурсов в вашем приложении Node.js. Сюда также входит отслеживание ресурсов, созданных родными модулями Node, такими как fs и net.

Во время жизни асинхронного ресурса происходит 4 события, которые мы можем отслеживать с помощью Async Hooks. К ним относятся:

  • init – Вызывается во время создания асинхронного ресурса
  • before – Вызывается перед обратным вызовом ресурса
  • after – Вызывается после того, как обратный вызов ресурса был вызван
  • destroy – Вызывается после уничтожения асинхронного ресурса
  • promiseResolve – Вызывается при вызове функции resolve() Promise.

Ниже приведен обобщенный фрагмент API Async Hooks из обзора в документации Node.js:

const async_hooks = require('async_hooks');

const exec_id = async_hooks.executionAsyncId();
const trigger_id = async_hooks.triggerAsyncId();
const asyncHook = async_hooks.createHook({
  init: function (asyncId, type, triggerAsyncId, resource) { },
  before: function (asyncId) { },
  after: function (asyncId) { },
  destroy: function (asyncId) { },
  promiseResolve: function (asyncId) { }
});
asyncHook.enable();
asyncHook.disable();

Метод executionAsyncId() возвращает идентификатор текущего контекста выполнения.

Метод triggerAsyncId() возвращает идентификатор родительского ресурса, который вызвал выполнение асинхронного ресурса.

Метод createHook() создает экземпляр асинхронного крючка, принимая вышеупомянутые события в качестве необязательных обратных вызовов.

Чтобы включить отслеживание наших ресурсов, мы вызываем метод enable() нашего экземпляра асинхронного крючка, который мы создаем с помощью метода createHook().

Мы также можем отключить отслеживание, вызвав функцию disable().

Рассмотрев, что собой представляет API Async Hooks, давайте разберемся, зачем его использовать.

Когда использовать Async Hooks

Добавление Async Hooks к основному API обеспечило множество преимуществ и вариантов использования. Некоторые из них включают:

  • Улучшенная отладка – используя Async Hooks, мы можем улучшить и обогатить трассировку стека асинхронных функций.
  • Мощные возможности трассировки, особенно в сочетании с API производительности Node. Кроме того, поскольку Async Hooks API является родным, накладные расходы на производительность минимальны.
  • Обработка контекста веб-запроса – получение информации о запросе во время его жизни, без передачи объекта запроса повсюду. С помощью Async Hooks это можно сделать в любом месте кода и может быть особенно полезно при отслеживании поведения пользователей на сервере.

В этой статье мы рассмотрим, как обрабатывать трассировку идентификатора запроса с помощью Async Hooks в приложении Express.

Использование Async Hooks для обработки контекста запроса

В этом разделе мы проиллюстрируем, как можно использовать Async Hooks для выполнения простой трассировки идентификатора запроса в приложении Node.js.

Настройка обработчиков контекста запроса

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

mkdir async_hooks && cd async_hooks 

Далее нам нужно инициализировать наше приложение Node.js в этом каталоге с помощью npm и настроек по умолчанию:

npm init -y

Это создаст файл package.json в корне каталога.

Далее нам нужно установить пакеты Express и uuid в качестве зависимостей. Мы будем использовать пакет uuid для генерации уникального идентификатора для каждого входящего запроса.

Наконец, мы установим модуль esm, чтобы версии Node.js ниже v14 могли запустить этот пример:

npm install express uuid esm --save

Далее создайте файл hooks.js в корне каталога:

touch hooks.js

Этот файл будет содержать код, взаимодействующий с модулем async_hooks. Он экспортирует две функции:

  • Одна из них включает Async Hook для HTTP-запроса, отслеживая его заданный ID запроса и любые данные запроса, которые мы хотели бы сохранить.
  • Другая возвращает данные запроса, управляемые хуком, учитывая его идентификатор Async Hook.

Давайте запишем это в коде:

require = require('esm')(module);
const asyncHooks = require('async_hooks');
const { v4 } = require('uuid');
const store = new Map();

const asyncHook = asyncHooks.createHook({
    init: (asyncId, _, triggerAsyncId) => {
        if (store.has(triggerAsyncId)) {
            store.set(asyncId, store.get(triggerAsyncId))
        }
    },
    destroy: (asyncId) => {
        if (store.has(asyncId)) {
            store.delete(asyncId);
        }
    }
});

asyncHook.enable();

const createRequestContext = (data, requestId = v4()) => {
    const requestInfo = { requestId, data };
    store.set(asyncHooks.executionAsyncId(), requestInfo);
    return requestInfo;
};

const getRequestContext = () => {
    return store.get(asyncHooks.executionAsyncId());
};

module.exports = { createRequestContext, getRequestContext };

В этом фрагменте кода мы сначала требуем модуль esm, чтобы обеспечить обратную совместимость для версий Node, которые не имеют встроенной поддержки экспорта экспериментальных модулей. Эта возможность используется внутри модуля uuid.

Далее нам также необходимы модули async_hooks и uuid. Из модуля uuid мы деструктурируем функцию v4, которую мы будем использовать позже для генерации UUID версии 4.

Далее мы создаем хранилище, которое будет сопоставлять каждый асинхронный ресурс с его контекстом запроса. Для этого мы используем простую карту JavaScript.

Далее мы вызываем метод createHook() модуля async_hooks и реализуем обратные вызовы init() и destroy(). При реализации обратного вызова init() мы проверяем, присутствует ли идентификатор triggerAsyncId в хранилище.

Если он существует, мы создаем отображение asyncId на данные запроса, хранящиеся под triggerAsyncId. Это фактически гарантирует, что мы храним один и тот же объект запроса для дочерних асинхронных ресурсов.

Обратный вызов destroy() проверяет, есть ли в хранилище asyncId ресурса, и удаляет его, если это так.

Чтобы использовать наш хук, мы включаем его, вызывая метод enable() созданного нами экземпляра asyncHook.

Далее мы создаем две функции – createRequestContext() и getRequestContext, которые мы используем для создания и получения контекста запроса соответственно.

Функция createRequestContext() получает данные запроса и уникальный ID в качестве аргументов. Затем она создает объект requestInfo из обоих аргументов и пытается обновить хранилище с async ID текущего контекста выполнения в качестве ключа и requestInfo в качестве значения.

Функция getRequestContext(), с другой стороны, проверяет, есть ли в хранилище ID, соответствующий ID текущего контекста выполнения.

В заключение мы экспортируем обе функции, используя синтаксис module.exports().

Мы успешно настроили нашу функциональность обработки контекста запроса. Давайте перейдем к настройке нашего сервера Express, который будет принимать запросы.

Настройка сервера Express

Настроив наш контекст, мы теперь будем приступить к После настройки контекста мы переходим к созданию нашего Express-сервера, чтобы мы могли перехватывать HTTP-запросы. Для этого создайте файл server.js в корне каталога следующим образом:

touch server.js

Наш сервер будет принимать HTTP-запрос на порт 3000. Он создает Async Hook для отслеживания каждого запроса, вызывая createRequestContext() в промежуточной функции – функции, которая имеет доступ к объектам запроса и ответа HTTP. Затем сервер отправляет JSON-ответ с данными, захваченными Async Hook.

Внутри файла server.js введите следующий код:

const express = require('express');
const ah = require('./hooks');
const app = express();
const port = 3000;

app.use((request, response, next) => {
    const data = { headers: request.headers };
    ah.createRequestContext(data);
    next();
});

const requestHandler = (request, response, next) => {
    const reqContext = ah.getRequestContext();
    response.json(reqContext);
    next()
};

app.get('/', requestHandler)

app.listen(port, (err) => {
    if (err) {
        return console.error(err);
    }
    console.log(`server is listening on ${port}`);
});

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

Далее мы устанавливаем промежуточное ПО, которое разрушает заголовки запросов, сохраняя их в переменной data. Затем она вызывает функцию createRequestContext(), передавая данные в качестве аргумента. Это гарантирует, что заголовки запроса будут сохранены на протяжении всего жизненного цикла запроса с помощью Async Hook.

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

После нашего промежуточного ПО мы пишем функцию requestHandler(), которая обрабатывает GET-запрос на корневом домене сервера. Вы заметите, что в этой функции мы можем получить доступ к нашему контексту запроса через функцию getRequestContext(). Эта функция возвращает объект, представляющий заголовки запроса и идентификатор запроса, сгенерированные и сохраненные в контексте запроса.

Затем мы создаем простую конечную точку и подключаем наш обработчик запроса в качестве обратного вызова.

Наконец, мы заставляем наш сервер прослушивать соединения на порту 3000, вызывая метод listen() нашего экземпляра приложения.

Перед запуском кода откройте файл package.json в корне каталога и замените раздел test скрипта на следующий:

"start": "node server.js"

После этого мы можем запустить наше приложение с помощью следующей команды:

npm start

Вы должны получить ответ на терминале о том, что приложение запущено на порту 3000, как показано на рисунке:

> async-hooks-demo@1.0.0 start /Users/allanmogusu/StackAbuse/async-hooks-demo
> node server.js

(node:88410) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time
server is listening on 3000

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

curl http://localhost:3000

Эта команда curl выполняет GET-запрос к нашему маршруту по умолчанию. Вы должны получить ответ, подобный этому:

$ curl http://localhost:3000
{"requestId":"3aad88a6-07bb-41e0-ab5a-fa9d5c0269a7","data":{"headers":{"host":"localhost:3000","user-agent":"curl/7.64.1","accept":"*/*"}}}%

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

$ curl http://localhost:3000
{"requestId":"38da84792-e782-47dc-92b4-691f4285b172","data":{"headers":{"host":"localhost:3000","user-agent":"curl/7.64.1","accept":"*/*"}}}%

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

Заключение

Async Hooks предоставляет API для отслеживания времени жизни асинхронных ресурсов в приложении Node.js.

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

Однако, начиная с 14 версии Node.js, Async Hooks API поставляется с async local storage, API, который упрощает работу с контекстом запроса в Node.js. Подробнее об этом можно прочитать здесь. Кроме того, код для этого руководства доступен здесь.

Оригинал: “https://stackabuse.com/using-async-hooks-for-request-context-handling-in-node-js/”