Context Propagation และ MDC ใน WebFlux

Sharing is caring!

Spring WebFlux เป็นเฟรมเวิร์กที่ทำงานแบบ Non-blocking บนพื้นฐานของ Project Reactor ซึ่งทำให้สามารถประมวลผลข้อมูลแบบ Reactive ได้อย่างมีประสิทธิภาพ แต่หนึ่งในปัญหาที่พบได้บ่อยคือ MDC (Mapped Diagnostic Context) ไม่สามารถทำงานได้ตรงตามคาดเหมือนใน Spring MVC ที่ใช้ ThreadLocal

🔍 MDC คืออะไร?

  • MDC เป็นวิธีการจัดเก็บข้อมูล context เช่น traceId หรือ userId เพื่อใช้ใน log
  • มักใช้ร่วมกับ SLF4J, Logback, หรือ Log4j2
  • ใน WebFlux การทำงานแบบ asynchronous ทำให้ Thread เปลี่ยนตลอด และ MDC ที่ใช้ ThreadLocal จึง “หลุด”

💡 ทางออก: ใช้ Reactor Context แทน

Reactor Context คือ object ที่สามารถส่งผ่านข้อมูล context ไปตาม Reactive chain ได้โดยไม่ผูกกับ thread

Mono.deferContextual(ctx -> {
    String traceId = ctx.get("traceId");
    log.info("TRACE = {}", traceId);
    return Mono.just("ok");
})
.contextWrite(Context.of("traceId", "abc-123"));

🛠️ ใส่ Context ให้กับ Service

public Mono<String> handleRequest() {
    String traceId = UUID.randomUUID().toString();
    return service.doWork()
        .contextWrite(Context.of("traceId", traceId));
}
public Mono<Void> doWork() {
    return Mono.deferContextual(ctx -> {
        String traceId = ctx.getOrDefault("traceId", "N/A");
        log.info("Doing work for traceId = {}", traceId);
        return Mono.empty();
    });
}

⚠️ ปัญหา MDC แบบเก่า

Mono.just("start")
    .doOnNext(v -> log.info("traceId = {}", MDC.get("traceId")));

ผลลัพธ์: traceId จะเป็น null หรือ “หายไป” เพราะไม่มีการ bind MDC ให้ทุก thread ใหม่

🔁 วิธีเชื่อม MDC กับ Reactor Context

Hooks.onEachOperator("mdc", Operators.lift((sc, sub) ->
    new CoreSubscriber<>() {
        @Override
        public void onSubscribe(Subscription s) {
            sub.onSubscribe(s);
        }

        @Override
        public void onNext(Object t) {
            Context context = sub.currentContext();
            String traceId = context.getOrDefault("traceId", "N/A");
            try (MDC.MDCCloseable c = MDC.putCloseable("traceId", traceId)) {
                sub.onNext(t);
            }
        }

        @Override
        public void onError(Throwable t) {
            sub.onError(t);
        }

        @Override
        public void onComplete() {
            sub.onComplete();
        }

        @Override
        public Context currentContext() {
            return sub.currentContext();
        }
    }
));

📄 ตัวอย่าง Logback Pattern

&lt;pattern&gt;[%X{traceId}] %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n&lt;/pattern&gt;

📦 Spring Cloud Sleuth (Deprecated)

Spring Cloud Sleuth เคยช่วยให้เราสามารถใช้ MDC ได้ง่ายในระบบ distributed แต่ตอนนี้แนะนำให้ใช้ Micrometer Tracing แทนใน Spring Boot 3+

📸 ภาพประกอบ Context Propagation

✅ สรุป

  • WebFlux ต้องใช้ Context ที่ไม่พึ่งพา ThreadLocal
  • ควรใช้ contextWrite() และ deferContextual() แทนการใช้ MDC โดยตรง
  • ถ้าจำเป็นต้องใช้ MDC ให้ผสานกับ context ผ่าน hook
  • แนะนำใช้ Micrometer Tracing + Brave/OTel หากต้องการ distributed trace

🔍 คำค้น SEO

context propagation mdc, spring mdc, context passing webflux, reactor context, reactor deferContextual, spring log traceId, webflux logging, reactive logging traceId, micrometer tracing spring boot

Leave a Reply

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องข้อมูลจำเป็นถูกทำเครื่องหมาย *