# Logging Spring Boot năm 2026: log có cấu trúc trên production với Logback và JSON > Hướng dẫn đầy đủ về logging có cấu trúc trong Spring Boot. Cấu hình Logback JSON, MDC cho tracing, các best practice production và tích hợp ELK Stack. - Published: 2026-03-27 - Updated: 2026-05-04 - Author: SharpSkill - Tags: spring boot logging, logback json, structured logs, elk stack, observability - Reading time: 14 min --- Log dạng văn bản truyền thống nhanh chóng trở nên khó kiểm soát trên production. Với hàng trăm instance sinh ra hàng nghìn dòng mỗi giây, việc tìm một lỗi cụ thể trở thành ác mộng. Log có cấu trúc dạng JSON thay đổi tình hình này hoàn toàn vì mỗi sự kiện đều có thể truy vấn và phân tích tự động. > **Điểm chính** > > Spring Boot 3.4+ hỗ trợ native cho structured logging JSON mà không cần thư viện ngoài. Với phiên bản cũ hơn, Logback Logstash Encoder vẫn là giải pháp tham chiếu. ## Vì sao nên áp dụng log có cấu trúc ### Hạn chế của log văn bản truyền thống Một log văn bản điển hình trông như sau: ``` 2026-03-27 10:15:32.456 INFO [order-service,abc123] c.e.s.OrderService - Order created for user john@example.com, amount: 150.00€, items: 3 ``` Định dạng này gây ra nhiều vấn đề trên production. Việc trích xuất thông tin cụ thể đòi hỏi các regex phức tạp và mong manh. Việc tương quan giữa các service buộc phải có quy ước nghiêm ngặt mà mỗi đội lại diễn giải theo cách khác nhau. Các công cụ phân tích như Elasticsearch khó lòng index hiệu quả các chuỗi không có cấu trúc này. ### Lợi ích của định dạng JSON Cùng sự kiện đó ở dạng JSON có thể khai thác ngay lập tức: ```json { "@timestamp": "2026-03-27T10:15:32.456Z", "level": "INFO", "logger": "com.example.service.OrderService", "message": "Order created", "service": "order-service", "traceId": "abc123", "userId": "john@example.com", "orderId": "ORD-789456", "amount": 150.00, "currency": "EUR", "itemCount": 3 } ``` Mỗi trường đều có thể lọc và tổng hợp. Một truy vấn Elasticsearch có thể tìm ngay tất cả đơn hàng trên 100 € trong mười lăm phút gần nhất. Các dashboard Kibana trực quan hóa xu hướng mà không cần parsing thủ công. ## Cấu hình native của Spring Boot 3.4+ ### Bật log JSON có cấu trúc Spring Boot 3.4 giới thiệu hỗ trợ native cho structured logging thông qua thuộc tính `logging.structured`. Cách tiếp cận này không cần thêm phụ thuộc nào. ```yaml # application.yml # Native structured logging configuration for Spring Boot 3.4+ logging: structured: # Output format: ecs (Elastic), logstash, gelf format: console: ecs file: ecs file: name: /var/log/app/application.log level: root: INFO com.example: DEBUG ``` Định dạng ECS (Elastic Common Schema) đảm bảo tương thích trực tiếp với Elasticsearch và Kibana mà không cần cấu hình bổ sung. ### Tùy biến các trường JSON Để thêm các trường nghiệp vụ vào mỗi log, Spring Boot cho phép cấu hình các thuộc tính bổ sung. ```yaml # application.yml # Custom fields in structured logs logging: structured: format: console: ecs ecs: # Service information added to every log service: name: ${spring.application.name} version: ${app.version:1.0.0} environment: ${spring.profiles.active:default} node-name: ${HOSTNAME:unknown} ``` ```java // LoggingConfig.java // Programmatic configuration for additional fields package com.example.logging.config; import org.springframework.boot.logging.structured.StructuredLogFormatterCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class LoggingConfig { @Bean StructuredLogFormatterCustomizer ecsCustomizer() { return formatter -> formatter // Adds static fields to all logs .addStaticField("team", "backend") .addStaticField("region", System.getenv("AWS_REGION")) // Customizes exception formatting .setIncludeStacktrace(true) .setStacktraceMaxLength(5000); } } ``` Các trường này xuất hiện trong mọi dòng log, giúp lọc theo đội nhóm hoặc khu vực trên dashboard. ## Cấu hình Logback cổ điển với encoder JSON ### Phụ thuộc Logstash Encoder Với các phiên bản Spring Boot trước 3.4 hoặc nhu cầu tùy biến nâng cao, Logstash Logback Encoder vẫn là giải pháp tham chiếu. ```xml net.logstash.logback logstash-logback-encoder 7.4 ``` ### Cấu hình Logback đầy đủ File `logback-spring.xml` cung cấp toàn quyền kiểm soát định dạng đầu ra. ```xml {"service":"${appName}","version":"${appVersion}"} traceId spanId userId requestId yyyy-MM-dd'T'HH:mm:ss.SSSZ 30 4096 36 true /var/log/${appName}/application.json /var/log/${appName}/application.%d{yyyy-MM-dd}.%i.json.gz 30 100MB 3GB {"service":"${appName}","version":"${appVersion}"} %d{HH:mm:ss.SSS} %highlight(%-5level) [%thread] %cyan(%logger{36}) - %msg%n ``` Cấu hình này chỉ bật log JSON ở môi trường production, đồng thời giữ log dễ đọc ở môi trường phát triển. > **Spring profile** > > Việc dùng `` cho phép tự động chuyển đổi giữa định dạng văn bản và JSON theo môi trường mà không phải sửa cấu hình. ## MDC cho distributed tracing ### Lan truyền context trace MDC (Mapped Diagnostic Context) bổ sung vào mỗi log những thông tin context như request id hay trace id. ```java // TracingFilter.java // Filter for automatic trace context injection package com.example.logging.filter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.MDC; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.UUID; @Component @Order(Ordered.HIGHEST_PRECEDENCE) public class TracingFilter extends OncePerRequestFilter { // Standard MDC keys for tracing private static final String TRACE_ID_KEY = "traceId"; private static final String SPAN_ID_KEY = "spanId"; private static final String REQUEST_ID_KEY = "requestId"; private static final String USER_ID_KEY = "userId"; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { // Retrieve or generate trace identifiers String traceId = extractOrGenerate(request, "X-Trace-Id", TRACE_ID_KEY); String spanId = generateSpanId(); String requestId = extractOrGenerate(request, "X-Request-Id", REQUEST_ID_KEY); String userId = request.getHeader("X-User-Id"); // Inject into MDC to appear in all logs MDC.put(TRACE_ID_KEY, traceId); MDC.put(SPAN_ID_KEY, spanId); MDC.put(REQUEST_ID_KEY, requestId); if (userId != null) { MDC.put(USER_ID_KEY, userId); } // Propagate to responses for inter-service chaining response.setHeader("X-Trace-Id", traceId); response.setHeader("X-Request-Id", requestId); filterChain.doFilter(request, response); } finally { // Clean MDC after each request MDC.clear(); } } private String extractOrGenerate(HttpServletRequest request, String header, String key) { String value = request.getHeader(header); return value != null ? value : UUID.randomUUID().toString().replace("-", "").substring(0, 16); } private String generateSpanId() { return UUID.randomUUID().toString().replace("-", "").substring(0, 8); } } ``` Mọi log phát ra trong quá trình xử lý request sẽ tự động chứa các id này. ### Sử dụng MDC trong code nghiệp vụ ```java // OrderService.java // Business service with enriched contextual logging package com.example.service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import org.springframework.stereotype.Service; @Service public class OrderService { private static final Logger log = LoggerFactory.getLogger(OrderService.class); public Order createOrder(CreateOrderRequest request) { // Add business information to MDC context MDC.put("orderId", request.getOrderId()); MDC.put("customerId", request.getCustomerId()); try { log.info("Creating order with {} items", request.getItems().size()); // Business logic... Order order = processOrder(request); log.info("Order created successfully, total: {} {}", order.getTotal(), order.getCurrency()); return order; } catch (Exception e) { // Exception appears with full MDC context log.error("Failed to create order", e); throw e; } finally { // Clean business keys added MDC.remove("orderId"); MDC.remove("customerId"); } } } ``` Log JSON sinh ra chứa toàn bộ thông tin cần thiết cho việc debug: ```json { "@timestamp": "2026-03-27T10:15:32.456Z", "level": "INFO", "logger": "com.example.service.OrderService", "message": "Order created successfully, total: 150.00 EUR", "traceId": "a1b2c3d4e5f67890", "spanId": "12345678", "requestId": "req-abc-123", "userId": "user-456", "orderId": "ORD-789", "customerId": "CUST-321" } ``` ## Logging bất đồng bộ cho hiệu năng ### Cấu hình thread pool Ở production, các thao tác ghi log đồng bộ ảnh hưởng đến độ trễ của request. Appender bất đồng bộ tách phần logging ra khỏi luồng chính. ```xml 1024 true 20 false ``` ### Metrics của hệ thống logging Giám sát chính hệ thống logging giúp tránh việc mất log một cách âm thầm. ```java // LoggingMetrics.java // Exposing Logback metrics via Micrometer package com.example.logging.metrics; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.Appender; import ch.qos.logback.classic.AsyncAppender; import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.MeterRegistry; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import jakarta.annotation.PostConstruct; import java.util.Iterator; @Component public class LoggingMetrics { private final MeterRegistry registry; public LoggingMetrics(MeterRegistry registry) { this.registry = registry; } @PostConstruct void registerMetrics() { LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); Logger rootLogger = context.getLogger(Logger.ROOT_LOGGER_NAME); // Iterate through appenders to find AsyncAppenders Iterator> it = rootLogger.iteratorForAppenders(); while (it.hasNext()) { Appender appender = it.next(); if (appender instanceof AsyncAppender asyncAppender) { registerAsyncMetrics(asyncAppender); } } } private void registerAsyncMetrics(AsyncAppender appender) { String appenderName = appender.getName(); // Current queue size Gauge.builder("logback.async.queue.size", appender, AsyncAppender::getQueueSize) .tag("appender", appenderName) .description("Current async appender queue size") .register(registry); // Remaining capacity Gauge.builder("logback.async.queue.remaining", appender, AsyncAppender::getRemainingCapacity) .tag("appender", appenderName) .description("Remaining capacity in async queue") .register(registry); // Number of dropped logs Gauge.builder("logback.async.discarded", appender, AsyncAppender::getNumberOfElementsInQueue) .tag("appender", appenderName) .description("Number of discarded log events") .register(registry); } } ``` Cảnh báo Prometheus với điều kiện `logback.async.queue.remaining < 100` cảnh báo nguy cơ mất log. ## Tích hợp ELK Stack ### Cấu hình Filebeat Filebeat thu thập các file JSON và gửi về Elasticsearch mà không cần biến đổi. ```yaml # filebeat.yml # Filebeat configuration for Spring Boot JSON logs filebeat.inputs: - type: log enabled: true paths: - /var/log/*/application.json # Automatic JSON parsing json: keys_under_root: true overwrite_keys: true add_error_key: true message_key: message processors: # Add Kubernetes metadata if available - add_kubernetes_metadata: host: ${NODE_NAME} matchers: - logs_path: logs_path: "/var/log/containers/" # Parse timestamp - timestamp: field: "@timestamp" layouts: - '2006-01-02T15:04:05.000Z' - '2006-01-02T15:04:05.000-07:00' test: - '2026-03-27T10:15:32.456Z' output.elasticsearch: hosts: ["elasticsearch:9200"] index: "logs-%{[service]}-%{+yyyy.MM.dd}" pipeline: "spring-boot-logs" setup.template: name: "logs" pattern: "logs-*" ``` ### Pipeline Elasticsearch để làm giàu dữ liệu ```json // PUT _ingest/pipeline/spring-boot-logs { "description": "Spring Boot logs enrichment", "processors": [ { "geoip": { "field": "client.ip", "target_field": "client.geo", "ignore_missing": true } }, { "user_agent": { "field": "user_agent.original", "target_field": "user_agent", "ignore_missing": true } }, { "set": { "field": "event.ingested", "value": "{{_ingest.timestamp}}" } }, { "script": { "description": "Classify log level severity", "source": """ def level = ctx.level; if (level == 'ERROR') ctx.severity = 4; else if (level == 'WARN') ctx.severity = 3; else if (level == 'INFO') ctx.severity = 2; else ctx.severity = 1; """ } } ] } ``` ## Best practice trên production ### Thông tin cần đưa vào một cách có hệ thống Mỗi log nên chứa lượng thông tin tối thiểu phục vụ debug và đối chiếu. ```java // StructuredLogger.java // Helper for consistent structured logs package com.example.logging; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import java.util.Map; import java.util.function.Supplier; public final class StructuredLogger { private final Logger delegate; private StructuredLogger(Class clazz) { this.delegate = LoggerFactory.getLogger(clazz); } public static StructuredLogger getLogger(Class clazz) { return new StructuredLogger(clazz); } // Log with temporary business context public void info(String message, Map context) { try { context.forEach(MDC::put); delegate.info(message); } finally { context.keySet().forEach(MDC::remove); } } // Log with supplier for lazy evaluation public void debug(Supplier messageSupplier, Map context) { if (delegate.isDebugEnabled()) { try { context.forEach(MDC::put); delegate.debug(messageSupplier.get()); } finally { context.keySet().forEach(MDC::remove); } } } // Error log with full context public void error(String message, Throwable t, Map context) { try { context.forEach(MDC::put); delegate.error(message, t); } finally { context.keySet().forEach(MDC::remove); } } } ``` ```java // Usage in business code private static final StructuredLogger log = StructuredLogger.getLogger(PaymentService.class); public void processPayment(Payment payment) { log.info("Processing payment", Map.of( "paymentId", payment.getId(), "amount", String.valueOf(payment.getAmount()), "currency", payment.getCurrency(), "method", payment.getMethod().name() )); } ``` ### Thông tin nhạy cảm cần loại trừ Log không bao giờ được chứa dữ liệu cá nhân hoặc nhạy cảm. ```java // SensitiveDataFilter.java // Sensitive data masking filter package com.example.logging.filter; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.filter.Filter; import ch.qos.logback.core.spi.FilterReply; import java.util.regex.Pattern; public class SensitiveDataFilter extends Filter { // Sensitive data patterns to mask private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"); private static final Pattern CREDIT_CARD_PATTERN = Pattern.compile("\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b"); private static final Pattern PASSWORD_PATTERN = Pattern.compile("(?i)(password|pwd|secret|token)[\"']?\\s*[:=]\\s*[\"']?[^\\s,}\"']+"); private static final Pattern PHONE_PATTERN = Pattern.compile("\\+?\\d{1,3}[- ]?\\d{6,14}"); @Override public FilterReply decide(ILoggingEvent event) { // Accept all logs but modify the message // Note: for real masking, use a custom converter return FilterReply.NEUTRAL; } // Utility method to mask data public static String maskSensitiveData(String input) { if (input == null) return null; String result = input; result = EMAIL_PATTERN.matcher(result).replaceAll("[EMAIL_MASKED]"); result = CREDIT_CARD_PATTERN.matcher(result).replaceAll("[CARD_MASKED]"); result = PASSWORD_PATTERN.matcher(result).replaceAll("$1=[REDACTED]"); result = PHONE_PATTERN.matcher(result).replaceAll("[PHONE_MASKED]"); return result; } } ``` > **GDPR và tuân thủ** > > Log chứa dữ liệu cá nhân thuộc phạm vi GDPR. Địa chỉ IP, email và id người dùng đòi hỏi chính sách lưu trữ và đôi khi cần sự đồng ý. ### Mức log phù hợp ```java // LogLevelGuidelines.java // Appropriate log level guidelines package com.example.logging; public class LogLevelGuidelines { // ERROR: Failure requiring intervention // - Unrecoverable exceptions // - Critical transaction failures // - External service unavailability log.error("Payment gateway unreachable after 3 retries", exception); // WARN: Abnormal but handled situation // - Retry in progress // - Performance degradation // - Resources near limits log.warn("Database connection pool at 85% capacity"); // INFO: Significant business events // - Transaction start/end // - Important state changes // - Key user actions log.info("Order {} shipped to customer {}", orderId, customerId); // DEBUG: Diagnostic information // - Execution details // - Important variable values // - Branching decisions log.debug("Cache miss for key {}, fetching from database", cacheKey); // TRACE: Very fine details // - Method entry/exit // - Complete object contents // - Loops and iterations log.trace("Processing item {} of {}", index, total); } ``` ## Kiểm thử và xác thực log ### Unit test cho cấu trúc JSON ```java // StructuredLoggingTest.java // Structured log validation tests package com.example.logging; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.read.ListAppender; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import static org.assertj.core.api.Assertions.assertThat; class StructuredLoggingTest { private ListAppender listAppender; private Logger logger; private ObjectMapper objectMapper; @BeforeEach void setUp() { logger = (Logger) LoggerFactory.getLogger(StructuredLoggingTest.class); listAppender = new ListAppender<>(); listAppender.start(); logger.addAppender(listAppender); objectMapper = new ObjectMapper(); } @Test void shouldIncludeMdcFieldsInLog() { // Given MDC.put("traceId", "test-trace-123"); MDC.put("userId", "user-456"); // When logger.info("Test message with MDC context"); // Then ILoggingEvent event = listAppender.list.get(0); assertThat(event.getMDCPropertyMap()) .containsEntry("traceId", "test-trace-123") .containsEntry("userId", "user-456"); MDC.clear(); } @Test void shouldLogExceptionWithStackTrace() { // Given Exception testException = new RuntimeException("Test error"); // When logger.error("Operation failed", testException); // Then ILoggingEvent event = listAppender.list.get(0); assertThat(event.getThrowableProxy()).isNotNull(); assertThat(event.getThrowableProxy().getMessage()).isEqualTo("Test error"); } } ``` ## Kết luận Log có cấu trúc dạng JSON thay đổi cách quan sát hệ thống của ứng dụng Spring Boot: ✅ **Có thể truy vấn**: mọi trường có thể lọc trong Elasticsearch hoặc CloudWatch ✅ **Có thể tương quan**: MDC truyền các id trace giữa các service ✅ **Hiệu năng cao**: appender bất đồng bộ tách logging khỏi luồng xử lý ✅ **An toàn**: che giấu dữ liệu nhạy cảm bảo đảm tuân thủ GDPR ✅ **Tích hợp**: tương thích native với ELK Stack, Datadog, Splunk ✅ **Có thể alert**: trường có cấu trúc cho phép xây dựng quy tắc cảnh báo chính xác ✅ **Dễ bảo trì**: định dạng JSON loại bỏ các regex parse mong manh Cách tiếp cận này tạo nên nền tảng cho observability hiện đại bên cạnh metrics (Micrometer) và distributed tracing (OpenTelemetry). --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/spring-boot/spring-boot-structured-logging-logback-json