Spring Boot Structured Logging ในปี 2026: Log JSON สำหรับ Production ด้วย Logback และ OpenTelemetry

คู่มือฉบับสมบูรณ์สำหรับ structured logging Spring Boot 4.x รองรับ JSON แบบ native, OpenTelemetry starter, MDC tracing และการรวมกับ ELK Stack สำหรับ observability ใน production

Structured logging ใน Spring Boot ด้วย Logback และ JSON

ล็อกแบบข้อความดั้งเดิมจัดการได้ยากอย่างรวดเร็วในระบบ production เมื่อมีอินสแตนซ์หลายร้อยตัวสร้างล็อกหลายพันบรรทัดต่อวินาที การค้นหาข้อผิดพลาดเฉพาะเจาะจงกลายเป็นฝันร้าย ล็อกแบบมีโครงสร้างในรูปแบบ JSON เปลี่ยนสถานการณ์นี้ เพราะทำให้ทุก event สามารถสืบค้นและวิเคราะห์ได้โดยอัตโนมัติ

ประเด็นสำคัญ

Spring Boot 4.x (สร้างบน Spring Framework 7) รองรับ structured logging แบบ JSON ในตัว พร้อมรูปแบบ ECS, Logstash และ GELF Starter ใหม่ spring-boot-starter-opentelemetry ให้ observability แบบรวมศูนย์โดยไม่ต้องพึ่งพาไลบรารีภายนอก

ทำไมต้องใช้ Structured Logging ใน Spring Boot

ข้อจำกัดของล็อกแบบข้อความดั้งเดิม

ล็อกข้อความทั่วไปมีลักษณะดังนี้:

text
2026-08-22 10:15:32.456 INFO  [order-service,abc123] c.e.s.OrderService - Order created for user john@example.com, amount: 150.00€, items: 3

รูปแบบนี้ก่อให้เกิดปัญหาหลายอย่างใน production การดึงข้อมูลเฉพาะเจาะจงต้องใช้ regular expression ที่ซับซ้อนและเปราะบาง การเชื่อมโยงระหว่าง service บังคับให้มีข้อตกลงเข้มงวดที่แต่ละทีมตีความต่างกัน เครื่องมือวิเคราะห์อย่าง Elasticsearch มีความยากลำบากในการ index สตริงที่ไม่มีโครงสร้างเหล่านี้อย่างมีประสิทธิภาพ

ข้อดีของรูปแบบ JSON

event เดียวกันในรูปแบบ JSON ใช้งานได้ทันที:

json
{
  "@timestamp": "2026-08-22T10: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
}

ทุก field สามารถกรองและรวมข้อมูลได้ คำสั่ง Elasticsearch สามารถค้นหาคำสั่งซื้อทั้งหมดที่เกิน 100 € ในช่วงสิบห้านาทีล่าสุดได้ในทันที แดชบอร์ด Kibana แสดงแนวโน้มได้โดยไม่ต้อง parse ด้วยมือ เรื่องนี้เกี่ยวข้องเป็นพิเศษในคำถามสัมภาษณ์ Spring Boot ที่ความเข้าใจเกี่ยวกับ pattern observability ใน production แยกผู้สมัครระดับอาวุโสออกจากคนอื่น

การตั้งค่า Native Structured Logging ของ Spring Boot 4.x

การเปิดใช้งานล็อก JSON แบบมีโครงสร้าง

Spring Boot 4.x สร้างบน Spring Framework 7 เพิ่มการรองรับ structured logging แบบ mature ผ่าน property logging.structured วิธีนี้ไม่ต้องการไลบรารีเพิ่มเติมและรวมเข้ากับ Logback 1.5.38 โดยตรง

yaml
# application.yml
# Native structured logging configuration for Spring Boot 4.x
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 โดยไม่ต้องตั้งค่าเพิ่มเติม

การปรับแต่ง field JSON

เพื่อเพิ่ม field ทางธุรกิจให้กับล็อกทุกบรรทัด Spring Boot อนุญาตให้ตั้งค่า attribute เพิ่มเติม

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}
LoggingConfig.javajava
// 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);
    }
}

field เหล่านี้ปรากฏในทุกบรรทัดล็อก ทำให้กรองตามทีมหรือภูมิภาคบนแดชบอร์ดได้ง่ายขึ้น

