أسس قابلية الرصد

ممارسات التسجيل المنظم

18 دقيقة الدرس 6 من 28

ممارسات التسجيل المنظم

السجلات هي أقدم إشارة للمراقبة، وهي أيضاً الأكثر إساءةً في الاستخدام. تنتج استراتيجية التسجيل السيئة تيرابايتات من الضوضاء، وفواتير Datadog بـ50 ألف دولار شهرياً، ومهندسين يبحثون بأمر grep في جدران من النص غير المنسق الساعة 3 صباحاً بينما يشتعل حادث في الإنتاج. أما الاستراتيجية الجيدة فتنتج سجلاً قابلاً للبحث والتربط وفعّالاً من حيث التكلفة لكل ما فعله نظامك — ولماذا.

يغطي هذا الدرس الممارسات الثلاث التي تفصل التسجيل الاحترافي عن الفوضى: السجلات المنظمة (JSON)، ومعرّفات الترابط، ومستويات السجل المنضبطة. هذه هي الإعدادات الافتراضية في كل شركة تقنية كبرى لسبب وجيه.

لماذا سجلات JSON أفضل من النص العادي

عالم التسجيل القديم يبدو هكذا: 2024-03-15 10:23:41 INFO User 4291 placed order 8820. هذا السطر مقروء للإنسان بشكل منفرد، لكنه شبه عديم الفائدة على نطاق واسع. تحليله يتطلب تعبيرات regex مخصصة لكل خدمة، وإضافة حقل جديد يعني كسر لوحات التحكم الموجودة.

التسجيل المنظم يعني إصدار كل إدخال سجل كوثيقة قابلة للقراءة آلياً — JSON في الغالب — حيث كل حقل زوج مفتاح-قيمة بنوع محدد. يصبح نفس الحدث:

{"timestamp":"2024-03-15T10:23:41.882Z","level":"info","service":"order-svc","version":"1.4.7","user_id":4291,"order_id":8820,"amount_cents":4999,"currency":"USD","event":"order.placed","duration_ms":43}

الآن يمكن لأي منصة تجميع سجلات — Elasticsearch أو Loki أو Splunk أو CloudWatch — فهرسة كل حقل. يمكنك تصفية level:error AND service:order-svc AND amount_cents:>10000 في ملي ثانية دون كتابة أي regex. لوحات التحكم يمكنها تجميع avg(duration_ms) مجمّعاً حسب service. هذا ليس كمالياً على نطاق واسع؛ إنه متطلب أساسي للمراقبة.

الحقول المطلوبة: ما يجب أن تحمله كل سطر سجل

اتفق على مخطط موحد عبر جميع الخدمات، يُفرَّض على مستوى مكتبة التسجيل — وليس بالاعتماد على المطورين الأفراد لتذكره. كحد أدنى، يجب أن يحمل كل سطر سجل:

  • timestamp — UTC، بصيغة ISO 8601 مع المللي ثانية. لا وقت محلي أبداً.
  • level — نص بأحرف صغيرة: debug أو info أو warn أو error أو fatal.
  • service — اسم الخدمة المنطقية، وليس اسم المضيف.
  • version — إصدار القطعة المنشورة أو SHA الالتزام. ضروري لربط خطأ بنشر.
  • trace_id / correlation_id — تُغطى بالتفصيل أدناه.
  • message — نص قصير ثابت. لا تبنِ الرسالة بدمج نصوص ديناميكية؛ ضع تلك البيانات في حقول مخصصة.
  • envproduction أو staging أو dev.
حقل الرسالة ثابت. اكتب "message": "order.placed" وضع البيانات الديناميكية في "order_id": 8820. إذا تغيرت سلسلة الرسالة مع كل استدعاء، لا يمكن لمجمّعات السجل تجميع تكرارات نفس الحدث — ستحصل على 10,000 سطر سجل فريد بدلاً من حدث واحد أُطلق 10,000 مرة.

معرّفات الترابط: تتبع طلب عبر الخدمات

طلب مستخدم واحد في نظام الخدمات المصغرة يلمس 5 إلى 20 خدمة. عندما يفشل ذلك الطلب، تحتاج لمتابعة السلسلة من بوابة API عبر خدمة المصادقة وخدمة المنتجات وخدمة الدفع وخدمة الإشعارات. بدون معرّف مشترك أنت تطابق الطوابع الزمنية وتتمنى.

