Автор оригинала: Guilaume Besson.
Я недавно получил проблему, когда я строл HTTP-сервер в Nodejs. Я лежу в вещах во многих местах внутри моей кодовой базы, и у меня есть уникальный идентификатор для каждого запроса. Я хочу добавить этот идентификатор для каждого из моих сообщений журнала, чтобы проследить то, что происходит в каждом запросе. Как эффективно сделать это?
Самый простой способ – пройти этот идентификатор запроса в качестве аргумента в каждой из моих функций. Проблема с этим является то, что она не поддерживалась: если мне 5 функций глубоко в моем стеке, и я хочу воплотить что-то, мне нужно редактировать 5 функций, чтобы добавить аргумент и редактировать каждый вызов функции.
Вы можете всегда передавать объект «контекст» в каждой из ваших функций, но все еще есть проблема. Я использую SQL Lib, который можно настроить для запуска функции, когда обнаруживается длинный запрос, но эта функция называется только с строкой запроса. Я не могу пройти свой контекст запроса.
Если вы готовы использовать экспериментальные Nodejs, у меня действительно элегантное решение для вас благодаря async_hooks Отказ
Некоторые теория о Async_Hooks
async_hooks
Модуль разоблачает функции для отслеживания асинхронных ресурсов. Эти крючки называются всякий раз, когда Сетримс
Слушатель сервера или любая другая асинхронная задача создана, начата, закончена и уничтожена.
Когда создан асинхронный ресурс, новый Asyncid
будет назначен ему и наше init
Крюк будет вызван с этим идентификатором и Asyncid
родительского ресурса. Этот модуль также подвергает очень полезное ExecutionAsyncid ()
Метод получения текущего Asyncid
нашего исполнения функций.
Вот как мы можем использовать его для простого сообщения сообщения, когда крюки называются:
const fs = require('fs'); const async_hooks = require('async_hooks'); const log = (str) => fs.writeSync(1, `${str}\n`); async_hooks.createHook({ init(asyncId, type, triggerAsyncId) { log(`INIT: asyncId: ${asyncId} / type: ${type} / trigger: ${triggerAsyncId}`); }, destroy(asyncId) { log(`DESTROY: asyncId: ${asyncId}`); }, }).enable();
Вы можете заметить, что мы не используем console.log
здесь. Это потому, что console.log
это асинхронная операция и будет вызвать крючок, который позвонит console.log
Что является асинхронной операцией и будет вызвать крючок … и это случай бесконечной петли. Решение для использования Fs.writeync
который синхронно и не запускает один из наших крючков.
Давайте попробуем эти крючки простым Сетримс
Пример, в котором мы регистрируем текущий Asyncid
снаружи и внутри Сетримс
:
log(`>> Calling setTimeout: asyncId: ${async_hooks.executionAsyncId()}`); setTimeout(() => { log(`>> Inside setTimeout callback: asyncId: ${async_hooks.executionAsyncId()}`); }, 0); log(`>> Called setTimeout: asyncId: ${async_hooks.executionAsyncId()}`);
При выполнении этого кода мы получим этот выход:
>> Calling setTimeout: asyncId: 1 INIT: asyncId: 2 / type: Timeout / trigger: 1 >> Called setTimeout: asyncId: 1 >> Inside setTimeout callback: asyncId: 2 DESTROY: asyncId: 2
Посмотрим, что здесь произошло.
- Мы начали с
Asyncid
равный 1 и называетсяСетримс
Отказ - Это создало
Тайм-аут
асинхронный ресурс и вызвал нашиinit
крючок с недавно созданнымAsyncid
из 2 и его родительAsyncid
из 1. - Мы регистрировали конец нашей программы с помощью
Asyncid
все еще равняется 1 -
Тайм-аут
Вызов ресурса был вызван, и мы регистрировали текущийAsyncid
что равно 2 -
Тайм-аут
Ресурс был уничтожен и нашуничтожить
Крюк был вызван
Есть также два других крючка: до
и после
Отказ Их можно использовать для контроля времени некоторых асинхронных ресурсов, таких как внешние HTTP-запросы или SQL-запросы.
Хорошо, но какой смысл?
С ExecutionAsyncid ()
и init
Мы можем воссоздать «стек» наших функций звонков, даже если они были асинхронными.
Вот настоящий пример. Мы создаем HTTP-сервер, чтение и отправка содержимого A test.txt
Файл по каждому запросу.
const fs = require('fs'); const async_hooks = require('async_hooks'); const http = require('http'); const log = (str) => fs.writeSync(1, `${str}\n`); async_hooks.createHook({ init(asyncId, type, triggerAsyncId) { log(`asyncId: ${asyncId} / trigger: ${triggerAsyncId}`); }, }).enable(); const readAndSendFile = (res) => { fs.readFile('./test.txt', (err, file) => { log(`>> Inside readAndSendFile: execution: ${async_hooks.executionAsyncId()}`); res.end(file); }); } const requestHandler = (req, res) => { log(`>> Inside request: execution: ${async_hooks.executionAsyncId()}`); readAndSendFile(res); } const server = http.createServer(requestHandler); server.listen(8080);
Давайте выполним этот код и отправлю два запроса. Я удалил некоторые бесполезные линии от вывода.
>> Inside request: execution: 6 asyncId: 9 / trigger: 6 asyncId: 11 / trigger: 9 asyncId: 12 / trigger: 11 asyncId: 13 / trigger: 12 >> Inside readAndSendFile: execution: 13 [...] >> Inside request: execution: 31 asyncId: 34 / trigger: 31 asyncId: 36 / trigger: 34 asyncId: 37 / trigger: 36 asyncId: 38 / trigger: 37 >> Inside readAndSendFile: execution: 38
Мы видим, что наши два запроса были назначены два Asyncid
: 6 и 31. Чтение нашего файла создано новые async-ресурсы, прозрачные для нашего кода, а затем наши readandsendfile
Зарегистрировано два Asyncid
: 13 и 38.
От readandsendfile
Функция, мы можем получить наш оригинальный запрос Asyncid
извлекая нашу «асинхронный путь». Например, для нашего первого запроса мы начинаем с Asyncid
равняется 13, а затем мы получаем 13 → 12 → 11 → 9 → 6.
Получить что-то полезное
Со всеми из них мы можем создавать две функции для создания и получения объекта контекста для каждой из наших запросов. Это также может быть использовано для любого другого использования, а не только HTTP Server.
const async_hooks = require('async_hooks'); const contexts = {}; async_hooks.createHook({ init: (asyncId, type, triggerAsyncId) => { // A new async resource was created // If our parent asyncId already had a context object // we assign it to our resource asyncId if (contexts[triggerAsyncId]) { contexts[asyncId] = contexts[triggerAsyncId]; } }, destroy: (asyncId) => { // some cleaning to prevent memory leaks delete contexts[asyncId]; }, }).enable(); function initContext(fn) { // We force the initialization of a new Async Resource const asyncResource = new async_hooks.AsyncResource('REQUEST_CONTEXT'); return asyncResource.runInAsyncScope(() => { // We now have a new asyncId const asyncId = async_hooks.executionAsyncId(); // We assign a new empty object as the context of our asyncId contexts[asyncId] = {} return fn(contexts[asyncId]); }); } function getContext() { const asyncId = async_hooks.executionAsyncId(); // We try to get the context object linked to our current asyncId // if there is none, we return an empty object return contexts[asyncId] || {}; }; module.exports = { initContext, getContext, };
Давайте напишем небольшой тест, чтобы проверить, все ли он работает правильно.
const {initContext, getContext} = require('./context.js'); const logId = () => { const context = getContext(); console.log(`My context id is: ${context.id}`); } initContext((context) => { context.id = 1; setTimeout(logId, 100); setTimeout(logId, 300); }); initContext((context) => { context.id = 2; setTimeout(logId, 200); setTimeout(logId, 400); });
Выполняя это, мы получаем:
My context id is: 1 My context id is: 2 My context id is: 1 My context id is: 2
Что дальше?
С этими двумя функциями мы реализовали простой, но очень полезный способ создать контекст для хранения данных между нашей функцией без необходимости передавать эти данные в качестве аргументов.
Реальное использование будет создано контекст для каждого HTTP-запроса, генерируя идентификатор запроса и выбирая этот идентификатор внутри нашей функции ведения журнала для печати его на каждой строке. Я также использовал его для запуска каждого вызова базы данных одного HTTP-запроса в той же транзакции SQL.
Вы должны быть осторожны о том, сколько информации вы включаете в свой контекст. Это должно быть максимально простое, чтобы сохранить поток данных в вашей программе простой для понимания и предотвращения случаев Edge, которые могут создавать ошибки. Используя этот вид контекста, также путают инструменты, такие как Teadercript, который не сможет знать, что в вашем текущем контексте.
Имейте в виду, что async_hooks
все еще экспериментал, но если вы любите жить на краю, идите и попробуйте!
Обложка изображения по EFE Kurnaz На бессплашне