Structured Logging: JSON Log Cho ELK và Kỹ Thuật MDC
Khám phá structured logging JSON theo chuẩn ECS trong Spring Boot 3.4+, kỹ thuật MDC correlation ID và dynamic log level qua Actuator.
TL;DR: Ghi log dạng văn bản thuần túy buộc hệ thống giám sát phải sử dụng các bộ lọc Grok regex tốn kém tài nguyên và dễ gãy vỡ. Spring Boot 3.4+ hỗ trợ Structured Logging trực tiếp sang định dạng JSON theo chuẩn Elastic Common Schema (ECS) chỉ với một dòng cấu hình. Kỹ thuật MDC (Mapped Diagnostic Context) gắn mã requestId xuyên suốt vòng đời xử lý của thread, kết hợp cùng endpoint /actuator/loggers giúp thay đổi mức log tức thì trên production mà không cần khởi động lại ứng dụng.
Tại sao JSON log vượt trội hơn log văn bản thuần túy?
Log văn bản truyền thống xuất ra chuỗi ký tự tự do. Để hệ thống Elasticsearch có thể tìm kiếm theo từng trường, bộ chuyển đổi Logstash phải chạy các mẫu biểu thức chính quy (Grok pattern):
%{TIMESTAMP_ISO8601:timestamp} %{LOGLEVEL:level} %{NUMBER:pid} --- \[%{DATA:thread}\] %{DATA:logger} : %{GREEDYDATA:message}
Quy trình bóc tách này gây ra ba vấn đề nghiêm trọng trong môi trường tải lớn:
- Rất dễ gãy vỡ: Chỉ cần thay đổi nhẹ mẫu định dạng ngày tháng hoặc bổ sung mã trace, biểu thức regex sẽ không khớp và toàn bộ log bị bỏ rơi.
- Tiêu tốn CPU: Với hệ thống xử lý hàng chục nghìn request mỗi giây, Logstash phải thực thi hàng chục nghìn phép tính regex liên tục.
- Sai lệch kiểu dữ liệu: Mọi thông tin sau bóc tách đều mặc định là kiểu chuỗi, gây khó khăn khi cần thực hiện truy vấn so sánh số học.

Định dạng JSON giải quyết triệt để các hạn chế trên nhờ cấu trúc trường tường minh:
{
"@timestamp": "2026-04-15T10:00:00.123Z",
"log.level": "INFO",
"process.pid": 12345,
"process.thread.name": "http-nio-8080-exec-3",
"log.logger": "com.olhub.OrderService",
"message": "Order placed: id=42, total=99.99",
"service.name": "order-service"
}
Filebeat có thể đẩy thẳng bản ghi JSON vào Elasticsearch mà không cần thông qua bước phân tích chuỗi trung gian.
Cách kích hoạt JSON log chuẩn ECS trong Spring Boot 3.4+?
Từ phiên bản Spring Boot 3.4, structured logging đã được tích hợp sẵn vào core framework. Bạn chỉ cần khai báo một dòng trong file cấu hình profile production:
# application-prod.yml: Enable ECS structured logging for production
logging:
structured:
format:
console: ecs
json:
add:
environment: <code>{'${ENV:prod}'}</code>
service.version: <code>{'${BUILD_VERSION:1.0.0}'}</code>
Khung hỗ trợ 3 chuẩn định dạng phổ biến:
ecs: Chuẩn Elastic Common Schema, tối ưu nhất cho hệ sinh thái ELK Stack.gelf: Chuẩn Graylog Extended Log Format cho hệ thống Graylog.logstash: Định dạng JSON truyền thống của Logstash.
Trong môi trường phát triển cục bộ (application.yml), hãy giữ nguyên định dạng văn bản mặc định để lập trình viên dễ đọc trên terminal console.
Cơ chế phân loại ngữ cảnh MDC per-request hoạt động ra sao?
Trong kiến trúc máy chủ web, một thread từ thread pool sẽ được tái sử dụng để phục vụ nhiều request nối tiếp nhau. Nếu không có định danh phiên, các dòng log sẽ bị trộn lẫn và không thể truy vết lỗi.
MDC (Mapped Diagnostic Context) lưu trữ một bản đồ key-value gắn liền với luồng thực thi thông qua ThreadLocal.

