حيل بسيطة لتحسين قابلية صيانة تطبيقك (الجزء 3): معرّفات الارتباط والتتبع الشامل من البداية إلى النهاية
معرّفات الارتباط والتتبّع الشامل من البداية إلى النهاية
باختصار: عندما يمتد التنفيذ عبر الخيوط وقوائم الانتظار والزمن، تكون معرّفات الارتباط هي الطريقة الموثوقة الوحيدة للحفاظ على تماسك سجلات الأحداث.
في الجزء 1 (→ لماذا تتوقف السجلات عن كونها مفيدة في الأنظمة الحقيقية)، رأينا كيف تفشل أنماط التسجيل الأساسية تحت التزامن والنطاق.
في الجزء 2 (→ سياق المستخدم دون الإخلال بتصميمك)، أضفنا سياق المستخدم باستخدام MDC، دون تلويث منطق الأعمال.
يحل هذا النهج العديد من مشكلات الإنتاج الحقيقية – ولكن ليس جميعها. لنلقِ نظرة على ما يحدث عندما لا يعود السياق القائم على المستخدم كافياً.
معرّف الارتباط
تصفية السجلات حسب معرّف المستخدم فعّالة في كثير من الحالات، لكن هناك حالات مهمة لا تنجح فيها ببساطة.
يجب أن يعرّض كل نظام يستخدم المصادقة نقطة نهاية مفتوحة واحدة على الأقل، مثل نقطة نهاية تسجيل الدخول. يمكن استدعاء نقطة النهاية تلك من قبل المستخدمين غير المصادق عليهم، مما يعني أنه لا يوجد userId متاح في MDC.
هناك أيضاً سيناريوهات أخرى حيث لا تكون هوية المستخدم موجودة أو غير مفيدة:
- المستخدمون الضيوف،
- المهام الخلفية،
- المهام المجدولة،
- المعالجة غير المتزامنة.
لنلقِ نظرة على مثال ملموس.
❌ المشكلة: التنفيذ غير المتزامن يُفسد التسلسل
يقوم مستخدم ضيف بطلب عنصر واختياريًا يطبق قسيمة خصم. تتم معالجة الدفع بشكل غير متزامن.
تبدو السجلات على النحو التالي:
2025-10-25 19:32:25,446 INFO 305188 [t=Thread-2] demo.PrintingDemo Guest used coupon code = ***** : [u=] 2025-10-25 19:32:25,451 INFO 305188 [t=Thread-2] demo.PrintingDemo Guest started payment process : [u=] 2025-10-25 19:32:25,454 INFO 305188 [t=Thread-1] demo.PrintingDemo Guest skipped coupon code : [u=] 2025-10-25 19:32:25,461 INFO 305188 [t=Thread-3] demo.PrintingDemo Guest skipped coupon code : [u=] 2025-10-25 19:32:25,548 INFO 305188 [t=Thread-1] demo.PrintingDemo Guest started payment process : [u=] 2025-10-25 19:32:25,567 INFO 305188 [t=Thread-3] demo.PrintingDemo Guest started payment process : [u=] 2025-10-25 19:32:25,772 INFO 305188 [t=Async--6] demo.PrintingDemo Guest aborted payment : [u=] 2025-10-25 19:32:25,967 INFO 305188 [t=Async--7] demo.PrintingDemo Guest finished payment : [u=] 2025-10-25 19:32:26,231 INFO 305188 [t=Async--8] demo.PrintingDemo Guest finished payment : [u=]
السؤال الذي نحتاج إلى الإجابة عليه بسيط: هل أكمل الضيف الذي استخدم رمز القسيمة عملية الدفع؟
من هذه السجلات وحدها، يستحيل تحديد ذلك. لن يساعد معرّف الخيط (Thread ID) هنا، لأن إنهاء الدفع يعمل على مجموعة خيوط مختلفة. ولن يساعد معرّف العملية (PID) أيضًا. أما معرّف المستخدم (User ID) فهو غير موجود.
لدينا جميع الفعاليات، لكن لا توجد طريقة موثوقة لتحديد أي منها ينتمي إلى بعضها.
✅ الحل: ابتكر معرّف تنفيذ
إذا كان معرّف المستخدم ومعرّف الخيط ومعرّف العملية غير كافية، فماذا يمكننا استخدام أيضًا؟ الإجابة بسيطة بشكل مفاجئ: لا نحتاج إلى إعادة استخدام معرف موجود – يمكننا إنشاء واحد.
أي قيمة فريدة محليًا ستفي بالغرض: سلسلة عشوائية، أو رقم عشوائي، أو UUID. ولأن هذا المعرف يُستخدم لإظهار العلاقات بين إدخالات السجل، فإننا نسميه معرف الارتباط.
يمثل معرّف الارتباط تنفيذاً واحداً، بغض النظر عن:
- كم عدد الخيوط المعنية،
- ما إذا كان التنفيذ متزامنًا أم غير متزامن،
- أو المدة التي يستغرقها.
الأمان مقابل قابلية القراءة
يتضمن اختيار تنسيق معرّف الارتباط المناسب مقايضة بين التفرد (الأمان) وقابلية القراءة (سهولة الاستخدام التشغيلي).
UUID القياسي هو الخيار الأكثر أمانًا:
4d2108d1-35a6-41a3-9ed2-1b146c78bb9c
يضمن التفرد، حتى عبر الأنظمة، لكنه طويل وصعب التعامل معه. لتقليل الطول، قد يفكر المرء في ترميز Base64:
TSEI0TWmQaOe0hsUbHi7nA==
ومع ذلك، يُدخل Base64 غموضًا بصريًا (0، O، l، I) ويكون عرضة للأخطاء عند النسخ يدويًا.
من الحلول الوسط الشائعة Base58، التي تتجنّب هذه الأحرف ومصمّمة للاستخدام البشري (وتُستخدم بشكل شهير في Bitcoin). وإذا لم يكن التفرّد العالمي المطلق مطلوبًا، فغالبًا ما يكفي معرّف قصير بصيغة Base58: ‘aMXaBGD‘.
استراتيجية المعرّف المزدوج
إذا كنت بحاجة إلى كل من الأمان وقابلية الاستخدام الجيدة، فإن النهج العملي هو تسجيل معرّفين اثنين:
- معرّف قصير مقروء للبشر (للبحث والتواصل)،
- UUID كامل (لليقين الجنائي).
في الحالة النادرة لحدوث تعارض، يمكنك تصفية السجلات بواسطة المعرّف القصير، ثم إزالة الغموض باستخدام UUID.
دمج معرّف الارتباط في MDC
تمامًا مثل معرّف المستخدم، يجب تخزين معرّف الارتباط في MDC في بداية التنفيذ. ثم يتم توسيع نمط التسجيل على النحو التالي:
%d{ISO8601} %-5level ${PID} [t=%thread] %-48logger{48} [c=%X{correlationId}] %msg : [u=%X{userId}] %n%throwable | +--- معرّف الارتباط …يُظهر العلاقة بين سطور السجل
هنا:
- %X{correlationId} يقرأ القيمة من MDC،
- كل سطر سجل يحمل الآن هوية التنفيذ.
مثال كامل غير متزامن
مع وجود معرّفات الارتباط، يصبح المثال غير المتزامن السابق مقروءًا:
2025-10-25 21:05:49,293 INFO 317606 [t=Thread-1] demo.PrintingDemo [c=FB6bA3f] Guest used coupon code = ***** : [u=] 2025-10-25 21:05:49,375 INFO 317606 [t=Thread-1] demo.PrintingDemo [c=FB6bA3f] Guest started payment process : [u=] 2025-10-25 21:05:51,203 INFO 317606 [t=Async--5] demo.PrintingDemo [c=FB6bA3f] Guest aborted payment : [u=]
تصفية السجلات حسب معرّف الارتباط تجيب فورًا على سؤالنا الأصلي: الضيف الذي استخدم القسيمة ألغى عملية الدفع.
لا تخمين ولا إعادة بناء يدوية.
معرّف الارتباط خارج السجلات
تصبح معرّفات الارتباط أكثر قوة عندما تتجاوز نظام التسجيل.
إذا قمنا بتضمين معرّف الارتباط في استجابات أخطاء API، فقد يحتوي تقرير خطأ المستخدم على شيء مثل هذا:
{ "timestamp": "2025-10-25T21:43:56Z", "message": "Unable to process order with negative price: -64", "userId": "ferdynand@oo.pl"، "error": "Bad request"، "status": 400، "method": "POST"، "path": "/api/orders"، "correlationId": "pHVAVwv" }
عندها يصبح تصحيح الأخطاء مباشراً. نبحث ببساطة في السجلات عن pHVAVwv ونرى التنفيذ الكامل فوراً:
2025-10-25 21:43:56,210 INFO 326565 [t=Thread-1] demo.PrintingDemo [c=pHVAVwv] Price of item = 64 : [u=ferdynand@oo.pl] 2025-10-25 21:43:56,234 INFO 326565 [t=Thread-1] demo.PrintingDemo [c=pHVAVwv] Due to shortages, quantity lowered by 3 items : [u=ferdynand@oo.pl] 2025-10-25 21:43:56,234 INFO 326565 [t=Thread-1] demo.PrintingDemo [c=pHVAVwv] Items in order = -1 : [u=ferdynand@oo.pl] 2025-10-25 21:43:56,249 ERROR 326565 [t=Thread-1] demo.PrintingDemo [c=pHVAVwv] Malformed request : [u=ferdynand@oo.pljava.lang.IllegalStateException: Unable to process order with negative price: -64 at demo.PrintingDemo.runSequence(PrintingDemo.java:92) at demo.PrintingDemo.lambda$onApplicationReady$0(PrintingDemo.java:66) at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:545) at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:328) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1095) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:619) at java.base/java.lang.Thread.run(Thread.java:1447) 2025-10-25 21:43:56,252 INFO 326565 [t=Thread-1] demo.PrintingDemo [c=pHVAVwv] Request body: {"quantity":2} : [u=ferdynand@oo.pl]
في هذه المرحلة، لا يوجد عمل تحقيقي.
الوصول إلى ما هو أبعد من المستخدمين التقنيين
غالبًا ما يبلغ المستخدمون غير التقنيين عن المشكلات عن طريق إرسال لقطات شاشة. قد يلتقطون رسالة خطأ منبثقة بدلاً من استجابة HTTP.

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