OpenTelemetry Starter สำหรับ Observability ครบวงจร

มาตรฐานใหม่ใน Spring Boot 4

Spring Boot 4.0 เปิดตัว spring-boot-starter-opentelemetry แทนที่ setup multi-dependency ที่ซับซ้อนก่อนหน้านี้ dependency เดียวนี้ให้ observability แบบ vendor-neutral รวมถึง traces, metrics และ log correlation

xml
<!-- pom.xml -->
<!-- OpenTelemetry starter for Spring Boot 4.x -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-opentelemetry</artifactId>
</dependency>
<!-- Log correlation for Logback -->
<dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-logback-appender-1.0</artifactId>
    <version>2.21.0-alpha</version>
</dependency>

starter นี้รวม OpenTelemetry API, Micrometer tracing bridge และ OTLP exporters Spring Cloud Sleuth ตอนนี้ถือว่าเป็น legacy โดย OpenTelemetry เป็นมาตรฐานอุตสาหกรรมสำหรับ distributed tracing

การกำหนดค่า OpenTelemetry กับ Structured Logs

yaml
# application.yml
# OpenTelemetry configuration with structured logging
spring:
  application:
    name: order-service
    group: commerce

otel:
  exporter:
    otlp:
      endpoint: http://otel-collector:4317
      protocol: grpc
  resource:
    attributes:
      service.namespace: production
      deployment.environment: prod

logging:
  structured:
    format:
      console: ecs
  pattern:
    level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"

trace และ span ID ถูก inject เข้า MDC โดยอัตโนมัติ เชื่อมโยงล็อกกับ distributed traces ข้าม service สำหรับข้อมูลเพิ่มเติมเกี่ยวกับการตั้งค่า monitoring ใน production ดูคู่มือ Spring Boot Actuator กับ Micrometer และ Prometheus

ประกาศ Deprecation

ZipkinWithOpenTelemetryTracingAutoConfiguration deprecated แล้วและถูกกำหนดให้ลบออกใน Spring Boot 4.2 ให้ย้ายไปใช้ native OTLP exporters เพื่อความเข้ากันได้ในอนาคต

การตั้งค่า Logback แบบดั้งเดิมด้วย JSON Encoder

Logstash Encoder สำหรับการปรับแต่งขั้นสูง

สำหรับความต้องการปรับแต่งขั้นสูงหรือเมื่อย้ายจาก Spring Boot รุ่นเก่า Logstash Logback Encoder 9.0 ยังคงใช้ได้ โปรดทราบว่าเวอร์ชัน 9.0 ต้องการ Jackson 3.0 และ Java 17 ขั้นต่ำ

xml
<!-- pom.xml -->
<!-- Dependency for JSON logging with Logback (Jackson 3 required) -->
<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>9.0</version>
</dependency>

การตั้งค่า Logback แบบเต็มรูปแบบ

ไฟล์ logback-spring.xml ให้ความควบคุมเต็มรูปแบบเหนือรูปแบบของผลลัพธ์

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 เฉพาะใน production เท่านั้น พร้อมรักษาล็อกที่อ่านง่ายไว้ในขั้นตอนพัฒนา

MDC สำหรับ Distributed Tracing

การส่งต่อ Context ของ Trace

MDC (Mapped Diagnostic Context) เพิ่มข้อมูลบริบทให้กับล็อกแต่ละบรรทัด เช่น identifier ของ request หรือ trace

TracingFilter.javajava
// 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 ในโค้ดธุรกิจ

OrderService.javajava
// 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:

json
{
  "@timestamp": "2026-08-22T10: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

ใน production การเขียนล็อกแบบ synchronous กระทบต่อ latency ของ request appender แบบ asynchronous แยกการบันทึกล็อกออกจาก thread หลัก

xml
<!-- 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>
แก้ไขบั๊กร้ายแรงใน Spring Boot 4.1.1

Spring Boot 4.1.1 แก้ไขปัญหาร้ายแรงที่ JSON encode ล้มเหลวอาจทำให้ log event ถัดไปบน thread เดียวกันเสียหาย (#51371) ให้อัพเกรดจาก 4.0.x หรือ 4.1.0 เพื่อหลีกเลี่ยง log corruption ที่ไม่ถูกตรวจพบ

Metrics ของระบบ Logging

การตรวจสอบระบบ logging เองช่วยป้องกันการสูญหายของล็อกแบบเงียบ

LoggingMetrics.javajava
// 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 โดยไม่มีการแปลง

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-08-22T10: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 สำหรับเพิ่มข้อมูล

json
{
  "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;"
      }
    }
  ]
}