معرّف الترابط (يُسمى أيضاً trace ID أو request ID) هو معرّف فريد يُنشأ عند نقطة دخول النظام — بوابة API أو موزع الحمل — ويُنقل كرأس HTTP عبر كل استدعاء لاحق. كل خدمة تقرأ الرأس وتُرفق المعرّف بكل سطر سجل تُصدره لذلك الطلب.

Correlation ID propagation across microservices Client Browser / App API Gateway يولّد trace_id: a3f9... X-Trace-Id: a3f9b2c1 يُنقل في كل استدعاء لاحق Auth Service يسجل trace_id Order Service يسجل trace_id Notify Service يسجل trace_id Payment Svc يسجل trace_id Log Aggregator filter: trace_id=a3f9b2c1
معرّف ترابط واحد يُنشأ عند البوابة يربط سجلات جميع الخدمات لطلب واحد.

آلية النقل القياسية هي رؤوس HTTP. معيار W3C Trace Context (traceparent / tracestate) هو المعيار الحديث المستخدم في OpenTelemetry وZipkin وJaeger. إذا لم تستخدم بعد مكدس التتبع الكامل، حتى رأس مخصص بسيط مثل X-Request-ID يُعدّ تحسناً هائلاً مقارنة بلا شيء.

# وسيط Node.js / Express يقرأ أو يولد معرّف ترابط # ويربطه بكل استدعاء تسجيل في نطاق ذلك الطلب app.use((req, res, next) => { const traceId = req.headers['traceparent'] || req.headers['x-request-id'] || crypto.randomUUID(); req.traceId = traceId; res.setHeader('x-request-id', traceId); req.log = logger.child({ trace_id: traceId }); next(); }); // في معالج المسار: app.post('/orders', async (req, res) => { req.log.info({ user_id: req.user.id, event: 'order.attempt' }); // ... منطق الأعمال ... req.log.info({ order_id: result.id, amount_cents: result.total, event: 'order.placed' }); });
أرفق معرّف الترابط بالاستدعاءات الصادرة أيضاً. عندما تستدعي خدمة الطلبات خدمة الدفع، أعد توجيه رأس traceparent. في Kubernetes غالباً تتم هذه العملية تلقائياً بواسطة شبكة الخدمات (Istio / Linkerd)، لكن لا تفترض ذلك أبداً — تحقق باختبار فعلي وأكّد وصول الرأس للخدمات الفرعية.

مستويات السجل بشكل صحيح

مستويات السجل موجودة لتتيح لك ضبط التفصيل في وقت التشغيل دون إعادة نشر. عند الاستخدام الصحيح تتيح لك العمل بهدوء في الإنتاج، ورفع مستوى التفاصيل خلال الحوادث، وعدم الدفع أبداً مقابل سجلات لا تحتاجها. عند الاستخدام الخاطئ، كل سطر INFO، فاتورتك 40 ألف دولار شهرياً، والإشارة مدفونة في الضوضاء.

إليك الدلالات القياسية لكل مستوى كما تُستخدم في أنظمة الإنتاج واسعة النطاق:

  • DEBUG — حالة داخلية تفصيلية، قيم المتغيرات، الخطوات الوسيطة. يجب أن يكون معطّلاً افتراضياً في الإنتاج. فعّله ديناميكياً لخدمة محددة خلال تحقيق نشط. التكلفة: حجم عالٍ وتكلفة عالية.
  • INFO — أحداث العمل المهمة: اكتمل طلب بنجاح، بدأت مهمة، سجل مستخدم دخوله. يجب أن تكون ذات معنى لشخص غير مطوّر يقرأ السجلات. تجنب تسجيل كل استدعاء دالة كـINFO.
  • WARN — حدث شيء غير متوقع لكن النظام تعافى. نجحت إعادة المحاولة. استُدعيت نقطة نهاية API مهجورة. فتح قاطع الدائرة وتراجع. يستحق التتبع كمؤشر أولي للأخطاء المستقبلية.
  • ERROR — فشلت عملية ولم يستطع النظام التعافي منها لهذا الطلب. أدرج دائماً الاستثناء/تتبع المكدس. يجب إنذار المهندس المناوب إذا استمر. لا تستخدم ERROR للفشل المتوقع كإدخال المستخدم كلمة مرور خاطئة.
  • FATAL — العملية على وشك الخروج بسبب حالة غير قابلة للاسترداد (OOM، تكوين فاسد، فقدان اتصال قاعدة البيانات عند البدء). نادر ودائماً مثير للقلق.
