Spring Boot Logging ในปี 2026: ล็อกแบบมีโครงสร้างสำหรับโปรดักชันด้วย Logback และ JSON
คู่มือฉบับสมบูรณ์สำหรับ structured logging ใน Spring Boot การตั้งค่า Logback JSON, MDC สำหรับ tracing แนวปฏิบัติที่ดีที่สุดในโปรดักชัน และการรวมกับ ELK Stack

ล็อกแบบข้อความเดิมจัดการได้ยากอย่างรวดเร็วในระบบโปรดักชัน เมื่อมีอินสแตนซ์หลายร้อยตัวสร้างล็อกหลายพันบรรทัดต่อวินาที การค้นหาข้อผิดพลาดเฉพาะเจาะจงจะกลายเป็นฝันร้าย ล็อกแบบมีโครงสร้างในรูปแบบ JSON เปลี่ยนสถานการณ์นี้โดยสิ้นเชิง เพราะทำให้ทุกเหตุการณ์สามารถสืบค้นและวิเคราะห์ได้โดยอัตโนมัติ
Spring Boot 3.4+ รองรับ structured logging แบบ JSON ในตัว โดยไม่ต้องพึ่งพาไลบรารีภายนอก สำหรับเวอร์ชันก่อนหน้า Logback Logstash Encoder ยังเป็นโซลูชันอ้างอิง
ทำไมต้องใช้ล็อกแบบมีโครงสร้าง
ข้อจำกัดของล็อกแบบข้อความ
ล็อกข้อความทั่วไปมีลักษณะดังนี้
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รูปแบบนี้ก่อให้เกิดปัญหาหลายอย่างในโปรดักชัน การดึงข้อมูลเฉพาะเจาะจงต้องใช้ regular expression ที่ซับซ้อนและเปราะบาง การเชื่อมโยงระหว่างเซอร์วิสบังคับให้มีข้อตกลงเข้มงวดที่แต่ละทีมตีความต่างกัน เครื่องมือวิเคราะห์อย่าง Elasticsearch มีความยากลำบากในการ index สตริงที่ไม่มีโครงสร้างเหล่านี้อย่างมีประสิทธิภาพ
ข้อดีของรูปแบบ JSON
เหตุการณ์เดียวกันในรูปแบบ 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
}ทุกฟิลด์สามารถกรองและรวมข้อมูลได้ คำสั่ง Elasticsearch สามารถค้นหาคำสั่งซื้อทั้งหมดที่เกิน 100 € ในช่วงสิบห้านาทีล่าสุดได้ในทันที แดชบอร์ด Kibana แสดงแนวโน้มได้โดยไม่ต้อง parse ด้วยมือ
การตั้งค่าแบบ native ของ Spring Boot 3.4+
การเปิดใช้งานล็อก JSON แบบมีโครงสร้าง
Spring Boot 3.4 เพิ่มการรองรับ structured logging แบบ native ผ่าน property logging.structured วิธีนี้ไม่ต้องการไลบรารีเพิ่มเติม
# 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รูปแบบ ECS (Elastic Common Schema) รับประกันความเข้ากันได้โดยตรงกับ Elasticsearch และ Kibana โดยไม่ต้องตั้งค่าเพิ่มเติม
การปรับแต่งฟิลด์ JSON
เพื่อเพิ่มฟิลด์ทางธุรกิจให้กับล็อกทุกบรรทัด Spring Boot อนุญาตให้ตั้งค่าแอตทริบิวต์เพิ่มเติม
# 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}// 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<EcsStructuredLogFormatter> 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);
}
}ฟิลด์เหล่านี้ปรากฏในทุกบรรทัดล็อก ทำให้กรองตามทีมหรือภูมิภาคบนแดชบอร์ดได้ง่ายขึ้น
การตั้งค่า Logback แบบดั้งเดิมด้วย JSON encoder
Dependency Logstash Encoder
สำหรับ Spring Boot รุ่นก่อน 3.4 หรือกรณีที่ต้องการปรับแต่งขั้นสูง Logstash Logback Encoder ยังเป็นโซลูชันอ้างอิง
<!-- pom.xml -->
<!-- Dependency for JSON logging with Logback -->
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>การตั้งค่า Logback แบบเต็มรูปแบบ
ไฟล์ logback-spring.xml ให้ความควบคุมเต็มรูปแบบเหนือรูปแบบของผลลัพธ์
<!-- src/main/resources/logback-spring.xml -->
<!-- Logback configuration for structured JSON logs -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Spring Boot properties -->
<springProperty scope="context" name="appName" source="spring.application.name" defaultValue="app"/>
<springProperty scope="context" name="appVersion" source="app.version" defaultValue="1.0.0"/>
<!-- JSON console appender for production -->
<appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<!-- Custom fields added to every log -->
<customFields>{"service":"${appName}","version":"${appVersion}"}</customFields>
<!-- Includes MDC (tracing context) -->
<includeMdcKeyName>traceId</includeMdcKeyName>
<includeMdcKeyName>spanId</includeMdcKeyName>
<includeMdcKeyName>userId</includeMdcKeyName>
<includeMdcKeyName>requestId</includeMdcKeyName>
<!-- ISO8601 timestamp format -->
<timestampPattern>yyyy-MM-dd'T'HH:mm:ss.SSSZ</timestampPattern>
<!-- Complete stack traces -->
<throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
<maxDepthPerThrowable>30</maxDepthPerThrowable>
<maxLength>4096</maxLength>
<shortenedClassNameLength>36</shortenedClassNameLength>
<rootCauseFirst>true</rootCauseFirst>
</throwableConverter>
</encoder>
</appender>
<!-- Rolling JSON file appender -->
<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/${appName}/application.json</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/var/log/${appName}/application.%d{yyyy-MM-dd}.%i.json.gz</fileNamePattern>
<maxHistory>30</maxHistory>
<maxFileSize>100MB</maxFileSize>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"service":"${appName}","version":"${appVersion}"}</customFields>
</encoder>
</appender>
<!-- Text appender for development -->
<appender name="TEXT_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %highlight(%-5level) [%thread] %cyan(%logger{36}) - %msg%n</pattern>
</encoder>
</appender>
<!-- Activation by Spring profile -->
<springProfile name="prod,staging">
<root level="INFO">
<appender-ref ref="JSON_CONSOLE"/>
<appender-ref ref="JSON_FILE"/>
</root>
</springProfile>
<springProfile name="dev,local">
<root level="DEBUG">
<appender-ref ref="TEXT_CONSOLE"/>
</root>
</springProfile>
</configuration>การตั้งค่านี้เปิดใช้งานล็อก JSON เฉพาะในโปรดักชันเท่านั้น พร้อมรักษาล็อกที่อ่านง่ายไว้ในขั้นตอนพัฒนา
การใช้ <springProfile> ช่วยให้สลับระหว่างรูปแบบข้อความและ JSON อัตโนมัติตามสภาพแวดล้อม โดยไม่ต้องแก้ไขการตั้งค่า
MDC สำหรับ distributed tracing
การส่งต่อ context ของ trace
MDC (Mapped Diagnostic Context) เพิ่มข้อมูลบริบทให้กับล็อกแต่ละบรรทัด เช่น identifier ของ request หรือ trace
// 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);
}
}ล็อกทุกบรรทัดที่เกิดขึ้นระหว่างประมวลผล request จะมีตัวระบุเหล่านี้โดยอัตโนมัติ
การใช้ MDC ในโค้ดธุรกิจ
// 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");
}
}
}ล็อก JSON ที่เกิดขึ้นมีข้อมูลทั้งหมดที่จำเป็นสำหรับการ debug
{
"@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"
}พร้อมที่จะพิชิตการสัมภาษณ์ Spring Boot แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
Logging แบบ asynchronous เพื่อประสิทธิภาพ
การตั้งค่า thread pool
ในโปรดักชัน การเขียนล็อกแบบ synchronous กระทบต่อ latency ของ request appender แบบ asynchronous แยกการบันทึกล็อกออกจาก thread หลัก
<!-- logback-spring.xml -->
<!-- High-performance asynchronous appender configuration -->
<appender name="ASYNC_JSON" class="ch.qos.logback.classic.AsyncAppender">
<!-- Pending log buffer size -->
<queueSize>1024</queueSize>
<!-- Never block the calling thread -->
<neverBlock>true</neverBlock>
<!-- Threshold before dropping DEBUG/TRACE logs -->
<discardingThreshold>20</discardingThreshold>
<!-- Include caller information (expensive) -->
<includeCallerData>false</includeCallerData>
<!-- Actual appender for writing -->
<appender-ref ref="JSON_FILE"/>
</appender>
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="ASYNC_JSON"/>
</root>
</springProfile>Metrics ของระบบ logging
การตรวจสอบระบบ logging เองช่วยป้องกันการสูญหายของล็อกแบบเงียบๆ
// 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<Appender<ILoggingEvent>> it = rootLogger.iteratorForAppenders();
while (it.hasNext()) {
Appender<ILoggingEvent> 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);
}
}การแจ้งเตือน Prometheus ที่เงื่อนไข logback.async.queue.remaining < 100 เตือนถึงความเสี่ยงที่ล็อกจะสูญหาย
การรวมกับ ELK Stack
การตั้งค่า Filebeat
Filebeat เก็บไฟล์ JSON และส่งไปยัง Elasticsearch โดยไม่มีการแปลง
# 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 สำหรับเพิ่มข้อมูล
// 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;
"""
}
}
]
}แนวปฏิบัติที่ดีที่สุดในโปรดักชัน
ข้อมูลที่ควรใส่อย่างเป็นระบบ
ล็อกแต่ละบรรทัดควรมีข้อมูลขั้นต่ำสำหรับ debug และการเชื่อมโยง
// 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<String, String> 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<String> messageSupplier, Map<String, String> 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<String, String> context) {
try {
context.forEach(MDC::put);
delegate.error(message, t);
} finally {
context.keySet().forEach(MDC::remove);
}
}
}// 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()
));
}ข้อมูลละเอียดอ่อนที่ต้องไม่ใส่
ล็อกต้องไม่บรรจุข้อมูลส่วนบุคคลหรือข้อมูลละเอียดอ่อน
// 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<ILoggingEvent> {
// 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 ที่อยู่ IP อีเมล และตัวระบุผู้ใช้ต้องมีนโยบายการเก็บรักษาและบางครั้งต้องได้รับความยินยอม
ระดับล็อกที่เหมาะสม
// 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);
}การทดสอบและตรวจสอบล็อก
Unit test สำหรับโครงสร้าง JSON
// 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<ILoggingEvent> 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");
}
}บทสรุป
ล็อกแบบมีโครงสร้างในรูปแบบ JSON เปลี่ยน observability ของแอปพลิเคชัน Spring Boot
✅ ค้นหาได้: ทุกฟิลด์สามารถกรองได้ใน Elasticsearch หรือ CloudWatch
✅ เชื่อมโยงได้: MDC ส่งต่อตัวระบุ trace ระหว่างเซอร์วิส
✅ ประสิทธิภาพสูง: appender แบบ asynchronous แยกการบันทึกล็อกจากการประมวลผล
✅ ปลอดภัย: การปิดบังข้อมูลละเอียดอ่อนช่วยให้สอดคล้องกับ GDPR
✅ บูรณาการ: เข้ากันได้กับ ELK Stack, Datadog, Splunk โดยตรง
✅ ตั้งการแจ้งเตือนได้: ฟิลด์ที่มีโครงสร้างช่วยให้กำหนดกฎการแจ้งเตือนได้แม่นยำ
✅ ดูแลรักษาง่าย: รูปแบบ JSON ขจัด regex parsing ที่เปราะบาง
วิธีนี้เป็นรากฐานของ observability สมัยใหม่ ควบคู่กับ metrics (Micrometer) และ distributed tracing (OpenTelemetry)
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แท็ก
แชร์
บทความที่เกี่ยวข้อง

Spring Boot Actuator: การตรวจสอบบนโปรดักชันด้วย Micrometer และ Prometheus
คู่มือ Spring Boot Actuator ฉบับสมบูรณ์สำหรับการตรวจสอบบนโปรดักชัน ทั้งการตั้งค่า Micrometer, เมตริก Prometheus, endpoint ที่กำหนดเอง และการแจ้งเตือน

Spring Kafka: สถาปัตยกรรม event-driven พร้อม consumer ที่ทนทาน
คู่มือ Spring Kafka แบบครบถ้วนสำหรับสถาปัตยกรรม event-driven การตั้งค่า consumer ที่ทนทาน นโยบาย retry dead letter queue และรูปแบบโปรดักชันสำหรับแอปพลิเคชันแบบกระจาย

สัมภาษณ์ Spring GraphQL: Resolver, DataLoader และวิธีแก้ปัญหา N+1
เตรียมตัวสำหรับการสัมภาษณ์ Spring GraphQL ด้วยคู่มือที่ครบถ้วนนี้ Resolver, DataLoader, การจัดการปัญหา N+1, mutation และแนวปฏิบัติที่ดีที่สุดสำหรับคำถามทางเทคนิค