แนวปฏิบัติที่ดีที่สุดสำหรับ Production ใน Spring Boot Logging

ข้อมูลที่ควรใส่อย่างเป็นระบบ

ล็อกแต่ละบรรทัดควรมีข้อมูลขั้นต่ำสำหรับ debug และการเชื่อมโยง

StructuredLogger.javajava
// 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);
        }
    }
}
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()
    ));
}

ข้อมูลละเอียดอ่อนที่ต้องไม่ใส่

ล็อกต้องไม่บรรจุข้อมูลส่วนบุคคลหรือข้อมูลละเอียดอ่อน

SensitiveDataFilter.javajava
// Sensitive data masking utility
package com.example.logging.filter;

import java.util.regex.Pattern;

public final class SensitiveDataFilter {

    // 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 SensitiveDataFilter() {}

    // 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]");

        return result;
    }
}

ระดับล็อกที่เหมาะสม

Levelกรณีใช้งานตัวอย่าง
ERRORความล้มเหลวที่ต้องการการแทรกแซงException ที่ไม่สามารถกู้คืนได้, ความล้มเหลวของธุรกรรมสำคัญ, บริการภายนอกไม่พร้อมใช้งาน
WARNสถานการณ์ผิดปกติแต่จัดการได้กำลัง retry, ประสิทธิภาพลดลง, ทรัพยากรใกล้ถึงขีดจำกัด
INFOevent ธุรกิจที่สำคัญเริ่ม/สิ้นสุดธุรกรรม, การเปลี่ยนแปลง state ที่สำคัญ, การกระทำของผู้ใช้หลัก
DEBUGข้อมูลการวินิจฉัยรายละเอียดการทำงาน, ค่าตัวแปรสำคัญ, การตัดสินใจแยกสาขา
TRACEรายละเอียดละเอียดมากเข้า/ออก method, เนื้อหา object ทั้งหมด, loops และ iterations

การทดสอบและตรวจสอบ Structured Logs

Unit Test สำหรับโครงสร้าง JSON

StructuredLoggingTest.javajava
// 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 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;

    @BeforeEach
    void setUp() {
        logger = (Logger) LoggerFactory.getLogger(StructuredLoggingTest.class);
        listAppender = new ListAppender<>();
        listAppender.start();
        logger.addAppender(listAppender);
    }

    @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");
    }
}

สำหรับ pattern integration testing ที่ทำงานได้ดีกับ structured logging ดูคู่มือ Testcontainers Spring Boot integration testing

แหล่งข้อมูล

Checklist Structured Logging สำหรับ Spring Boot 4.x

  • Native structured logging ด้วยรูปแบบ ECS, Logstash หรือ GELF ต้องการเพียงการกำหนดค่า logging.structured.format
  • spring-boot-starter-opentelemetry แทนที่ setup Sleuth legacy ด้วย observability แบบ vendor-neutral
  • MDC ส่งต่อ trace identifiers โดยอัตโนมัติระหว่าง services
  • Appender แบบ async ที่มี neverBlock=true ป้องกันไม่ให้ logging กระทบ request latency
  • Logstash Logback Encoder 9.0 ต้องการ Jackson 3.0 และ Java 17
  • Spring Boot 4.1.1 แก้ไขบั๊กร้ายแรงเกี่ยวกับ JSON encoding ที่ส่งผลกระทบต่อ concurrent threads
  • การ masking ข้อมูลละเอียดอ่อนช่วยให้มั่นใจการปฏิบัติตาม GDPR ในล็อก production
  • Metrics เกี่ยวกับ capacity ของ async queue เปิดใช้งานการแจ้งเตือนก่อนเกิดการสูญเสียล็อก

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

ชาเลนจ์ประจำวัน

คุณหาบั๊กใน Spring Boot เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 22 สิงหาคม 2569

แท็ก

#spring boot logging
#logback json
#structured logs
#elk stack
#observability

แชร์

บทความที่เกี่ยวข้อง