Chúng ta cài đặt một Servlet Filter ở cổng vào để khởi tạo mã truy vết:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorrelationIdFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpReq = (HttpServletRequest) req;
String requestId = httpReq.getHeader("X-Request-Id");
if (requestId == null) {
requestId = UUID.randomUUID().toString();
}
MDC.put("requestId", requestId);
try {
chain.doFilter(req, res);
} finally {
// CRITICAL: Always clean up in finally block to prevent thread pool leakage
MDC.clear();
}
}
}
Khi kết hợp cùng Spring Boot 3.4, toàn bộ cặp khóa trong MDC sẽ tự động được đóng gói vào đối tượng mdc trong chuỗi JSON xuất ra:
{
"@timestamp": "2026-04-15T10:00:00.123Z",
"log.level": "INFO",
"message": "Order placed successfully",
"mdc": {
"requestId": "c4b2-9a81-42ef"
}
}
Làm thế nào để thay đổi log level động qua Spring Boot Actuator?
Khi xảy ra sự cố đột xuất trên production, việc cấu hình lại file YAML rồi triển khai lại container sẽ làm gián đoạn dịch vụ và xóa sạch trạng thái bộ nhớ đệm.
Spring Boot Actuator cung cấp endpoint /actuator/loggers để điều chỉnh mức log ngay tại runtime:
# application.yml: Expose loggers endpoint on dedicated internal management port
management:
server:
port: 9090
endpoints:
web:
exposure:
include: health, loggers
Kiểm tra mức log hiện tại qua lệnh GET:
curl http://localhost:9090/actuator/loggers/com.olhub.PaymentService
Bật mức DEBUG tức thì mà không cần restart máy chủ:
curl -X POST http://localhost:9090/actuator/loggers/com.olhub.PaymentService \
-H "Content-Type: application/json" \
-d '{"configuredLevel": "DEBUG"}'
Sau khi hoàn tất việc điều tra lỗi, gửi giá trị null để trả logger về mức cấu hình mặc định ban đầu:
curl -X POST http://localhost:9090/actuator/loggers/com.olhub.PaymentService \
-H "Content-Type: application/json" \
-d '{"configuredLevel": null}'
Khi nào nên cân nhắc chuyển từ Logback sang Log4j2?
Logback là lựa chọn hoàn hảo cho hầu hết ứng dụng Spring Boot. Tuy nhiên, nếu hệ thống có thông lượng cực lớn (vượt quá 50.000 đến 100.000 bản ghi log mỗi giây), việc chuyển sang Log4j2 sẽ mang lại lợi ích hiệu năng nhờ kiến trúc Asynchronous Loggers dựa trên LMAX Disruptor:
<!-- Switch from Logback to Log4j2 in pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
Vì toàn bộ mã nguồn sử dụng SLF4J, việc hoán đổi này hoàn toàn không làm thay đổi các lớp nghiệp vụ.
Bản chất hoạt động của MDC và biến ThreadLocal là gì?
MDC được xây dựng trên cấu trúc ThreadLocal<Map<String, String>>. Mỗi luồng trong máy ảo Java sở hữu một bản đồ độc lập và không chia sẻ dữ liệu với các luồng khác.
Khi bạn chuyển việc xử lý sang luồng bất đồng bộ (sử dụng @Async hoặc CompletableFuture), luồng con mới được sinh ra sẽ có bản đồ MDC rỗng. Bạn phải sao chép ngữ cảnh sang luồng con một cách tường minh:
public void executeAsyncTask(Order order) {
Map<String, String> contextMap = MDC.getCopyOfContextMap();
taskExecutor.submit(() -> {
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
try {
processAsync(order);
} finally {
MDC.clear();
}
});
}
Những cạm bẫy nào cần tránh khi làm việc với MDC và JSON log?
Dưới đây là các sai lầm cần lưu ý khi thiết kế hệ thống logging:
- Quên đặt lệnh
MDC.clear()trong khốifinally: Nếu luồng ném ra ngoại lệ trước khi dọn dẹp, mã định danh cũ sẽ bị giữ lại và gắn sai vào các request tiếp theo được luồng đó phục vụ. - Bật JSON log trên môi trường dev: Khiến lập trình viên gặp khó khăn khi theo dõi log cục bộ do cấu trúc JSON chiếm nhiều diện tích màn hình.
- Để lộ cổng Actuator loggers ra mạng công cộng: Cho phép kẻ xấu bật mức log debug trên diện rộng gây quá tải đĩa cứng và làm lộ các dữ liệu nhạy cảm.
- Quên sao chép ngữ cảnh MDC sang luồng bất đồng bộ: Dẫn đến hiện tượng mất dấu vết truy vết khi các tác vụ nền được kích hoạt.
Tóm tắt
- JSON log giúp tối ưu hóa hiệu năng thu thập nhật ký, loại bỏ hoàn toàn gánh nặng xử lý Grok regex của Logstash.
- Spring Boot 3.4 cung cấp tính năng cấu hình structured logging chuẩn ECS out-of-the-box qua thuộc tính
logging.structured.format.console=ecs. - MDC quản lý ngữ cảnh truy vết gắn liền với luồng qua
ThreadLocal, phục vụ bài toán truy vết lỗi phân tán. - Lệnh
MDC.clear()bắt buộc phải nằm trong khốifinallyđể ngăn ngừa rò rỉ ngữ cảnh giữa các request. - Endpoint Actuator
/actuator/loggershỗ trợ điều chỉnh mức log trực tiếp tại runtime mà không cần khởi động lại dịch vụ.
Tự kiểm tra
- Q1Tại sao việc dùng định dạng JSON log (Structured Logging) lại giúp hệ thống giám sát ELK hoạt động hiệu quả hơn log văn bản?
- Q2Trong Spring Boot 3.4+, thuộc tính nào cho phép bật structured logging định dạng Elastic Common Schema ra console?
- Q3Vì sao lệnh MDC.clear() bắt buộc phải được đặt bên trong khối finally của Servlet Filter?
- Q4Phương thức nào cho phép bật mức DEBUG cho một package cụ thể tại runtime qua Spring Boot Actuator mà không cần restart?
- Q5Điều gì sẽ xảy ra với ngữ cảnh MDC khi tác vụ được chuyển sang xử lý trong một luồng mới bằng annotation @Async?
Bài tiếp theo: Tổng kết module
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Logging Cơ Bản: SLF4J Facade, Logback và Phân Cấp Level
Bài tiếpTổng Kết Module 06: Config, Profiles Và Logging Spring
Lộ trình Junior Java Backend Developer
Chặng 14/21 · Spring Boot & autoconfiguration — bài 15/16 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này