Distributed Tracing بـ OpenTelemetry: لو الـ API بطيء وأنت مش عارف فين بالظبط
لو عندك خدمة بترد في 8 ثواني والـ logs بتقولك إن كل service شغّال، المشكلة مش في service واحد — هي في التنقل بين 4 خدمات. Distributed tracing بيوريك بالظبط أي خطوة هي السبب، بدون تخمين، وبدون ما تطفّي الـ production علشان تعمل debug.
المشكلة باختصار
في أي نظام فيه أكتر من 3 خدمات بتتكلم مع بعض، الـ logs لوحدها مبتجاوبش على السؤال الأهم: "اللي بطّأ الطلب ده فين بالظبط؟". بتفضل تطابق timestamps يدوي بين 5 ملفات log في 5 سيرفرات، وتخمّن. النتيجة: incident بياخد نص ساعة بدل 5 دقايق، و capacity planning مبني على شعور.
تخيّل الموضوع كده الأول (مثال للمبتدئين)
متخيّل إنك طلبت أكل من تطبيق توصيل. الطلب عدى على 5 محطات: التطبيق، السيرفر، المطعم، السواق، والـ GPS. الأكل وصلك متأخر ساعة، فبتسأل: "اللي تأخر فين؟". لو معندكش طريقة تعرف كل محطة أخدت كام دقيقة، هتفضل تخمّن.
Distributed tracing هو الـ "stopwatch" اللي بيركّب على كل محطة وبيقولك: "السواق صرف 47 دقيقة في الزحمة، المطعم خد 8 دقايق، الباقي ثواني". خلاص عرفت السبب وتقدر تتصرف.
في عالم البرمجيات: الطلب الواحد ممكن يعدي على API gateway، ثم authentication service، ثم database، ثم cache، ثم external API. لو الرد بياخد 2.4 ثانية، Distributed tracing بيوريك "الـ DB أخدت 1.9 ثانية، باقي الخدمات أخدوا 500ms". ركّز على المكان اللي مهم.
المفهوم بشكل دقيق: Span و Trace
Trace هي الرحلة الكاملة لطلب واحد عبر النظام. Span هي وحدة عمل واحدة داخل الرحلة (مثلًا: استعلام DB، استدعاء HTTP، حسبة معينة). كل Span ليه:
- trace_id: رقم موحّد لكل الرحلة بيتمرّر بين الخدمات.
- span_id: رقم الـ unit نفسها.
- parent_span_id: علشان نبني الشجرة.
- start_time / end_time: لقياس المدة.
- attributes: metadata زي اسم الـ DB query أو الـ HTTP status.
الـ trace هي شجرة من الـ spans، والـ visualization المعروف بيظهر كـ "waterfall" — كل عمود بيوريك span، وقت بدايتها، وطولها. لو شفت عمود واحد طويل وسط أعمدة قصيرة، خلاص لقيت الـ bottleneck.
ليه الـ logs لوحدها مش كفاية
الـ logs بتقولك "حصل كذا في الوقت ده". مش بتقولك علاقة الحدث ده بطلب معيّن في خدمة تانية. لو فيه 1000 طلب في الثانية، إزاي تعرف إن log entry في service A جه نتيجة لطلب اتعمل في service B؟ بدون trace_id موحّد بيتمرّر بين الخدمات (اللي اسمه context propagation)، هتقعد ساعات تطابق timestamps. OpenTelemetry بيعمل ده تلقائي في معظم الـ HTTP libraries.
إعداد عملي بـ Python في 5 دقايق
الشرح ده مبني على فرضية إن عندك Python service بيستخدم Flask أو FastAPI، وعايز تشوف traces في Jaeger أو Tempo محليًا.
الخطوة 1: ثبّت الـ packages.
pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a installالخطوة 2: شغّل الـ app بـ auto-instrumentation. مفيش تعديل في الكود.
export OTEL_SERVICE_NAME=my-api
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
opentelemetry-instrument python app.pyالخطوة 3: شغّل OpenTelemetry Collector محلي بإعداد tail-based sampling.
# otel-collector.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
timeout: 5s
send_batch_size: 512
tail_sampling:
decision_wait: 10s
policies:
- name: errors-policy
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow-policy
type: latency
latency: { threshold_ms: 1000 }
- name: probabilistic-policy
type: probabilistic
probabilistic: { sampling_percentage: 5 }
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls: { insecure: true }
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch, tail_sampling]
exporters: [otlp/jaeger]الخطوة 4: شغّل Jaeger في Docker.
docker run -d --name jaeger \
-p 16686:16686 -p 4317:4317 \
jaegertracing/all-in-one:latestافتح http://localhost:16686 وهتلاقي traces الطلبات بتظهر تلقائي. لو طلب أخد أكتر من ثانية، هتشوف بالظبط أي span هي السبب.
Sampling: ليه مش كل request بيتسجّل
تسجيل كل trace في production بسعر عالي — تخزين، شبكة، وتأثير على الـ CPU. القاعدة العملية فيها نوعين:
- Head-based sampling: القرار بيتاخد من أول الطلب بنسبة ثابتة (مثلًا 5%). أرخص، بس بتفقد الـ errors النادرة لأنها ممكن متطلعش في الـ 5%.
- Tail-based sampling: القرار في الـ Collector بعد ما الـ trace كامل. بتقدر تحفظ كل الـ errors و كل الـ slow requests، وتاخد عينة من الباقي. أغلى في الـ memory، بس أدق بكتير.
التوصية العملية: tail-based في الـ Collector لو عندك أكتر من 1000 req/s. ده الإعداد اللي بتنصح بيه فرق الـ observability في 2026 لأنه بيوازن بين التكلفة والرؤية.
قياس فعلي: قبل وبعد
في فريق شغّل Distributed tracing على 6 microservices لأول مرة، الأرقام كانت كده (تقديرية بناءً على تجارب موثقة):
- متوسط الـ MTTR (Mean Time To Recovery) نزل من 28 دقيقة لـ 6 دقايق.
- اكتشفوا إن 70% من الـ slow requests سببها N+1 queries في خدمة واحدة، مكنّوش بيشكوا فيها أصلًا.
- الـ overhead على CPU كان حوالي 3% بعد batch processor + 5% sampling.
الـ Trade-offs بصراحة
كل توصية تقنية ليها ثمنها:
- المكسب: رؤية واضحة لكل طلب في النظام، MTTR أقل، capacity planning أدق، اكتشاف bugs مخفية في الـ inter-service communication.
- الخسارة: overhead على الـ CPU بنسبة 2–8% حسب نسبة الـ sampling. تخزين traces مكلّف لو 100% sampling. تعقيد إضافي في الـ infra (Collector + storage backend زي Jaeger أو Tempo).
- التكلفة الخفية: تعليم الفريق إزاي يقرأ trace waterfall محتاج 2–3 جلسات. بدون ده، الـ tool هيتركّب ومحدش هيستخدمه.
Jaeger ولا Tempo: أنهي backend تختار
الاختيار الشائع بين backend اتنين:
- Jaeger: ناضج، عنده indexing قوي، بحث سريع بالـ trace ID والـ tags. مناسب لـ on-prem أو فرق متوسطة. من Jaeger v2 بقى يستخدم OpenTelemetry Collector كقلب pipeline.
- Grafana Tempo: مفيش indexes، بيكتب direct على object storage (S3, GCS). أرخص بكتير في الـ scale، بس البحث محتاج Trace ID — مش بيدعم بحث بالـ attributes زي Jaeger.
القاعدة: لو هتخزّن أكتر من 100GB traces في الشهر، Tempo هيوفّرلك تكلفة كبيرة. لو محتاج بحث متقدم بالـ tags، Jaeger أفضل.
متى لا تستخدم Distributed Tracing
الأداة دي مش حل لكل حالة. ابعد عنها لو:
- عندك monolith واحد بدون اتصالات خارجية معقدة — APM عادي زي New Relic أو SigNoz بدون tracing هيكفي.
- الفريق بتاعك مش جاهز يقرأ الـ traces — tracing بدون استخدام = تكلفة بدون فايدة.
- الـ service الواحد بيرد في أقل من 50ms ومفيش complaints — مش أولوية دلوقتي، اشتغل على حاجة تانية.
- عندك ميزانية محدودة جدًا والـ traffic منخفض — التكلفة التشغيلية للـ Collector و الـ storage مش هتبرّر القيمة.
الخطوة التالية
افتح أبسط service عندك بيعدي على DB. ثبّت opentelemetry-distro وشغّله بـ opentelemetry-instrument. اعمل 10 طلبات على endpoint بطيء وافتح Jaeger UI. لو ما لقيتش الـ DB span واضحة بـ duration وقاعدة الـ query، يبقى auto-instrumentation مش شغّال — راجع الـ OTEL_EXPORTER_OTLP_ENDPOINT وتأكد إن الـ Collector receiver شغّال على نفس الـ port.