أكثر خطأين شائعين في مستويات السجل في الإنتاج: (1) تسجيل كل خطأ HTTP 4xx معالَج كـERROR — هذا يغمر لوحات التحكم ويسبب التعب من التنبيهات. خطأ 404 من مستخدم كتب رابطاً خاطئاً هو INFO أو WARN على الأكثر، وليس ERROR. احتفظ بـERROR لفشل نظامك، وليس فشل المُستدعي. (2) عدم تسجيل أي شيء بمستوى WARN — الفرق يتجاهل WARN كلياً ثم يتساءل لماذا تأتي الأخطاء دائماً كمفاجآت تامة دون مؤشرات سابقة.

التكوين العملي: التسجيل المنظم مع Pino (Node.js)

معظم مكتبات التسجيل الحديثة تدعم الإخراج المنظم أصلاً. Pino لـNode.js وZap لـGo هي المعيار الذهبي للأداء. إليك إعداد Pino جاهز للإنتاج يغطي كل ما سبق:

# pino-logger.js — إعداد المسجّل المنظم القياسي import pino from 'pino'; const logger = pino({ level: process.env.LOG_LEVEL || 'info', // تجاوز عبر متغير البيئة في وقت التشغيل base: { service: process.env.SERVICE_NAME || 'unknown-svc', version: process.env.APP_VERSION || 'dev', env: process.env.NODE_ENV || 'development', }, timestamp: pino.stdTimeFunctions.isoTime, // UTC بصيغة ISO 8601 formatters: { level: (label) => ({ level: label }), // إصدار "level":"info" وليس رقماً }, transport: process.env.NODE_ENV === 'production' ? undefined : { target: 'pino-pretty', options: { colorize: true } }, redact: { paths: ['req.headers.authorization', 'body.password', 'body.card_number'], censor: '[REDACTED]', }, }); export default logger;

نقاط رئيسية: LOG_LEVEL متغير بيئة حتى يتمكن الفريق التشغيلي من تغيير التفصيلية دون إعادة نشر. خيار redact يزيل الأسرار قبل أن تصل إلى تدفق السجل — هذا غير اختياري؛ معيارا PCI-DSS وSOC 2 يتطلبانه. يتحول النقل لطباعة جميلة محلياً بينما يُصدر JSON خاماً في الإنتاج.

سجّل إلى stdout فقط. لا تكتب أبداً إلى ملفات داخل الحاوية. وقت تشغيل الحاوية أو Fluentd/Fluent Bit أو مزوّد السحابة يلتقط stdout ويوجهه إلى خلفية تجميع السجلات. الكتابة إلى ملفات تكسر هذه الأنبوبة وتكون غير مرئية لعملاء جمع السجلات في المنصة.

أخذ العينات والتحكم في التكلفة

عند أحجام حركة مرور عالية، تسجيل كل طلب بمستوى INFO ينتج كميات بيانات هائلة. خدمة تتعامل مع 100,000 طلب في الثانية، كل منها يُصدر 5 أسطر سجل، هي 500,000 سطر في الثانية — عشرات الغيغابايت في الساعة لكل خدمة. تكلفة استيعاب السجلات في Datadog أو Splunk أو New Relic بهذا الحجم تتجاوز تكلفة الحوسبة للخدمة نفسها.

استراتيجيات الإنتاج للتحكم في التكلفة دون فقدان الإشارة:

  • أخذ عينات من الرأس: سجّل 1% من الطلبات الناجحة بتفصيل كامل؛ سجّل دائماً 100% من الأخطاء والتحذيرات. طبّق على مستوى البوابة بـX-Sample-Rate حتى تحترم جميع الخدمات قرار أخذ العينات نفسه.
  • أخذ عينات من الذيل: خزّن السجلات في الذاكرة مؤقتاً، وأفرغها فقط إذا استغرق الطلب أكثر من 500ms أو أسفر عن خطأ. يتطلب وكيلاً ذكياً (OpenTelemetry Collector مع معالج أخذ عينات من الذيل).
  • فهرسة حقول مختارة: معظم الخلفيات تتيح تخزين حمولة السجل الكاملة لكن فهرسة حقول محددة فقط. فهرس level وservice وtrace_id وحقول الأحداث الرئيسية؛ خزّن الباقي كسمات باردة.

التسجيل المنظم هو الأساس الذي تُبنى عليه لوحات التحكم والتتبع الموزع والتنبيهات. أتقنه من اليوم الأول وكل استثمار آخر في المراقبة يتراكم فوقه. أضفه لاحقاً لنظام قائم وستدفع الثمن مرتين.