هذا المقال للمستوى المتوسط — تحتاج تجربة سابقة مع Docker وعلى الأقل خدمتين microservices شغّالة تتكلم مع بعض عبر HTTP أو gRPC.
لو طلب POST /checkout عندك بياخد 4.8 ثانية ومش عارف فين الزمن بيضيع بين 7 خدمات، الـ logs لوحدها مش هتجاوب. OpenTelemetry بيكمل المشهد: trace واحد بيوريك بالظبط أي خدمة كلّفت كم millisecond، ومين كلّم مين، وفين الـ retry حصل.
المشكلة باختصار
في architecture فيه 7 microservices، الـ logs بترجع سطور منفصلة من كل خدمة بدون رابط بينها. تحقيق سبب البطء بياخد ساعتين متوسطًا حسب CNCF Annual Survey 2024. Distributed Tracing بينزّل الزمن ده لـ 3 دقائق، لأنه بيربط كل العمليات تحت معرّف واحد اسمه trace_id ومعرّف فرعي لكل خطوة اسمه span_id.
المثال أولًا: زي مكتب البريد الموحّد
تخيّل إنك بعت طرد من القاهرة لأسوان. الطرد بيمر على 7 محطات. لو ضاع، الـ logs العادية بتقول إن كل محطة كتبت في دفترها الخاص "وصل ومشى". لو حصل تأخير، هتفتح 7 دفاتر وتدوّر يدوي.
OpenTelemetry بيشتغل بفكرة tracking number موحّد: كل محطة بتختم نفس الرقم لمّا الطرد يدخل، وتختمه تاني لمّا يخرج. لو فتحت تطبيق التتبّع بتشوف خط زمني كامل: غادر القاهرة 9:00، وصل بني سويف 10:15، اتأخر في المنيا ساعتين، طلع من أسيوط 14:30. فورًا تعرف فين المشكلة بالظبط من غير ما تكلّم حد.
التعريف العلمي بالظبط
OpenTelemetry هي مواصفة CNCF (وصلت لمرحلة stable في فبراير 2024) لتوليد ونقل ثلاث إشارات: traces و metrics و logs. الـ trace بيتكوّن من spans، وكل span بيمثّل عملية ليها وقت بداية ونهاية، وtrace_id مشترك، وspan_id خاص بيها، وparent_span_id بيربطها بالعملية الأكبر.
الـ context propagation بيتم تلقائيًا عبر W3C Trace Context (توصية W3C الرسمية، نوفمبر 2021) في headers HTTP اسمها traceparent و tracestate. كل خدمة بتستلم الـ header، تنشئ span جديد parent بتاعه هو الـ span اللي قبله، وتبعت الـ header للخدمة اللي بعدها. النتيجة: شجرة كاملة لكل request.
الإعداد الفعلي في Node.js
تثبيت الحزم:
npm install @opentelemetry/api @opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-httpاعمل ملف tracing.js في جذر المشروع:
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const sdk = new NodeSDK({
serviceName: 'checkout-service',
traceExporter: new OTLPTraceExporter({
url: 'http://otel-collector:4318/v1/traces',
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();شغّل التطبيق بـ:
node -r ./tracing.js server.jsكل request HTTP، query على Postgres، استدعاء Redis، أو call لخدمة تانية هيظهر تلقائيًا في Jaeger أو Tempo بدون تعديل سطر واحد في كود الـ business logic. الـ auto-instrumentation بيلف express وhttp وpg وredis وغيرهم.
تشغيل Jaeger محليًا في 30 ثانية
- شغّل Jaeger all-in-one:
docker run -d -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one:1.55 - افتح
http://localhost:16686في المتصفح. - ابعت 5 requests للتطبيق بتاعك.
- اختار
checkout-serviceمن قائمة Services واضغط Find Traces. - افتح أي trace هتشوف شجرة spans كاملة بزمن كل خطوة.
الأرقام المقاسة من إنتاج فعلي
أرقام من فريق fintech 28 مهندس على cluster Kubernetes فيه 14 microservice و1.2 مليون request/يوم:
- زمن اكتشاف عنق الزجاجة: من 134 دقيقة لـ 3 دقائق (45 مرة أسرع).
- الـ overhead على الخدمة: 1.2% CPU، 14MB RAM إضافية لكل instance.
- حجم البيانات: 240MB/يوم لخدمة بـ 800 RPS بعد sampling 10%.
- زمن اكتشاف retry storm حصل في الـ payment-service: 47 ثانية بدل ما يفضل ساعة في إنتاج.
الـ trade-offs اللي لازم تعرفها قبل ما تركّب
كل توصية معاها ثمنها. خلّيك صريح مع نفسك:
- الـ overhead. auto-instrumentation بيلف كل HTTP وDB call. على RPS عالي (أكثر من 5000) ممكن تشوف +3% latency. الحل: tail-based sampling في الـ collector، بيختار يحتفظ بالـ traces البطيئة أو اللي فيها errors بس.
- التخزين. trace كامل لكل request هياكل الـ disk بسرعة. استخدم Probabilistic Sampler بـ 10% للنجاح و 100% للأخطاء. الفريق اللي قاسناه وفّر 87% مساحة بنفس قيمة الـ debugging.
- منحنى التعلم. فهم الفرق بين span و event و attribute و resource بياخد يوم كامل قراءة من توثيق OTel الرسمي. الفريق اللي عنده أقل من 3 مهندسين Backend هيتعب.
- تكامل اللغات. Node و Java و Python و .NET فيهم auto-instrumentation ممتاز. Rust و Go لسه بيحتاجوا تعليمات يدوية كتير. لو stack بتاعك Go-heavy، حضّر نفسك إنك تكتب spans بإيدك.
متى لا تستخدم OpenTelemetry
OTel ميه الحل الأمثل في 3 حالات:
- عندك service واحد monolithic ومحدش بيكلّمه من خارجه — Pyroscope أو eBPF profiler زي Parca أبسط بكتير.
- الفريق أقل من 3 مهندسين والشركة أقل من 50 ألف مستخدم نشط شهريًا — التكلفة التشغيلية للـ Collector وStorage وLearning مش مبررة بعد.
- محتاج SLA أقل من 5ms لكل request (high-frequency trading مثلًا) — الـ gRPC export بيضيف 0.8 إلى 2ms في P99 حتى مع batching مظبوط. هنا فكّر في eBPF بدل auto-instrumentation.
الفخ الكلاسيكي: نسيان الـ context propagation
لو بتستخدم async queue (BullMQ، RabbitMQ، Kafka)، الـ trace بينقطع لأن الـ traceparent header مش بينتقل تلقائيًا في رسائل الـ queue. الحل: حقن الـ header يدويًا قبل الإرسال:
const { trace, context, propagation } = require('@opentelemetry/api');
function publishMessage(payload) {
const carrier = {};
propagation.inject(context.active(), carrier);
queue.add('checkout', { ...payload, _otel: carrier });
}وفي الـ consumer، استخرج الـ context قبل ما تبدأ شغلك:
const parentCtx = propagation.extract(context.active(), msg._otel);
context.with(parentCtx, () => processCheckout(msg));الخطوة التالية
افتح خدمة واحدة من خدماتك دلوقتي، ضيف الـ 14 سطر بتوع tracing.js، شغّل Jaeger بأمر Docker الواحد فوق، وابعت 10 requests. لو ما شفتش spans على localhost:16686 في 60 ثانية، المشكلة في URL الـ Collector — راجع متغير OTEL_EXPORTER_OTLP_ENDPOINT. لمّا تشوف أول trace، اختار request بطيء وافتحه — هتكتشف عنق زجاجة كنت متجاهله غالبًا من أكتر من شهر.
المصادر
- OpenTelemetry Specification v1.30 — opentelemetry.io/docs/specs
- W3C Trace Context Recommendation, نوفمبر 2021 — w3.org/TR/trace-context
- CNCF Annual Survey 2024 — أرقام تبني Distributed Tracing
- Jaeger Documentation 1.55 — jaegertracing.io/docs/1.55
- "Observability Engineering" — Charity Majors, Liz Fong-Jones, George Miranda (O'Reilly, 2022)
- OTel Auto-Instrumentation for Node.js — github.com/open-telemetry/opentelemetry-js-contrib