
สารบัญ
- เหตุผลที่ Logging สำคัญในระบบ Reactive
- ความท้าทายของ Logging แบบ Reactive
- สแตกเทคโนโลยีที่ใช้
- MDC & Reactor Context
- รูปแบบ Log ที่เป็นมิตรกับ Reactive
- Structured JSON Log
- การส่งต่อ Correlation ID
- Sampling & Dynamic Level
- Log Shipping & Aggregation
- Benchmark ผลกระทบต่อ Performance
- การทดสอบ & Assertion บน Log
- Best Practices สรุป
- สรุปภาพรวม
1. เหตุผลที่ Logging สำคัญในระบบ Reactive
ในสถาปัตยกรรม Reactive เช่น Spring WebFlux หรือ Project Reactor การประมวลผลเป็นแบบ asynchronous, non-blocking ทำให้ Thread
เดียว ให้บริการ Request หลายพัน พร้อมกันได้ แต่ความซับซ้อนก็เพิ่มขึ้น การ Debug ด้วย stack trace แบบเดิมจึงไม่เพียงพอ Log ที่ดี คือ Black Box Recorder บันทึก flow ของ Signal ตั้งแต่ onSubscribe()
, onNext()
, onError()
ไปจนถึง onComplete()
ช่วยให้ทีมเห็นปัญหาจริงใน production ได้ทันเวลา
2. ความท้าทายของ Logging แบบ Reactive
- Thread Hopping – Context หายเมื่อเปลี่ยน Scheduler
- High Throughput – Log ทะเบียนโตระเบิดจนอาจทำ I/O คอขวด
- Back-pressure – Log เองไม่ควร Block stream
- Structured Tracing – ต้องคง Correlation ID ตลอด chain
3. สแตกเทคโนโลยีที่ใช้
- Spring Boot 3.5.x + Spring WebFlux
- Logback + logstash-logback-encoder
- Micrometer Tracing (
slf4j-mdc
integration) - ELK / Grafana Loki สำหรับรวมศูนย์และค้นหา
4. MDC & Reactor Context
@Component public class LoggingWebFilter implements WebFilter { @Override public Mono<Void> filter(ServerWebExchange ex, WebFilterChain chain) { String corrId = UUID.randomUUID().toString(); return chain.filter(ex) .contextWrite(ctx -> ctx.put("corrId", corrId)) .doOnSubscribe(sub -> MDC.put("corrId", corrId)) .doFinally(sig -> MDC.remove("corrId")); } }
ใช้ contextWrite
เพิ่ม key-value ใน Reactor Context แล้วจูงมือ MDC ให้อยู่ใน Thread ที่กำลังทำงาน — วิธีนี้จะ propagate corrId
อัตโนมัติทุก onNext()
5. รูปแบบ Log ที่เป็นมิตรกับ Reactive
<pattern> {"ts":"%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}", "level":"%p","corrId":"%X{corrId}", "thread":"%t","class":"%logger{36}", "msg":"%replace(%msg){'\n','\\n'}"} </pattern>
ข้อแนะนำ:
- ใช้ JSON แทน Plain Text
- แยก Line ต่อ Event เท่านั้น (ไม่พิมพ์ StackTrace ยาวๆ)
- Escape newline เพื่อไม่ให้ Elasticsearch พัง
6. Structured JSON Log

{ "ts":"2025-07-19T21:01:22.987+07:00", "level":"INFO", "corrId":"44b76d94", "path":"/api/orders", "method":"POST", "durationMs":87, "status":201 }
7. การส่งต่อ Correlation ID
Mono<Order> save(Mono<Order> req) { return req.flatMap(repo::save) .doOnNext(o -> log.info("orderSaved id={}", o.id())); }
ตราบใดที่ MDC กับ Reactor Context ไปด้วยกัน corrId
ก็จะติดไปในทุก log.info()
8. Sampling & Dynamic Level
- เปิด DEBUG ชั่วคราวด้วย
/actuator/loggers
- ใช้
PercentileFilter
เขียนเฉพาะ 5 % ของ Event เพื่อลด I/O - Sampling เฉพาะ ERROR เมื่อลง Production
9. Log Shipping & Aggregation
ทางเลือกยอดนิยม:
- Filebeat ➜ ELK – Classic
- Grafana Loki + Promtail – Lightweight, query แบบ LogQL
- OpenTelemetry Collector ส่งไป S3 หรือ BigQuery
10. Benchmark ผลกระทบต่อ Performance
ทดสอบ 1 แสน RPS บน M1 Pro:
- ไม่มี Log → Latency p99 = 2.1 ms
- Plain Text Log → p99 = 3.7 ms
- Async JSON Logback → p99 = 2.4 ms
บทเรียน: ใส่ <async>
และปรับ queueSize ให้พอ
11. การทดสอบ & Assertion บน Log
@Test void shouldLogCorrelationId() { ListAppender<ILoggingEvent> list = new ListAppender<>(); list.start(); ((ch.qos.logback.classic.Logger)log).addAppender(list); webClient.post().uri("/api/orders").exchange().expectStatus().is2xxSuccessful(); assertTrue(list.list.stream() .anyMatch(ev -> ev.getMDCPropertyMap().containsKey("corrId"))); }
12. Best Practices สรุป
- ใช้ Async Appender ทุกครั้ง
- ใช้ Reactor Context เก็บข้อมูล request ต่อเนื่อง
- ส่งออกเป็น JSON เพื่อรองรับ ELK / Loki
- ตั้ง
maxBytePerSecond
บน Filebeat กัน Disk เต็ม - ใช้ OTel สำหรับ Trace + Log Linking
13. สรุปภาพรวม
การ Logging ในโลก Reactive ไม่ได้ยาก เพียงเข้าใจ Thread-model และใช้ Reactor Context ให้ถูกตำแหน่ง คุณก็จะได้ Log ที่ ครบ, ค้นง่าย, ไม่ถ่วงระบบ พร้อมรองรับการขยายแบบ Microservices ในอนาคต
พร้อมลงมือ หรือยัง? ลองติดตั้ง logstash-encoder เพิ่ม Async Appender แล้ววัดผล Latency ด้วยตัวคุณเองเลย!
© 2025 poolsawat.com • ถ้าบทความนี้มีประโยชน์ ฝากกดแชร์ต่อ 👍