لإظهار أهمية اختيار صيغة معرف قابلة للقراءة، ضع في اعتبارك المشكلات التي تنشأ إذا استخدمنا معرفاً معقداً، مثل UUID مُشفَّر بـ Base64. يجعل الغموض البصري النسخ عرضة للأخطاء:

معرّف Base58 قصير مثل WpJ9ZWr سهل القراءة والنسخ والبحث – على عكس المعرّف الطويل والغامض بصريًا.
من خلال أخذ معرّف الارتباط من لقطة شاشة الشريط السفلي الواضحة (WpJ9ZWr) والبحث في نظام السجلات لدينا، يمكننا العثور بسرعة على تفاصيل الطلب المعني:
2025-10-25 22:19:52,938 INFO 338180 [t=Thread-1] demo.PrintingDemo [c=WpJ9ZWr] Price of item = 3 : [u=mr.1337@pwnd.it] 2025-10-25 22:19:52,961 INFO 338180 [t=Thread-1] demo.PrintingDemo [c=WpJ9ZWr] Due to shortages, quantity lowered by 3 items : [u=mr.1337@pwnd.it] 2025-10-25 22:19:52,961 INFO 338180 [t=Thread-1] demo.PrintingDemo [c=WpJ9ZWr] Items in order = -1 : [u=mr.1337@pwnd.it] 2025-10-25 22:19:53,150 ERROR 338180 [t=Thread-1] demo.PrintingDemo [c=WpJ9ZWr] Malformed request : [u=mr.1337@pwnd.itjava.lang.IllegalStateException: Unable to process order with negative price: -36 at demo.PrintingDemo.runSequence(PrintingDemo.java:92) at demo.PrintingDemo.lambda$onApplicationReady$0(PrintingDemo.java:66) at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:545) at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:328) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1095) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:619) at java.base/java.lang.Thread.run(Thread.java:1447) 2025-10-25 22:19:53,153 INFO 338180 [t=Thread-1] demo.PrintingDemo [c=WpJ9ZWr] Request body: {"quantity":2} : [u=mr.1337@pwnd.it]
ماذا لو لم يرَ أحد الفشل؟
أحيانًا تحدث الأعطال دون أن يلاحظها أي مستخدم – على سبيل المثال، في المهام المجدولة. في هذه الحالات، عادةً ما يتم تفعيل التنبيهات أو الإشعارات.
إذا كان لديك بالفعل نظام تنبيهات، ينطبق المبدأ نفسه: يجب أن يتضمن كل تنبيه معرّف ارتباط. بهذه الطريقة، يؤدي التنبيه مباشرة إلى التنفيذ ذي الصلة في السجلات.
الخلاصة
سلسلة المقالات هذه تلامس فقط سطح جعل أعمال الصيانة أسهل.
في الأنظمة الأكثر تعقيدًا، يجب نشر معرّفات الارتباط عبر الخدمات، وقد يكون الاستثمار في أدوات المراقبة المخصصة خيارًا أفضل.
ومع ذلك، تُظهر التجربة أن الغالبية العظمى من أنظمة الإنتاج لا تزال تعتمد على خدمات فردية أو عمليات نشر بسيطة. في هذه الحالات، يمكن لعدد قليل من التحسينات الصغيرة والمدروسة جيداً – مثل سياق المستخدم وMDC ومعرّفات الارتباط – أن تقلّل بشكل كبير من الوقت المستغرق في فهم حالات الفشل.
أحياناً، الحيل البسيطة تكون كافية حقاً.
تواصل معنا في حال وجود أي أسئلة!
arrow_circle_right مقالات موصى بها