بناء الخدمات المصغّرة بـ Spring Boot

التسجيل الموزّع ومعرّفات الارتباط

18 دقيقة الدرس 8 من 12

التسجيل الموزّع ومعرّفات الارتباط

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

المشكلة بأمثلة ملموسة

افترض أن طلبًا يدخل بوابة API الخاصة بك في الساعة 14:32:01.042. تستدعي البوابة خدمة الطلبات، التي تستدعي المخزون، الذي يستدعي محوّل المستودع. تسجّل كل خدمة أحداثها الخاصة. بدون ارتباط:

  • تبحث في سجلات خدمة الطلبات عن بريد المستخدم فتجد ثلاثة سجلات تتطابق مع مستخدمين مختلفين.
  • لا يمكنك تحديد استدعاء المخزون الذي ينتمي إلى أيّ استدعاء للطلبات.
  • خطأ 503 مدفون في سجل محوّل المستودع يظل غير مرئي ما لم تتفقد ذلك الملف بالصدفة.

بوجود معرّف ارتباط (X-Correlation-ID: a7f3c91b-4d2e-4f8a-b6c1-9e0d3a2b1f5c) في كل سطر سجل، يكشف بحث واحد عبر مُجمّع سجلاتك (Splunk أو Loki أو CloudWatch Insights) كل حدث في السلسلة بأكملها ترتيبًا زمنيًا.

Micrometer Tracing + Brave: مقاربة Spring Boot 3

يشحن Spring Boot 3 مع Micrometer Tracing كتجريد مدمج للتتبع الموزع، يحلّ محل Spring Cloud Sleuth الذي أُهمل. يلفّ Micrometer Tracing جسرًا قابلًا للتبديل — في معظم المشاريع يكون ذلك الجسر هو Brave من بيئة Zipkin. أضف هذه التبعيات إلى كل خدمة في pom.xml:

<!-- جسر Micrometer Tracing مع Brave --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-tracing-bridge-brave</artifactId> </dependency> <!-- مراسل Zipkin (اختياري — يصدّر النطاقات إلى Zipkin/Tempo) --> <dependency> <groupId>io.zipkin.reporter2</groupId> <artifactId>zipkin-reporter-brave</artifactId> </dependency>

بهذه التبعيات وحدها، يُهيّئ Spring Boot تلقائيًا كائن Tracer. يحصل كل طلب HTTP وارد تلقائيًا على معرّف التتبع (trace ID) الذي يُعرّف شجرة الطلب الموزعة بأكملها، ومعرّف النطاق (span ID) الذي يُعرّف وحدة العمل الحالية داخل تلك الشجرة. يُدرَج كلاهما في SLF4J MDC تحت مفتاحَي traceId وspanId، ما يجعلهما متاحَين في كل سطر سجل تلقائيًا.

معرّف التتبع مقابل معرّف النطاق: يبقى معرّف التتبع ثابتًا عبر سلسلة الاستدعاء الموزعة بأكملها. يتغير معرّف النطاق عند كل حدّ خدمة — إذ تُنشئ كل خدمة نطاقًا فرعيًا جديدًا. فكّر في التتبع كرقم تتبع الشحنة وفي كل نطاق كمرحلة واحدة من رحلة التوصيل.

ضبط نمط السجل

حدّث application.yml في كل خدمة ليظهر المعرّفان في كل سطر سجل:

# application.yml spring: application: name: order-service logging: pattern: console: "%d{HH:mm:ss.SSS} [%thread] %-5level [%X{traceId:-},%X{spanId:-}] %logger{36} - %msg%n" management: tracing: sampling: probability: 1.0 # 100% في بيئة التطوير؛ خفّضها إلى 0.1 أو أقل في الإنتاج

يقرأ التعبير %X{traceId:-} مفتاح MDC باسم traceId؛ واللاحقة :- تعني "سلسلة فارغة إذا غاب" فتُعرض سطور السجل من خيوط الخلفية التي لا تتبع تتبعًا نشطًا بشكل نظيف.

سيبدو سطر السجل الآن هكذا:

14:32:01.055 [http-nio-8081-exec-3] INFO [a7f3c91b4d2e4f8a,b6c19e0d3a2b1f5c] c.example.OrderService - Created order 9921 for user 42

نشر التتبع إلى الخدمات اللاحقة

ينشر Micrometer Tracing سياق التتبع تلقائيًا عند استخدام WebClient أو RestClient لأن Spring Boot يُسجّل مرشّح تتبع للتبادل. يجب الحصول على الـ bean الذي يُنشئه Spring Boot — لا تبنِ نسختك الخام:

