“يا جماعة، السيرفر واقع!”… قصة من قلب المعركة
أذكرها وكأنها البارحة. كانت ليلة هادئة، وفجأة، بدأت التنبيهات تنهال علينا كالمطر. “Error rate is high on checkout service”، “Payment gateway latency > 2s”. ولّعت الدنيا، وقاعة “War Room” الافتراضية على “سلاك” امتلأت بالمهندسين. المشكلة كانت واضحة: المستخدمون لا يستطيعون إتمام عمليات الدفع.
لكن… أين بالضبط تكمن المشكلة؟
بدأت رحلة العذاب. فريق الواجهة الأمامية (Frontend) يقول: “الـ API الخاص بخدمة الطلبات (Orders Service) يرجع خطأ 500”. فريق خدمة الطلبات يفتح سجلاته (logs) ويقول: “لا يوجد خطأ عندنا، لكننا ننتظر ردًا طويلاً من خدمة الدفع (Payment Service)”. فريق خدمة الدفع، وهم في عالم آخر، يقولون: “مقاييسنا (metrics) تبدو طبيعية، لكن لحظة… نرى بعض الأخطاء المتفرقة في السجلات النصية التي لا نراقبها عادةً”.
قضينا ساعات، يا جماعة الخير، ونحن نقفز بين عشرات لوحات المراقبة (Dashboards)، ونبحث يدويًا في ملفات سجلات ذات تنسيقات مختلفة، محاولين ربط “معرّف طلب” (Request ID) معين عبر ثلاث خدمات مختلفة. كل خدمة كانت جزيرة منعزلة، لها لغتها ومقاييسها الخاصة. سجلاتنا كانت مجرد صرخات متفرقة في وادٍ سحيق، لا أحد يسمعها أو يربطها ببعضها. في تلك الليلة، أدركنا أن طريقتنا في المراقبة كانت هي المشكلة الحقيقية.
هذه الفوضى هي التي دفعتنا للبحث عن حل جذري، حل يوحد اللغة ويحول هذه الجزر المعزولة إلى قارة واحدة متصلة. هذا الحل كان اسمه: OpenTelemetry.
ما هي “القابلية للمراقبة” (Observability) ولماذا هي ليست مجرد “مراقبة” (Monitoring)؟
قبل أن نغوص في تفاصيل OpenTelemetry، دعونا نوضح مفهوماً أساسياً. كثيرون يخلطون بين المراقبة (Monitoring) والقابلية للمراقبة (Observability).
- المراقبة (Monitoring): هي أن تراقب أشياء تعرف مسبقًا أنها قد تفشل. أنت تضع تنبيهات على استخدام المعالج (CPU)، استهلاك الذاكرة، ومعدل الأخطاء. إنها تجيب على سؤال: “هل النظام يعمل كما هو متوقع؟”.
- القابلية للمراقبة (Observability): هي القدرة على فهم الحالة الداخلية لنظامك من خلال البيانات التي يصدرها، حتى لو لم تكن تتوقع المشكلة مسبقًا. إنها تجيب على سؤال: “لماذا لا يعمل النظام كما هو متوقع؟”.
تعتمد القابلية للمراقبة على ثلاثة أعمدة رئيسية:
- السجلات (Logs): سجلات زمنية للأحداث. تخبرك “ماذا حدث” في نقطة معينة.
- المقاييس (Metrics): بيانات رقمية مجمّعة على فترة زمنية. تخبرك “كيف هي صحة النظام” بشكل عام (مثل: عدد الطلبات في الدقيقة).
- التتبعات (Traces): تمثل رحلة طلب واحد عبر الخدمات المختلفة في نظامك. تخبرك “أين وكيف” حدثت المشكلة خلال هذه الرحلة.
في عالم الخدمات المصغرة (Microservices)، تتبع رحلة طلب واحد (Trace) هو أهم عمود، لأنه يربط كل شيء ببعضه. وهنا كانت مشكلتنا: كل خدمة تتحدث لغة مختلفة، مما جعل بناء “تتبع” متكامل شبه مستحيل.
المنقذ OpenTelemetry: توحيد اللغة وإنهاء الفوضى
OpenTelemetry، أو “OTel” كما نحب أن نسميه، ليس أداة أو منتجًا بحد ذاته. إنه معيار ومجموعة من الأدوات والـ SDKs لإنشاء وجمع وتصدير بيانات المراقبة (السجلات والمقاييس والتتبعات).
الفكرة عبقرية في بساطتها: بدلاً من أن يقوم كل تطبيق بإنشاء بيانات المراقبة بتنسيق خاص وإرسالها إلى أداة محددة (مثل Datadog أو Jaeger)، يقوم التطبيق باستخدام OTel SDK لإنشاء البيانات بتنسيق موحد ومفتوح. ثم يمكنك أن تقرر لاحقًا إلى أين تريد إرسال هذه البيانات.
نصيحة من أبو عمر: أجمل ما في OpenTelemetry هو أنه يحررك من قيود الشركات (Vendor Lock-in). يمكنك اليوم استخدام Jaeger كـ Backend للتتبعات، وغدًا تقرر الانتقال إلى New Relic أو بناء نظامك الخاص، وكل ذلك دون تغيير سطر كود واحد في تطبيقاتك الأساسية. هذه مرونة لا تقدر بثمن.
كيف يعمل OpenTelemetry؟
يتكون نظام OTel البيئي من عدة أجزاء رئيسية:
- API: واجهة برمجية مجردة (Abstract) تستخدمها في الكود الخاص بك لإنشاء التتبعات والمقاييس.
- SDK: التنفيذ الفعلي للـ API. هو الذي يهتم بتفاصيل إنشاء البيانات وتجميعها وإرسالها.
- Exporter: قطعة برمجية مسؤولة عن تصدير البيانات من الـ SDK إلى نظام مراقبة معين (مثل Jaeger Exporter, Prometheus Exporter).
- Collector (المجمّع): مكون اختياري ولكنه قوي جدًا. وهو عبارة عن بروكسي يمكنه استقبال البيانات من عدة مصادر، معالجتها (مثل إزالة معلومات حساسة)، ثم تصديرها إلى وجهات متعددة.
لنطبق عمليًا: رحلة مع OTel في عالم Node.js
الكلام النظري جميل، لكن دعونا نرى كيف يبدو هذا على أرض الواقع. سأستخدم مثالاً بسيطًا لتطبيق Express.js في Node.js.
الخطوة 1: الإعداد الأساسي و”السحر” التلقائي
أحد أروع جوانب OTel هو “Instrumentation التلقائي”. يمكنك إضافة مكتبات تقوم تلقائيًا بإنشاء تتبعات للمكتبات الشائعة مثل Express, HTTP, PostgreSQL وغيرها دون تدخل منك.
لنفترض أن لدينا ملف `instrumentation.js` بسيط:
// instrumentation.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { ConsoleSpanExporter } = require('@opentelemetry/sdk-trace-node');
const {
getNodeAutoInstrumentations,
} = require('@opentelemetry/auto-instrumentations-node');
const {
PeriodicExportingMetricReader,
ConsoleMetricExporter,
} = require('@opentelemetry/sdk-metrics');
const sdk = new NodeSDK({
traceExporter: new ConsoleSpanExporter(), // الآن سنطبع التتبعات على الكونسول فقط
metricReader: new PeriodicExportingMetricReader({
exporter: new ConsoleMetricExporter(), // ونطبع المقاييس أيضًا
}),
instrumentations: [getNodeAutoInstrumentations()], // هذا هو السحر!
});
sdk.start();
ثم نقوم بتشغيل تطبيق Express الخاص بنا مع هذا الملف:
node --require ./instrumentation.js app.js
الآن، وبدون إضافة أي كود تتبع داخل `app.js`، أي طلب HTTP يصل إلى تطبيقنا سيتم إنشاء “تتبع” (Trace) و”نطاق” (Span) له تلقائيًا! إذا كان تطبيقنا يتصل بقاعدة بيانات، فسيتم إنشاء نطاق فرعي لمكالمة قاعدة البيانات أيضًا. كل هذا بشكل تلقائي.
الخطوة 2: إضافة تتبعات يدوية لسياق أعمق
التتبع التلقائي رائع، لكن أحيانًا نحتاج إلى تفاصيل أكثر حول منطق العمل (Business Logic) الخاص بنا. هنا يأتي دور التتبع اليدوي.
لنفترض أن لدينا دالة مهمة تقوم بمعالجة معينة، ونريد أن نراها كجزء من التتبع العام.
const api = require('@opentelemetry/api');
// ... داخل إحدى دوال الـ Controller في Express
async function processOrder(req, res) {
// الحصول على الـ tracer الحالي
const tracer = api.trace.getTracer('my-app-tracer');
// بدء نطاق جديد يدويًا
await tracer.startActiveSpan('processOrderBusinessLogic', async (span) => {
try {
console.log('بدء معالجة منطق الطلب...');
// إضافة معلومات إضافية مفيدة للنطاق
span.setAttribute('order.id', req.body.orderId);
span.setAttribute('user.id', req.user.id);
// ... هنا يتم تنفيذ منطق العمل الحقيقي ...
await someComplexFunction(req.body);
span.addEvent('Business logic completed successfully');
console.log('انتهت المعالجة بنجاح');
res.status(200).send('Order processed!');
} catch (err) {
// تسجيل الخطأ في النطاق، هذا مهم جدًا!
span.recordException(err);
span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
res.status(500).send('Something went wrong');
} finally {
// إنهاء النطاق
span.end();
}
});
}
الآن، عندما تنظر إلى التتبع الكامل للطلب، لن ترى فقط “طلب HTTP” و “استعلام قاعدة البيانات”، بل سترى أيضًا نطاقًا واضحًا اسمه `processOrderBusinessLogic` مع كل المعلومات التي أضفناها، مما يمنحك فهمًا عميقًا لما حدث داخل تطبيقك.
البطل المجهول: المجمّع (The OTel Collector)
في البداية، قد تبدو فكرة إرسال البيانات من تطبيقك مباشرة إلى Jaeger أو Prometheus مغرية. لكن صدقني، هذه وصفة للمشاكل المستقبلية.
نصيحة عملية من خبرتي: استخدم الـ OTel Collector من اليوم الأول. اعتبره نقطة الدخول والخروج الوحيدة لجميع بيانات المراقبة الخاصة بك. إنه يفصل تطبيقاتك تمامًا عن أنظمة المراقبة النهائية.
الـ Collector يعمل كخط أنابيب (pipeline):
- Receivers (المستقبلات): تستقبل البيانات بتنسيقات مختلفة (OTLP وهو البروتوكول الأصلي لـ OTel, Jaeger, Prometheus, …).
- Processors (المعالجات): تعالج البيانات أثناء مرورها. يمكنك استخدامه لـ:
- إضافة بيانات وصفية (metadata) مشتركة لكل التتبعات.
- إزالة معلومات حساسة (PII) من النطاقات.
- أخذ عينات (sampling) لتقليل حجم البيانات.
- Exporters (المصدرات): ترسل البيانات المعالجة إلى وجهة أو أكثر (Jaeger, Prometheus, Loki, Datadog, …).
هذا مثال بسيط لملف إعدادات `config.yaml` لـ Collector يستقبل البيانات عبر بروتوكول OTLP ويرسلها إلى Jaeger وإلى الكونسول (للتصحيح):
receivers:
otlp:
protocols:
grpc:
http:
processors:
batch:
exporters:
jaeger:
endpoint: jaeger-all-in-one:14250
tls:
insecure: true
logging:
loglevel: debug
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [jaeger, logging]
بهذه الطريقة، تطبيقاتك لا تعرف شيئًا عن Jaeger. كل ما تعرفه هو أنها ترسل البيانات إلى الـ Collector. إذا قررت غدًا إضافة Datadog، كل ما عليك فعله هو إضافة Datadog exporter إلى هذا الملف، دون لمس أي تطبيق!
الخلاصة: من جزر معزولة إلى قارة متصلة 🗺️
بالعودة إلى قصتنا في البداية، لو كان لدينا OpenTelemetry في تلك الليلة، لكان الوضع مختلفًا تمامًا. بمجرد وصول التنبيه، كنا سنفتح التتبع الخاص بأحد الطلبات الفاشلة ونرى القصة كاملة:
- الطلب بدأ في الواجهة الأمامية.
- وصل إلى خدمة الطلبات (Orders Service) واستغرق 20ms.
- خدمة الطلبات استدعت خدمة الدفع (Payment Service).
- خدمة الدفع استغرقت 2.5 ثانية، وداخل نطاقها نرى خطأً واضحًا مع رسالته ومكان حدوثه بالضبط في الكود.
كنا سنجد سبب المشكلة في دقائق، لا ساعات. هذا هو الفرق بين الصراخ في وادٍ وبين امتلاك خريطة واضحة لكل رحلة داخل نظامك.
الاستثمار في تطبيق OpenTelemetry ليس رفاهية، بل هو ضرورة حتمية لأي نظام حديث يعتمد على الخدمات المصغرة. إنه يمنحك الوضوح، المرونة، والقدرة على النوم بهدوء في الليل، مع العلم أنك قادر على فهم نظامك مهما كان معقدًا.
يلا يا جماعة، شدوا حيلكم وابدأوا رحلتكم مع OTel. الطريق قد يبدو طويلاً في البداية، لكن العائد يستحق كل دقيقة تقضونها في بنائه. 🚀