OpenTelemetry بالعربي: لاحق الـ 200ms المختفية بين 6 خدمات microservices
السيرفر بيرجّع الـ API في 1.4 ثانية. Prometheus بيقولك إن DB latency متوسطها 80ms والـ CPU عند 12%. Grafana نضيفة. Logs ما فيهاش errors. لكن الزائر بيشتكي. السبب بالظبط: 200ms بتضيع كل request بين خدمتين من الستة، وانت مش شايفها لأن الـ metrics بتشوف averages، مش spans فردية.
OpenTelemetry بيقفل الفجوة دي بسطر instrumentation واحد لكل خدمة: بياخد الطلب من لحظة دخوله للـ gateway، يلاحقه عبر كل خدمة وكل DB query وكل Redis call، ويرسمه كـ waterfall بيقولك أي خدمة أكلت كام millisecond بالظبط.
المشكلة باختصار
الفرق بين الثلاثة بسيط: Metrics بتقول لك "في مشكلة". Logs بتقول لك "في error". Traces بتقول لك "في الخدمة X، خد Z millisecond، والسبب call لـ Redis اتعمله timeout بعد retry فشل". الفرق هنا هو نفس الفرق بين "العربية بتسخن" وبين "الفان بلت محتاج تغيير".
في أنظمة microservices، طلب واحد ممكن يمر على 5 لـ 12 خدمة. من غير tracing، انت في الظلام. ضربة console.log في كل خدمة + إعادة deploy = ساعتين شغل لتشخيص حاجة كان trace كامل هيقولها في 30 ثانية.
مثال للمبتدئ — شركة الشحن
تخيّل شحنة من الإسكندرية للرياض. الشحنة بتمر على 5 محطات: مخزن المنشأ، الشاحنة، الميناء، الجمارك، شركة التوصيل المحلية. كل محطة بتحط ختم على ورقة الشحنة فيه وقت الدخول والخروج، ورقم الشحنة الموحد على كل الأختام.
لو الشحنة وصلت متأخرة 6 ساعات، الورقة دي بالظبط هي اللي بتقول لك "اتأخرت 5 ساعات في الجمارك"، مش "في حتة في السكة". OpenTelemetry trace هو نفس ورقة الشحنة دي للطلب اللي بيمر على خدماتك. كل محطة = service. كل ختم = span. وقت الدخول والخروج = start_time و end_time. ورقم الشحنة الموحد على كل الأختام = trace_id.
التعريف العلمي الدقيق
بعد ما المثال وضّح الفكرة، دلوقتي التعريف بدقة:
- Trace: مجموعة spans مرتبطة بنفس الـ trace_id (UUID 128-bit) تمثل دورة حياة طلب واحد عبر النظام.
- Span: وحدة عمل واحدة (HTTP call، DB query، function execution). بيحتوي على span_id، parent_span_id (لبناء الشجرة)، start_time/end_time، attributes زي
http.methodوdb.statement، و events (لحظات داخل الـ span زي retry أو cache miss). - Context Propagation: الميكانيزم اللي بيمشّي الـ trace_id من خدمة لخدمة عبر HTTP headers. القاعدة المعيارية اسمها W3C Trace Context وبتستخدم header
traceparent. من غيره، كل خدمة هتعمل trace منفصل ومش هتعرف تربطهم.
مثال تنفيذي — instrumentation Node.js في 4 سطور
ملف tracing.js منفصل عن business logic:
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({
traceExporter: new OTLPTraceExporter({
url: 'http://otel-collector:4318/v1/traces',
}),
instrumentations: [getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': { enabled: false },
})],
});
sdk.start();
شغّل التطبيق بـ node --require ./tracing.js app.js. كل request HTTP، كل query لـ pg أو mongo، كل call لـ Redis، هيتسجل تلقائيًا. مش محتاج تعدّل سطر business logic واحد.
علشان الـ trace_id يمشي بين الخدمات، التطبيق التاني لازم يكون عنده نفس الـ instrumentation. الـ HTTP client بيحط traceparent تلقائيًا، والـ HTTP server بيقراه ويبني span ابن من نفس الـ trace.
إعداد Collector و Jaeger في docker compose
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:0.95.0
command: --config=/etc/otel-config.yaml
volumes:
- ./otel-config.yaml:/etc/otel-config.yaml
ports: ["4318:4318"]
jaeger:
image: jaegertracing/all-in-one:1.56
ports: ["16686:16686", "4317:4317"]
# otel-config.yaml
receivers:
otlp:
protocols:
http: { endpoint: 0.0.0.0:4318 }
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls: { insecure: true }
service:
pipelines:
traces:
receivers: [otlp]
exporters: [otlp/jaeger]
افتح http://localhost:16686، اختار خدمتك، شوف الـ trace كاملة كـ waterfall.
أرقام حقيقية من الإنتاج
في تطبيق e-commerce بـ 6 خدمات (gateway ← auth ← product ← inventory ← pricing ← cart) جالنا P95 latency = 1.4 ثانية. فتحنا Jaeger وشفنا التوزيع التالي:
- gateway: 12ms — سليمة.
- auth: 45ms — سليمة.
- product: 980ms — المشكلة هنا.
- inventory: 90ms — مقبولة.
- pricing: 240ms — في retries على Redis.
- cart: 33ms — سليمة.
في خدمة product: query SQL بيرجّع 1200 صف وفيه N+1 على relation الـ images عبر الـ ORM. حلّيناها بـ eager loading. الـ trace اللي بعدها: P95 = 280ms. الـ pricing: بدّلنا الـ retry strategy من 3 retries × 80ms لـ circuit breaker، نزّل الزمن لـ 60ms. النتيجة الكلية: 1.4s → 380ms في يومين شغل. من غير tracing، الكشف ده كان هياخد أسبوع كامل من console.log و redeploy.
الـ trade-offs اللي لازم تحسبها
- Overhead: 2–7% CPU و 50–200 bytes per span على الذاكرة. لو عندك sustained throughput فوق 5,000 RPS per node، احسب التكلفة بدقة قبل ما تفعّل auto-instrumentation كامل.
- التخزين: trace فيها 6 spans = 5 لـ 15KB raw. عند 2,000 RPS، ده يعني حوالي 1TB يوميًا قبل أي ضغط. الحل: head sampling (احتفظ بـ 10% عشوائي) أو tail sampling (احتفظ بكل trace فيها error أو latency > P95).
- Noisy auto-instrumentation:
instrumentation-fsوdnsبيضيفوا spans كتيرة من غير قيمة. اقفلهم explicit زي ما عملت في الكود فوق. - Vendor lock-in عكسي: OpenTelemetry معياري، تقدر تبدّل الـ backend من Jaeger لـ Tempo لـ Honeycomb من غير ما تلمس كود التطبيق. ده مكسب نادر في عالم المراقبة.
متى لا تستخدم OpenTelemetry
- لو تطبيقك monolith بدون calls خارجية: structured logs + metrics بتكفي.
- لو الـ throughput منخفض (تحت 10 RPS) والـ debugging اليدوي ممكن: التكلفة العملية مش مبررة.
- لو الفريق مش هيستثمر في dashboards وalerting على traces: هتجمع بيانات مش بتنظر لها.
- لو كامل الـ stack على Lambda بـ cold starts متكررة: الـ SDK ممكن يضاعف cold start time. استخدم AWS X-Ray native بدل OTel SDK الكامل.
الخطوة التالية
افتح أبسط خدمة في النظام عندك، ضيف الـ 4 سطور instrumentation اللي فوق، شغّل Jaeger في Docker على جهازك المحلي، وابعت 100 request. شوف الـ waterfall. اللي هتلاقيه في الـ trace الأولى هيغيّر فهمك للنظام أكتر من شهر متابعة dashboards.
المصادر
- OpenTelemetry Documentation — Concepts: opentelemetry.io/docs/concepts
- W3C Trace Context Specification: w3.org/TR/trace-context
- Jaeger Architecture Overview: jaegertracing.io/docs/1.56/architecture
- Google Dapper Paper (2010) — أصل distributed tracing: research.google/pubs/dapper
- OpenTelemetry Collector — Tail Sampling Processor: github.com/open-telemetry/opentelemetry-collector-contrib