การ Logging ที่เหมาะสมในระบบ Reactive

Sharing is caring!

สารบัญ

  1. เหตุผลที่ Logging สำคัญในระบบ Reactive
  2. ความท้าทายของ Logging แบบ Reactive
  3. สแตกเทคโนโลยีที่ใช้
  4. MDC & Reactor Context
  5. รูปแบบ Log ที่เป็นมิตรกับ Reactive
  6. Structured JSON Log
  7. การส่งต่อ Correlation ID
  8. Sampling & Dynamic Level
  9. Log Shipping & Aggregation
  10. Benchmark ผลกระทบต่อ Performance
  11. การทดสอบ & Assertion บน Log
  12. Best Practices สรุป
  13. สรุปภาพรวม

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

โฟลว์ Event ➜ JSON ➜ Logstash ➜ Elasticsearch ➜ Kibana
{
  "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

ทางเลือกยอดนิยม:

  1. Filebeat ➜ ELK – Classic
  2. Grafana Loki + Promtail – Lightweight, query แบบ LogQL
  3. 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 • ถ้าบทความนี้มีประโยชน์ ฝากกดแชร์ต่อ 👍

Leave a Reply

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