
สารบัญ
- เหตุผลที่ 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-mdcintegration) - 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 • ถ้าบทความนี้มีประโยชน์ ฝากกดแชร์ต่อ 👍