يجمع هذا الدرس الختامي كل ما في الدرس التعليمي في سير عمل متكامل يمثل البيئة الإنتاجية بدقة. ستُجهّز تدفق تجارة إلكترونية من ثلاث خدمات — بوابة API وخدمة طلبات وخدمة مخزون — بـ OpenTelemetry، وتُوجّه الامتدادات عبر OTel Collector، وتُخزّنها في Jaeger، وتُجري تحقيقاً واقعياً في تأخر باستخدام شلال التتبع. كل خطوة هنا تعكس ما تفعله فعلاً فرق هندسية في Stripe وDoorDash وShopify عند تتبع حركة مرور الإنتاج.
معمارية المشروع التجريبي
السيناريو: يدخل طلب POST /orders إلى api-gateway (Node.js)، الذي يستدعي order-service (Python/FastAPI)، الذي بدوره يستدعي inventory-service (Go) لحجز المخزون. خدمة المخزون تكتب أيضاً إلى PostgreSQL. تُصدر الخدمات الثلاث امتدادات OTLP إلى OTel Collector مشترك، يُصدّرها إلى Jaeger. هذا تصميم بسيط لكنه واقعي — أنماط التجهيز ذاتها تتسع لشبكات من 200 خدمة.
معمارية المشروع: ثلاث خدمات مُجهَّزة تُصدر امتدادات OTLP إلى Collector مشترك يُصدّرها إلى Jaeger للعرض والتحليل.
الخطوة 1 — تشغيل البنية التحتية
استخدم Docker Compose لتشغيل Jaeger وOTel Collector جنباً إلى جنب. في الإنتاج ستنشرها كأحمال عمل Kubernetes (Deployment + Service)، لكن Compose مثالي للتكرار المحلي. احفظ هذا كـ docker-compose.yml في جذر مشروعك.
أنشئ الآن otel-collector-config.yml. يعكس هذا إعداد Collector إنتاجياً أدنى — معالج batch لتقليل ضغط الكتابة على Jaeger، ومحدد الذاكرة لمنع OOM تحت ذروة حركة المرور.
الفكرة الأساسية — ترتيب المعالجات مهم: ضع دائماً memory_limiterقبلbatch في الأنبوب. إن ارتفع استخدام الذاكرة، يُسقط المحدد الامتدادات قبل تراكمها في مخزن الـ batcher. عكس الترتيب يعني أن الـ batcher يمتلئ تحت الضغط ثم يتخلص المحدد من مخزن كبير دفعة واحدة — محدثاً ارتفاعات في زمن الاستجابة في الـ Collector ذاته.
الخطوة 2 — تجهيز خدمة الطلبات (Python)
خدمة الطلبات تطبيق FastAPI. ثبّت حزم OTel SDK وهيّئ التجهيز التلقائي لـ HTTP وSQLAlchemy. استخدم التجهيز بلا كود لطبقات الإطار، والامتدادات اليدوية لمنطق أعمالك الخاص — هذا هو النمط الإنتاجي: دع التجهيز التلقائي يغطي الشيفرة المتكررة الممللة، واحتفظ بالامتدادات اليدوية للعمليات المهمة لنطاق خدمتك.
# إضافات requirements.txt
opentelemetry-api==1.23.0
opentelemetry-sdk==1.23.0
opentelemetry-instrumentation-fastapi==0.44b0
opentelemetry-instrumentation-httpx==0.44b0
opentelemetry-instrumentation-sqlalchemy==0.44b0
opentelemetry-exporter-otlp-proto-grpc==1.23.0
# --- order_service/tracing.py ---
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
import os
def configure_tracing():
resource = Resource.create({
SERVICE_NAME: "order-service",
"service.version": os.getenv("APP_VERSION", "unknown"),
"deployment.environment": os.getenv("ENV", "local"),
})
provider = TracerProvider(resource=resource)
exporter = OTLPSpanExporter(
endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://otel-collector:4317"),
insecure=True,
)
provider.add_span_processor(BatchSpanProcessor(
exporter,
max_queue_size=2048,
max_export_batch_size=512,
schedule_delay_millis=5000,
))
trace.set_tracer_provider(provider)
# --- order_service/main.py ---
@app.post("/orders")
async def create_order(payload: dict):
with tracer.start_as_current_span("validate-order") as span:
span.set_attribute("order.item_count", len(payload.get("items", [])))
span.set_attribute("order.customer_tier", payload.get("tier", "standard"))
if not payload.get("items"):
span.set_status(trace.StatusCode.ERROR, "empty order")
span.record_exception(ValueError("Order has no items"))
raise HTTPException(400, "Order must contain at least one item")
# استدعاء httpx مُجهَّز تلقائياً — traceparent يُحقن تلقائياً
async with httpx.AsyncClient() as client:
resp = await client.post(
"http://inventory-service:8001/reserve",
json={"items": payload["items"]},
timeout=5.0,
)
resp.raise_for_status()
return {"order_id": "ord_" + str(uuid.uuid4())[:8], "status": "confirmed"}
ممارسة احترافية — سمات الـ Resource هي هويتك في Jaeger: اضبط دائماً service.name وservice.version وdeployment.environment في الـ Resource. يستخدم Jaeger service.name كمفتاح فهرس رئيسي لقائمة الخدمات. بدون service.version، لا تستطيع التمييز بين أي نشر أدخل انحدار زمن الاستجابة حين تعمل ثلاث بودات canary بإصدارات مختلفة في وقت واحد.
الخطوة 3 — تجهيز خدمة المخزون (Go)
تستخدم خدمة Go حزمة OTel Go SDK. غلّف استدعاءات قاعدة البيانات بامتدادات يدوية — مشغّل Go SQL ليس لديه تجهيز تلقائي مستقر في كل الإصدارات، لذا إنشاء الامتداد الصريح هو النهج الموثوق. لاحظ كيف تتبع سمات الامتداد الاتفاقيات الدلالية لـ OpenTelemetry لعمليات قاعدة البيانات: db.system وdb.name وdb.statement.
// inventory-service/tracing.go
func InitTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("otel-collector:4317"),
otlptracegrpc.WithInsecure(),
)
res, _ := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName("inventory-service"),
semconv.ServiceVersion("1.4.2"),
attribute.String("deployment.environment", "local"),
),
)
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
// 10% head-based sampling في الإنتاج؛ 100% محلياً
sdktrace.WithSampler(sdktrace.TraceIDRatioBased(1.0)),
)
otel.SetTracerProvider(tp)
return tp, nil
}
// inventory-service/handler.go
func reserveStock(ctx context.Context, items []Item) error {
tracer := otel.Tracer("inventory-service")
ctx, span := tracer.Start(ctx, "db.reserve-stock")
defer span.End()
span.SetAttributes(
attribute.String("db.system", "postgresql"),
attribute.String("db.name", "inventory"),
attribute.Int("db.items_count", len(items)),
// ملاحظة: احذف db.statement في الإنتاج إن كان يحتوي على بيانات شخصية أو أسرار
attribute.String("db.statement", "UPDATE stock SET reserved = reserved + $1 WHERE sku = $2"),
)
// تنفيذ استعلامات قاعدة البيانات...
span.SetStatus(codes.Ok, "")
return nil
}
الخطوة 4 — توليد الحمل والعثور على تتبع في Jaeger
شغّل الكومة، أرسل طلباً تجريبياً، وانتقل إلى واجهة Jaeger على http://localhost:16686.
# تشغيل Collector وJaeger
docker compose up -d
# إرسال طلب لإنشاء طلب شراء
curl -X POST http://localhost:8000/orders \
-H "Content-Type: application/json" \
-d '{"items":[{"sku":"SKU-001","qty":2},{"sku":"SKU-007","qty":1}],"tier":"gold"}'
# الاستجابة المتوقعة:
# {"order_id":"ord_a3f9bc12","status":"confirmed"}
# التحقق من أن الـ Collector يستقبل الامتدادات
curl http://localhost:55679/debug/tracez | grep -A5 "order-service"
# في واجهة Jaeger:
# 1. اختر "order-service" من قائمة الخدمات
# 2. انقر "Find Traces"
# 3. انقر على التتبع الذي يُظهر POST /orders
# 4. لاحظ الشلال: api-gateway جذر → validate-order → httpx POST → db.reserve-stock
الخطوة 5 — حقن خطأ تأخر وتصحيحه بالتتبعات
حاكِ الآن السيناريو الذي يواجهه كل مهندس مناوبة في نهاية المطاف: انحدار في زمن الاستجابة في خدمة خارجية غير مرئي من لوحة مقاييس المستوى الأعلى. احقن تأخيراً اصطناعياً في خدمة المخزون — هذا يمثل فحص SQL بطيئاً، أو كاش بارداً، أو API خارجية مثقلة.
// احقن هذا في reserveStock() لمحاكاة استعلام بطيء
time.Sleep(350 * time.Millisecond)
span.AddEvent("slow-index-scan-detected", trace.WithAttributes(
attribute.String("db.index", "stock_sku_idx"),
attribute.Int("db.rows_scanned", 95000),
))
بعد حقن هذا التأخير، أرسل طلباً آخر. افتح Jaeger، وابحث عن التتبعات من order-service بمدة أكبر من 300ms (استخدم مرشح "Min Duration"). انقر على التتبع البطيء. ستر بدقة ما يكشفه التجهيز:
الامتداد الجذر POST /checkout في api-gateway يُظهر الآن ~370ms إجمالاً.
امتداد validate-order يكتمل في ~2ms — ليس هو السبب.
امتداد httpx POST /reserve هو ~365ms — الاستدعاء إلى المخزون بطيء.
بداخله، db.reserve-stock هو ~355ms، وسجل أحداثه يُظهر slow-index-scan-detected مع db.rows_scanned: 95000.
السبب الجذري محدد في أقل من 60 ثانية. لا بحث بـ grep في السجلات. لا تنقل بين اللوحات. سمة الامتداد db.rows_scanned: 95000 تخبرك بالضبط ما تُصلحه: أضف فهرساً تغطوياً على stock(sku). هذه القيمة الكاملة للتتبع الموزع تتحقق عملياً.
شلال التتبع بعد حقن خطأ التأخر: حدث slow-index-scan على امتداد db.reserve-stock يحدد السبب الجذري والإصلاح فوراً.
في الإنتاج، تكتمل حلقة قابلية الملاحظة الكاملة حين تستطيع القفز من تنبيه Prometheus إلى تتبع محدد. تدعم OpenTelemetry exemplars — مراجع trace ID مضمّنة داخل نقاط بيانات المقاييس. حين يجمع Prometheus بياناتك، يمكن لحاويات المدرّج التكراري ذات الزمن العالي أن تحمل trace_id يرتبط مباشرة بالتتبع الذي أنتج تلك النقطة.
في Python مع مُصدّر OTel Prometheus، تُصدَر الـ exemplars تلقائياً حين يكون الامتداد الحالي مأخوذاً كعينة. في Grafana، فعّل الـ exemplars على لوحة المدرّج التكراري لترى نقاطاً برتقالية على الحاويات البطيئة — انقر أي نقطة للقفز مباشرة إلى تتبع Jaeger. هذا يلغي الخطوة اليدوية لنسخ trace ID من سطر سجل إلى مربع بحث Jaeger. إنه سير عمل "مقاييس-إلى-تتبعات" القياسي المستخدم في Grafana Cloud وDatadog وHoneycomb.
يجب أيضاً حقن trace_id في كل سطر سجل منظم يُصدَر خلال طلب. في Python مع structlog أو جسر تسجيل OTel، يحدث هذا تلقائياً حين تُهيّئ حزمة SDK تسجيل OTel. النتيجة: من استعلام Loki يمكنك استخراج حقل trace_id ومتابعته مباشرة إلى واجهة Jaeger — مغلقاً مسار الترابط "سجلات-إلى-تتبعات".
ممارسة احترافية — الترابط الثلاثي: في Uber وNetflix، المعيار الذهبي هو أن أي إشارة — تنبيه أو سطر سجل أو حاوية مدرّج تكراري ذات زمن عالٍ — تحمل trace_id قابلاً للنقر. إعداد هذا يتطلب: (1) نشر سياق تتبع OTel في السجلات عبر جسر التسجيل؛ (2) تفعيل exemplars على المدرّجات التكرارية؛ (3) تهيئة Grafana بمصدر بيانات Jaeger مرتبط بمصدر بيانات Prometheus. بعد التوصيل، يمكن لمهندس المناوبة الانتقال من تنبيه PagerDuty ← لوحة Grafana ← exemplar ذو زمن عالٍ ← تتبع Jaeger ← امتداد السبب الجذري في أقل من 3 دقائق. هذا هو هدف MTTD لمنظمة SRE ناضجة.
الخطوة 7 — أخذ العينات بالذيل لحجم الإنتاج
إعدادك المحلي يأخذ عينات من 100% من التتبعات. في الإنتاج عند إنتاجية ذات قيمة، لا تستطيع تخزين كل امتداد. الاستراتيجية الصحيحة — مغطاة بعمق في الدرس 7 — هي أخذ العينات بالذيل في الـ Collector: اجمع كل الامتدادات، اتخذ قرار أخذ العينة بعد اكتمال التتبع، واحتفظ بـ 100% من تتبعات الأخطاء والتتبعات البطيئة مع أخذ عينات من التتبعات الصحية السريعة بنسبة 1-10%.
أضف هذا إلى إعداد Collector لتفعيل أخذ العينات بالذيل في الإنتاج:
# أضف إلى قسم processors في otel-collector-config.yml:
processors:
tail_sampling:
decision_wait: 30s # انتظر حتى 30 ثانية لوصول كل الامتدادات
num_traces: 100000 # احتفظ بحتى 100k تتبع جارٍ في الذاكرة
expected_new_traces_per_sec: 1000
policies:
- name: keep-errors
type: status_code
status_code: {status_codes: [ERROR]}
- name: keep-slow
type: latency
latency: {threshold_ms: 500}
- name: probabilistic-baseline
type: probabilistic
probabilistic: {sampling_percentage: 5} # احتفظ بـ 5% من التتبعات السريعة الصحية
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling, batch]
exporters: [otlp/jaeger, logging]
مصيدة إنتاجية — ذاكرة أخذ العينات بالذيل: يحدد إعداد num_traces عدد التتبعات المفتوحة التي يحتفظ بها الـ Collector في الذاكرة أثناء انتظار وصول كل الامتدادات. عند 1,000 طلب في الثانية مع نافذة 30 ثانية، تحتاج سعة لـ 30,000 تتبع جارٍ. كل تتبع يحتفظ بامتداداته في الذاكرة — عند 10 امتدادات لكل تتبع بمتوسط 1KB لكل منها، ذلك 300MB من ذاكرة العمل فقط لمخزن أخذ العينات. حجّم بود الـ Collector وفقاً لذلك، واضبط limit_mib على memory_limiter باعتدال. سيُسقط الـ Collector الامتدادات بأناقة قبل الـ OOM، لكن نافذة قرار أخذ العينة ستتقلص تحت الضغط — راقب otelcol_processor_tail_sampling_sampling_decision_timer_latency في Prometheus.
اكتمال المشروع: ما بنيته
لديك الآن نظام تتبع موزع يعمل بشكل كامل. ثلاث خدمات تُصدر امتدادات بصيغة OTLP، يستقبلها الـ Collector ويجمعها بكفاءة ويطبق أخذ العينات بالذيل ويُصدّرها إلى Jaeger. أظهر شلال التتبع اختناق قاعدة بيانات بمقدار 355ms في أقل من 60 ثانية — وهو ما كان سيستغرق 30-45 دقيقة بربط السجلات. الـ Exemplars تغلق الحلقة من المقاييس إلى التتبعات، وجسر التسجيل يغلقها من السجلات إلى التتبعات. هذه هي قابلية الملاحظة الموحدة: نفس الحدث مرئي من ثلاثة زوايا، كل منها قابل للتنقل إلى الآخرين بنقرة واحدة.
الأنماط هنا — OTLP عبر gRPC، وBatchSpanProcessor، والاتفاقيات الدلالية لسمات db.* وhttp.*، وأخذ العينات بالذيل في الـ Collector، والـ exemplars في Prometheus — هي بالضبط ما ستوصّله لدى أي صاحب عمل في بيئة التقنية الكبرى. OpenTelemetry الآن الخيار الافتراضي في Google وAWS وMicrosoft وكل منظمة cloud-native رئيسية. حياد SDK من البائع يعني أنك تستطيع مبادلة Jaeger بـ Tempo، أو Tempo بـ Honeycomb، بتغيير سطرين في إعداد الـ Collector — شيفرة تطبيقك غير متأثرة. تلك الإمكانية على النقل هي القيمة الدائمة لبناء منظومة OTel القياسية.
اكتمل الدرس التعليمي: غطّيت قوس قابلية الملاحظة الكامل — من السبب في التتبع (الدرس 1) عبر الامتدادات ونشر السياق (الدرس 2)، ومعيار OpenTelemetry (الدرس 3)، وتجهيز حزمة SDK (الدرس 4)، والـ Collector (الدرس 5)، وخلفيات Jaeger وTempo (الدرس 6)، واستراتيجيات أخذ العينات (الدرس 7)، وسير عمل تصحيح الإنتاج (الدرس 8)، وقابلية الملاحظة الموحدة بالركائز الثلاث (الدرس 9)، وهذا المشروع الشامل (الدرس 10). أنت مجهّز لتصميم وتنفيذ وتشغيل تتبع موزع إنتاجي على نطاق واسع.
نستخدم ملفات تعريف الارتباط لتشغيل هذا الموقع وتحليل الزيارات وعرض إعلانات مخصّصة. يمكنك قبول كل ملفات تعريف الارتباط أو رفض غير الأساسية منها.
سياسة الخصوصية