AsyncLocalStorage بالعربي: requestId في كل log بدون تمريره يدويًا
هتكسب من المقال ده طريقة عملية تربط كل log في طلب Node.js بنفس requestId، بدل ما تفضل تمرره يدويًا بين controllers وservices وdatabase calls.
مستوى القارئ: متوسط
المشكلة باختصار
اللي بيحصل فعلاً في APIs الكبيرة إن الطلب الواحد يدخل Express، يعدّي على middleware، ينادي service، ينادي database، وبعدها يظهر خطأ في log بعيد عن بداية الطلب. لو اللوجات مش مرتبطة بنفس requestId، التشخيص بيتحول لتخمين.
سيناريو واقعي: عندك API بيخدم 50 ألف طلب في اليوم. 2% فقط بيرجعوا 500، يعني حوالي 1000 طلب محتاج تتبّع. لو 38% من اللوجات ناقصها requestId، فريقك هيضيّع وقت في ربط الأحداث يدويًا. في فريق صغير، ده ممكن يحول bug مدته 12 دقيقة إلى 45 دقيقة.
مثال بسيط قبل التعريف العلمي
ركز في المثال ده: بدل ما تبعت requestId كوسيط لكل دالة، بتحطه مرة واحدة في بداية الطلب. بعد كده أي كود asynchronous داخل نفس الطلب يقدر يقرأه.
import express from 'express';
import crypto from 'node:crypto';
import { AsyncLocalStorage } from 'node:async_hooks';
const app = express();
const requestContext = new AsyncLocalStorage();
function log(message, fields = {}) {
const store = requestContext.getStore() || {};
console.log(JSON.stringify({
level: 'info',
requestId: store.requestId,
...fields,
message
}));
}
app.use((req, res, next) => {
const requestId = req.get('x-request-id') || crypto.randomUUID();
requestContext.run({ requestId }, () => next());
});
async function loadUser(userId) {
log('loading user', { userId });
return { id: userId, plan: 'pro' };
}
app.get('/users/:id', async (req, res) => {
const user = await loadUser(req.params.id);
log('user loaded');
res.json(user);
});
app.listen(3000);لو طلبين دخلوا في نفس الوقت، كل واحد هيشوف requestId الخاص به. Node.js يحافظ على السياق خلال promise chains وcallbacks المرتبطة بنفس العملية asynchronous.
ما هو AsyncLocalStorage بالظبط؟
AsyncLocalStorage كلاس رسمي في Node.js داخل node:async_hooks. فكرته قريبة من thread-local storage في لغات تانية، لكن مناسبة لطبيعة Node.js القائمة على event loop وasync callbacks.
التعريف العلمي: هو مساحة تخزين مرتبطة بسياق asynchronous معين. لما تستدعي run(store, callback)، أي كود asynchronous يبدأ من داخل هذا callback يقدر يسترجع نفس store باستخدام getStore(). ده مفيد للـ request context، tracing، tenantId، correlationId، وتجميع metadata للوجات.