تقنيات وتطبيق
السجلات المنظمة والتتبع الموزع في بيئات الإنتاج
١٤ أكتوبر ٢٠٢٦ · 8 دقائق قراءة
أسطورة «اطبع النص في السجل وابحث عنه بـ grep»
في المشاريع الصغيرة، كتابة سجلات نصية عادية مثل:
INFO: 2026-10-14 10:00:00 - user 42 made an order with total 150
قد تبدو كافية ومريحة للقراءة في الطرفية. لكن بمجرد أن ينتقل تطبيقك للعمل خلف موازن أحمال (Load Balancer) عبر 8 حاويات Docker متوازية تخدم آلاف الطلبات في الثانية، تصبح هذه السجلات كابوساً تشغيلياً:
- لا يمكنك تصفية السجلات حسب المعرف
user_idدون كتابة تعابير regex بطيئة ومعقدة. - السجلات تتداخل بين آلاف الطلبات المتزامنة، فلا تعرف أي سطر ينتمي لأي طلب.
- أدوات التحليل والتجميع (مثل Datadog، Grafana Loki، ElasticSearch) لا تستطيع استخراج مؤشرات الأداء بسهولة من نصوص غير مهيكلة.
1. الانتقال إلى سجلات JSON المنظمة (Structured JSON)
في بيئة الإنتاج، قارئ السجلات الأساسي ليس عين الإنسان، بل نظام معالجة وتجميع السجلات. لذلك، يجب أن يخرج كل سطر ككائن JSON صالح وموحد الحقول:
{
"timestamp": "2026-10-14T10:00:00.124Z",
"level": "INFO",
"message": "Order processed successfully",
"service": "billing-api",
"correlation_id": "a9f8c12b-e452-4f8a-9213-91b4091a1e84",
"user_id": 42,
"order_id": 10842,
"duration_ms": 14.2,
"status_code": 201
}
بهذا التنسيق، يستطيع أي نظام مراقبة خلال جزء من الثانية الإجابة عن أسئلة مثل:
- «اعرض كل الطلبات التي استغرقت أكثر من 500 مللي ثانية للمستخدم 42».
- «احسب معدل الأخطاء (5xx) لكل مسار API خلال آخر 15 دقيقة».
2. معرّف الارتباط (Correlation ID) عبر مهام Asyncio
أكبر تحدٍ في بايثون غير المتزامنة هو أن مئات المهام (asyncio.Task) تتشارك نفس الخيط (Thread). إذا قمت بحفظ معرّف الطلب في متغير عام (global) أو في threading.local، ستتسرب قيم الطلبات بين المستخدمين وتختلط السجلات!
الحل المعماري السليم هو استخدام contextvars.ContextVar:
import contextvars
import uuid
from starlette.middleware.base import BaseHTTPMiddleware
correlation_id_ctx: contextvars.ContextVar[str] = contextvars.ContextVar("correlation_id", default="")
class CorrelationIdMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
# استخراج المعرف القادم أو إنشاء واحد جديد
cid = request.headers.get("X-Correlation-ID", str(uuid.uuid4()))
token = correlation_id_ctx.set(cid)
try:
response = await call_next(request)
response.headers["X-Correlation-ID"] = cid
return response
finally:
correlation_id_ctx.reset(token)
تتميز contextvars بأنها تُورَّث تلقائياً لأي مهمة فرعية يتم إنشاؤها عبر asyncio.create_task()، مما يضمن أن كل عملية خلفية أو استعلام قاعدة بيانات يحمل نفس معرّف الارتباط للطلب الأصلي.
3. تعتيم البيانات الحساسة تلقائياً (Secret Masking)
من أخطر الثغرات الأمنية في التطبيقات تسريب كلمات المرور، أو رموز المصادقة (Tokens)، أو أرقام البطاقات في سجلات النظام. بمجرد دخول هذه البيانات للسجلات، يمكن لأي مهندس أو نظام خارجي الاطلاع عليها.
يجب دمج مصفاة تصفية (Log Filter) تفحص الحقول الحساسة (authorization, password, secret, token) وتستبدلها بـ [REDACTED] قبل كتابتها على القرص أو إرسالها إلى stdout.
الخلاصة
المراقبة والتشخيص في الإنتاج ليست ترفاً؛ السجلات المنظمة بصيغة JSON، مع معرّف ارتباط موحد عبر contextvars، وتعتيم للأسرار، هي الفارق بين حل عطل معقد في 5 دقائق، أو قضاء ليلة كاملة في التخمين وسط آلاف السطور العشوائية.