@Configuration public class WebClientConfig { // احقن الباني المُهيَّأ تلقائيًا — فهو يحمل مرشّح التتبع بالفعل @Bean public WebClient inventoryClient(WebClient.Builder builder) { return builder .baseUrl("http://inventory-service") .build(); } }

عندما تستدعي OrderService الخاصة بك كائن inventoryClient، يُضيف Spring تلقائيًا ترويسة b3 (أو ترويسة W3C القياسية traceparent) إلى الطلب الصادر. تقرأ خدمة المخزون تلك الترويسة وتُنشئ نطاقًا فرعيًا وتسجّل بنفس معرّف التتبع. السلسلة بأكملها مرتبطة.

استخدم ترويسات W3C Trace Context في المشاريع الجديدة. اضبط management.tracing.propagation.type=W3C في جميع الخدمات. ترويسة W3C القياسية traceparent هي معيار IETF وتفهمها AWS X-Ray وAzure Monitor وGoogle Cloud Trace مباشرةً. الصيغة القديمة B3 (أسلوب Zipkin) شائعة لكنها أقل دعمًا عالميًا.

إنشاء نطاقات يدوية للعمليات التجارية

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

@Service @RequiredArgsConstructor public class OrderService { private final Tracer tracer; private final InventoryClient inventoryClient; public Order placeOrder(OrderRequest request) { // إنشاء نطاق فرعي مسمّى لخطوة الحجز Span reserveSpan = tracer.nextSpan().name("inventory.reserve").start(); try (Tracer.SpanInScope ws = tracer.withSpan(reserveSpan)) { reserveSpan.tag("product.id", String.valueOf(request.getProductId())); reserveSpan.tag("quantity", String.valueOf(request.getQuantity())); boolean reserved = inventoryClient.reserve(request.getProductId(), request.getQuantity()); if (!reserved) { reserveSpan.tag("reservation.result", "insufficient_stock"); throw new InsufficientStockException(request.getProductId()); } reserveSpan.tag("reservation.result", "success"); return saveOrder(request); } catch (Exception e) { reserveSpan.error(e); throw e; } finally { reserveSpan.end(); } } }

تُرفق استدعاءات tag() بيانات وصفية قابلة للبحث بالنطاق. عند فتح هذا التتبع في Zipkin أو Grafana Tempo سترى جدولًا زمنيًا مرئيًا يظهر فيه inventory.reserve متداخلًا تحت نطاق HTTP مُعلَّمًا بمعرّف المنتج والنتيجة.

إضافة ترويسة معرّف ارتباط مخصّص

أحيانًا تحتاج إلى معرّف ارتباط على مستوى الأعمال يُرسله عملاؤك أو شركاؤك — مثلاً بوابة دفع ترسل X-Payment-Reference يجب أن يظهر في كل سجل يتعلق بتلك الدفعة. يمكنك استخراجه وكتابته في MDC إلى جانب معرّف التتبع:

@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class CorrelationIdFilter extends OncePerRequestFilter { private static final String HEADER = "X-Correlation-ID"; private static final String MDC_KEY = "correlationId"; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String correlationId = request.getHeader(HEADER); if (correlationId == null || correlationId.isBlank()) { correlationId = UUID.randomUUID().toString(); } MDC.put(MDC_KEY, correlationId); response.setHeader(HEADER, correlationId); // أعده للعميل ليتتبع الطلب try { chain.doFilter(request, response); } finally { MDC.remove(MDC_KEY); // أساسي: تنظيف لتجنب تلويث مجمع الخيوط } } }
احذر دائمًا من إزالة مدخلات MDC في كتلة finally. تُعيد حاويات Servlet استخدام الخيوط. إذا نسيت استدعاء MDC.remove()، سيتسرب معرّف الارتباط من الطلب A إلى الطلب B على الخيط ذاته — منتجًا سجلات خاطئة بصمت أصعب تصحيحًا من غياب الارتباط كليًا.

أضف %X{correlationId:-} إلى نمط سجلك ليظهر المعرّف على مستوى الأعمال إلى جانب معرّف Micrometer، مانحًا إياك عدستين تكمّل إحداهما الأخرى لفهم كل طلب.

نشر السياق إلى الخيوط غير المتزامنة

يُخزَّن MDC في ThreadLocal. عند إرسال عمل إلى ExecutorService أو استخدام @Async، يبدأ الخيط الجديد بـ MDC فارغ. يحلّ Micrometer Tracing هذا لنطاقاته عبر ContextSnapshot:

@Async public CompletableFuture<Void> sendNotificationAsync(Order order) { // ينشر Micrometer النطاق النشط تلقائيًا إلى توابع @Async // عند استخدام تكامل TaskDecorator الخاص به. // TraceAsyncAspect الذي يُهيَّأ تلقائيًا يتولى الباقي. log.info("Sending notification for order {}", order.getId()); // traceId لا يزال موجودًا return CompletableFuture.completedFuture(null); }

لاستخدام مجمع الخيوط اليدوي، غلّف Runnable بلقطة سياق لنسخ MDC عبر الخيوط:

ContextSnapshot snapshot = ContextSnapshotFactory.builder().build() .captureAll(); executor.execute(snapshot.wrap(() -> { log.info("Async work — traceId is still here"); }));

اعتبار أمني: لا تثق بمعرّفات الارتباط الواردة بشكل أعمى

إذا قبلت قيمة X-Correlation-ID من عميل خارجي وسجّلتها حرفيًا، فأنت تفتح ثغرة حقن في السجل. قيمة خبيثة كـ abc\n14:32:00 WARN [attacker] Fake log line يمكنها تزوير مدخلات في مجرى سجلاتك مما يُفسد سجل المراجعة. عقّم الترويسة دائمًا قبل الكتابة إلى MDC:

// تعقيم آمن — احتفظ فقط بالأحرف الأبجدية الرقمية والشرطات والشرطات السفلية correlationId = correlationId.replaceAll("[^a-zA-Z0-9\\-_]", "").substring(0, Math.min(correlationId.length(), 64));

الخلاصة

يُمثّل التسجيل الموزع ومعرّفات الارتباط أساس مراقبة الخدمات المصغّرة. أضف micrometer-tracing-bridge-brave إلى كل خدمة واسمح لـ Spring Boot بتهيئة معرّفات التتبع والنطاق في MDC تلقائيًا. استخدم الـ bean الخاص بـ WebClient.Builder للحصول على نشر تلقائي للترويسات. أضف نطاقات فرعية صريحة للعمليات التجارية المهمة باستخدام Tracer. أكمل تتبعات Micrometer بمرشّح OncePerRequestFilter مخصّص لمعرّفات الارتباط على مستوى الأعمال — وعقّم دائمًا قيم الترويسات الواردة ونظّف MDC في كتلة finally. بهذه الأجزاء مجتمعة، يفتح معرّف تتبع واحد القصة الكاملة لأي طلب عبر نظامك بأكمله.