Observability ใน Spring Boot 2026: OpenTelemetry, Distributed Tracing และคำถามสัมภาษณ์

เรียนรู้ observability ใน Spring Boot ด้วย OpenTelemetry และ Micrometer Tracing คู่มือการตั้งค่า distributed tracing, Observation API, การส่งออก OTLP และการเตรียมตัวสัมภาษณ์งานเทคนิค

Observability ใน Spring Boot พร้อม distributed tracing ของ OpenTelemetry

Observability ใน Spring Boot รวม logging, metrics และ distributed tracing เข้าด้วยกันเป็นระบบที่เผยให้เห็นว่า request ไหลผ่าน microservices อย่างไร ตั้งแต่ Spring Boot 3 เป็นต้นมา framework นี้ได้นำ Micrometer Tracing มาใช้ (แทนที่ Spring Cloud Sleuth) และเปิดตัว Observation API ที่ให้จุด instrumentation เดียวสำหรับสร้างทั้ง metrics และ trace

รูปแบบ Observation API

Spring Boot แนะนำให้ใช้ Observation.observe() แทนการเรียก OpenTelemetry โดยตรง การเรียก instrumentation ครั้งเดียวจะสร้าง metrics ผ่าน Micrometer และ trace ผ่าน bridge ของ OpenTelemetry ช่วยลดการเขียนโค้ดซ้ำซ้อนและรับประกันการตั้งชื่อ tag ที่สอดคล้องกันในทุกสัญญาณ

Observation API เชื่อมต่อ Micrometer และ OpenTelemetry อย่างไร

Observation API ทำหน้าที่เป็น facade สำหรับทั้ง metrics และ tracing เมื่อโค้ดเรียก Observation.createNotStarted() Spring Boot จะส่ง observation ไปยัง handler ที่ลงทะเบียนไว้: MeterObservationHandler สำหรับ Micrometer metrics และ TracingObservationHandler สำหรับ distributed trace สถาปัตยกรรมนี้หมายความว่าการ instrument ครั้งเดียวสามารถส่งออกได้ทุกที่

Dependency bridge micrometer-tracing-bridge-otel เชื่อมต่อ Micrometer Tracing กับ SDK ของ OpenTelemetry Trace จะไหลผ่าน SdkTracerProvider ของ OpenTelemetry และส่งออกผ่าน OTLP ไปยัง backend เช่น Jaeger, Tempo หรือ collector ใดก็ได้ที่รองรับ OpenTelemetry

ObservabilityConfig.javajava
@Configuration
public class ObservabilityConfig {

    @Bean
    public ObservationRegistryCustomizer<ObservationRegistry> addLowCardinalityTags() {
        return registry -> registry.observationConfig()
            .observationHandler(new ObservationTextPublisher()); // Logs observations to console
    }
}

การตั้งค่าด้านบนลงทะเบียน ObservationHandler ที่บันทึก observation ทุกครั้งลง log ใน production handler ที่ถูกตั้งค่าอัตโนมัติจะส่งข้อมูลไปยัง Micrometer registry และ OpenTelemetry exporter โดยไม่ต้องเขียนโค้ดเพิ่มเติม

Dependency ที่จำเป็นสำหรับ Tracing ใน Spring Boot 3.4

Spring Boot 3.4 ต้องการ dependency ที่ชัดเจนสำหรับ distributed tracing spring-boot-starter-actuator ให้ Observation API แต่ tracing ต้องการ bridge ของ OpenTelemetry และ exporter

xml
<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-tracing-bridge-otel</artifactId>
    </dependency>
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-exporter-otlp</artifactId>
    </dependency>
</dependencies>

Artifact micrometer-tracing-bridge-otel เชื่อมต่อ Micrometer Tracing กับ API ของ OpenTelemetry opentelemetry-exporter-otlp ส่ง span ไปยัง collector โดยใช้โปรโตคอล OTLP ผ่าน HTTP หรือ gRPC Spring Boot จัดการการซิงค์เวอร์ชันผ่าน dependency management

การตั้งค่าการส่งออก OTLP ไปยัง Jaeger หรือ Tempo

Spring Boot ตั้งค่า OtlpHttpSpanExporter อัตโนมัติเมื่อ dependency OTLP พร้อมใช้งาน Exporter จะส่ง trace ไปยัง endpoint ที่ระบุใน application.yml

yaml
# application.yml
management:
  tracing:
    sampling:
      probability: 1.0  # 100% sampling สำหรับ dev ลดลงใน production
  otlp:
    tracing:
      endpoint: http://localhost:4318/v1/traces
  opentelemetry:
    resource-attributes:
      service.name: order-service
      deployment.environment: staging

resource-attributes แนบ metadata กับทุก span Backend เช่น Grafana Tempo และ Jaeger ใช้ attribute เหล่านี้ในการจัดกลุ่ม trace ตาม service และ environment การตั้งค่า sampling.probability เป็น 1.0 จะจับทุก request ระหว่างการพัฒนา แต่ระบบ production มักจะ sample ระหว่าง 1% ถึง 10% เพื่อควบคุมต้นทุนการจัดเก็บ

การสร้าง Custom Observation ด้วย Tag แบบ Low และ High Cardinality

Observation รองรับ tag สองประเภท: low cardinality สำหรับ metrics (ค่าที่จำกัดเช่น HTTP method หรือ status code) และ high cardinality สำหรับ trace (ค่าที่ไม่จำกัดเช่น user ID หรือ request ID)

PaymentService.javajava
@Service
public class PaymentService {

    private final ObservationRegistry observationRegistry;

    public PaymentService(ObservationRegistry observationRegistry) {
        this.observationRegistry = observationRegistry;
    }

    public PaymentResult processPayment(PaymentRequest request) {
        return Observation.createNotStarted("payment.process", observationRegistry)
            .lowCardinalityKeyValue("payment.method", request.getMethod().name())  // enum: CARD, BANK_TRANSFER
            .lowCardinalityKeyValue("currency", request.getCurrency())              // bounded: USD, EUR, GBP
            .highCardinalityKeyValue("payment.id", request.getPaymentId())          // unique per request
            .highCardinalityKeyValue("customer.id", request.getCustomerId())        // high cardinality
            .observe(() -> executePayment(request));
    }

    private PaymentResult executePayment(PaymentRequest request) {
        // Payment gateway call
        return new PaymentResult(true, "TXN-" + UUID.randomUUID());
    }
}

Tag แบบ low cardinality ปรากฏทั้งใน metrics และ trace Tag แบบ high cardinality ปรากฏเฉพาะใน trace เพราะ metrics ที่มีมิติไม่จำกัดจะทำให้พื้นที่จัดเก็บระเบิด การแยกแยะนี้ป้องกัน cardinality bomb ใน Prometheus ขณะที่รักษารายละเอียด trace ให้สมบูรณ์

คำถามสัมภาษณ์: Tag Cardinality

ผู้สัมภาษณ์มักถามว่าทำไม tag บางตัวควรถูกยกเว้นจาก metrics คำตอบเกี่ยวข้องกับ cardinality: tag user ID บน metric สร้าง time series หนึ่งชุดต่อ user อาจเป็นล้าน series Backend monitoring มีปัญหากับ high cardinality นำไปสู่หน่วยความจำหมดและ query ช้า Trace จัดการ high cardinality ผ่าน sampling ทำให้เป็นที่ที่ถูกต้องสำหรับ identifier เฉพาะ request

พร้อมที่จะพิชิตการสัมภาษณ์ Spring Boot แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

การส่งต่อ Trace Context ข้าม Async Boundary

Distributed tracing ต้องการการส่งต่อ context HTTP header นำ trace ID ระหว่าง service แต่การทำงานแบบ async เช่น method @Async หรือ chain ของ CompletableFuture สามารถสูญเสีย context ได้ถ้าไม่ได้ตั้งค่า

Spring Boot 3.4 ให้การส่งต่ออัตโนมัติสำหรับ reactive stream และ method @Async เมื่อตั้งค่า:

yaml
# application.yml
spring:
  reactor:
    context-propagation: auto
  task:
    execution:
      propagate-context: true

สำหรับ bean TaskExecutor ที่กำหนดเอง ให้ wrap ด้วย ContextPropagatingTaskDecorator:

AsyncConfig.javajava
@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setTaskDecorator(new ContextPropagatingTaskDecorator());
        executor.initialize();
        return executor;
    }
}

Decorator คัดลอก Observation ปัจจุบันและ trace context ไปยัง thread async ถ้าไม่มี span ที่เริ่มใน task async จะไม่มี parent ทำให้ trace tree ขาด

การใช้ Annotation @Observed และ @NewSpan

Spring Boot 3.4 รองรับ observability แบบ declarative ผ่าน annotation เปิดใช้งานการประมวลผล annotation โดยเพิ่ม AspectJ weaver:

xml
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
yaml
# application.yml
management:
  observations:
    annotations:
      enabled: true
InventoryService.javajava
@Service
public class InventoryService {

    @Observed(name = "inventory.check", contextualName = "checkStock")
    public StockLevel checkStock(String productId) {
        // Database query
        return new StockLevel(productId, 42);
    }

    @NewSpan("inventory.reserve")  // Creates a child span under the current trace
    public ReservationResult reserveStock(String productId, int quantity) {
        // Update inventory
        return new ReservationResult(true, "RES-" + System.currentTimeMillis());
    }
}

@Observed สร้างทั้ง metric (timer) และ span @NewSpan สร้างเฉพาะ span มีประโยชน์เมื่อ operation มี metrics อยู่แล้วที่อื่น Annotation ทั้งสองจัดการการบันทึก exception และสถานะ span โดยอัตโนมัติ

Component ที่ถูก Instrument อัตโนมัติใน Spring Boot 3.4

Spring Boot 3.4 instrument component หลายตัวโดยอัตโนมัติโดยไม่ต้องเปลี่ยนโค้ด:

Componentชื่อ Observationสิ่งที่จับได้
Spring MVChttp.server.requestsRequest path, method, status, exception
WebClienthttp.client.requestsOutbound HTTP call
RestClienthttp.client.requestsOutbound HTTP call (Spring 6.1+)
Spring Kafkaspring.kafka.listenerConsumer group, topic, partition
Spring Data JPAspring.data.repositoryRepository method, query time
Scheduled Tasksspring.schedulingTask name, execution time

เอกสาร Spring Boot Actuator แสดงรายการ component ทั้งหมดที่ถูก instrument อัตโนมัติ Library ของบุคคลที่สามเช่น Datasource Micrometer เพิ่ม tracing query JDBC

การกรอง Observation เพื่อลด Noise

Health check, readiness probe และ static resource สร้าง noise ใน trace กรองออกโดยใช้ predicate:

ObservationFilterConfig.javajava
@Configuration
public class ObservationFilterConfig {

    @Bean
    public ObservationPredicate noHealthChecks() {
        return (name, context) -> !name.equals("http.server.requests")
            || !isHealthEndpoint(context);
    }

    private boolean isHealthEndpoint(Observation.Context context) {
        if (context instanceof ServerRequestObservationContext http) {
            String path = http.getCarrier().getRequestURI();
            return path.startsWith("/actuator/health") || path.startsWith("/actuator/ready");
        }
        return false;
    }
}

หรือปิดการใช้งาน observation ตามชื่อในการตั้งค่า:

yaml
# application.yml
management:
  observations:
    enable:
      spring.security: false  # Disable Spring Security observations
      http.server.requests.actuator: false  # Custom predicate name
บริบทสัมภาษณ์: Trace Sampling กับ Filtering

Sampling ลดเปอร์เซ็นต์ของ trace ที่เก็บรวบรวมจากทุก endpoint Filtering ลบ endpoint เฉพาะทั้งหมด ใช้ filtering สำหรับ endpoint ที่ไม่เคยให้คุณค่าในการวินิจฉัย (health check, metrics scraping) ใช้ sampling เพื่อควบคุมต้นทุนขณะที่รักษาข้อมูลที่เป็นตัวแทน

การเชื่อมโยง Log กับ Trace ID

Spring Boot 3.4 เพิ่ม trace และ span ID ลงใน MDC (Mapped Diagnostic Context) โดยอัตโนมัติเมื่อ Micrometer Tracing ทำงาน Logback และ Log4j2 สามารถรวม ID เหล่านี้ในผลลัพธ์ log:

xml
<!-- logback-spring.xml -->
<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - traceId=%X{traceId} spanId=%X{spanId} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

ด้วย pattern นี้ ทุกบรรทัด log จะมี trace ID การค้นหา log ด้วย trace ID จะคืนข้อความทั้งหมดจาก request เดียวข้ามทุก service เชื่อมโยง log กับ span ใน Jaeger หรือ Tempo

คำถามสัมภาษณ์ที่พบบ่อยเกี่ยวกับ Observability ใน Spring Boot

ผู้สัมภาษณ์ประเมินทั้งความเข้าใจแนวคิดและประสบการณ์จริงกับ observability คำถามเหล่านี้ปรากฏบ่อยในตำแหน่ง backend อาวุโส

ถาม: ความแตกต่างระหว่าง Micrometer Tracing และ OpenTelemetry คืออะไร?

Micrometer Tracing เป็น API ที่ไม่ผูกกับ vendor สำหรับ distributed tracing คล้ายกับที่ Micrometer abstract metrics OpenTelemetry เป็น implementation และ protocol เฉพาะ Spring Boot ใช้ Micrometer Tracing เป็น API และเชื่อมต่อกับ OpenTelemetry สำหรับการส่งออกผ่าน micrometer-tracing-bridge-otel การแบ่งชั้นนี้ช่วยให้แอปพลิเคชันเปลี่ยน backend ได้โดยไม่ต้องเปลี่ยนโค้ด

ถาม: ทำไม Spring แนะนำ Observation API มากกว่า instrumentation OpenTelemetry โดยตรง?

Observation API ให้จุด instrumentation เดียวที่สร้างทั้ง metrics และ trace การเรียก OpenTelemetry โดยตรงสร้างเฉพาะ trace การใช้ Observation.observe() สร้าง timer metric และ span จากโค้ดเดียวกัน ลดการซ้ำซ้อนและรับประกันการตั้งชื่อที่สอดคล้อง

ถาม: วิธี debug trace ที่แสดงช่องว่างระหว่าง span?

ช่องว่างบ่งบอกว่าขาด instrumentation หรือการส่งต่อ context เสีย ตรวจสอบว่าโค้ดใช้การทำงาน async โดยไม่มี ContextPropagatingTaskDecorator ยืนยันว่า HTTP client ถูก instrument (WebClient, RestClient หรือ RestTemplate ที่ wrap เอง) สำหรับ message queue ยืนยันว่า trace header ถูกส่งต่อผ่าน property ของ message

ถาม: เกิดอะไรขึ้นถ้าเพิ่ม tag แบบ high cardinality ใน metric?

ทุกค่า tag ที่ไม่ซ้ำจะสร้าง time series ใหม่ tag user ID ที่มีผู้ใช้หลายล้านคนจะสร้างหลายล้าน series ทำให้หน่วยความจำหมดใน Prometheus หรือ backend TSDB อื่น วิธีแก้คือใช้ highCardinalityKeyValue() แทน lowCardinalityKeyValue() ซึ่งเพิ่ม tag เฉพาะใน trace ที่คาดว่าจะมี high cardinality

การสร้าง Pipeline Distributed Tracing ด้วย Docker Compose

Stack observability ในเครื่องช่วยตรวจสอบ instrumentation ก่อน deploy ไป production ตัวอย่างนี้ใช้ OpenTelemetry Collector และ Jaeger:

yaml
# docker-compose.yml
services:
  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.102.0
    command: ["--config", "/etc/otel-collector-config.yml"]
    volumes:
      - ./otel-collector-config.yml:/etc/otel-collector-config.yml
    ports:
      - "4318:4318"   # OTLP HTTP
      - "4317:4317"   # OTLP gRPC

  jaeger:
    image: jaegertracing/jaeger:2.3
    ports:
      - "16686:16686"  # UI
    environment:
      - COLLECTOR_OTLP_ENABLED=true

  app:
    build: .
    environment:
      - MANAGEMENT_OTLP_TRACING_ENDPOINT=http://otel-collector:4318/v1/traces
    depends_on:
      - otel-collector
yaml
# otel-collector-config.yml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  batch:
    timeout: 1s
    send_batch_size: 1024

exporters:
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp/jaeger]

Collector รับ span จากแอปพลิเคชัน Spring Boot และส่งต่อไปยัง Jaeger สถาปัตยกรรมนี้อนุญาตให้เพิ่ม exporter (Tempo, Zipkin, cloud backend) โดยไม่ต้องเปลี่ยนโค้ดแอปพลิเคชัน

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

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

ประเด็นสำคัญสำหรับ Observability ใน Spring Boot

  • Observation API รวม metrics และ tracing: การเรียก Observation.observe() หนึ่งครั้งสร้าง timer metric และ span ขจัดการ instrument ซ้ำ
  • เพิ่ม micrometer-tracing-bridge-otel และ opentelemetry-exporter-otlp เพื่อเปิดใช้งานการส่งออก OTLP Spring Boot 3.4 ตั้งค่า HTTP exporter อัตโนมัติด้วย endpoint จาก management.otlp.tracing.endpoint
  • ใช้ lowCardinalityKeyValue() สำหรับมิติที่ปรากฏใน metrics (ค่าที่จำกัด) ใช้ highCardinalityKeyValue() สำหรับข้อมูลเฉพาะ trace (user ID, request ID)
  • เปิดใช้งานการส่งต่อ context สำหรับโค้ด async ด้วย spring.task.execution.propagate-context=true และ wrap executor ที่กำหนดเองด้วย ContextPropagatingTaskDecorator
  • กรอง endpoint ที่มี noise เช่น /actuator/health โดยใช้ bean ObservationPredicate เพื่อให้ trace มุ่งเน้นที่การดำเนินงานทางธุรกิจ
  • Pattern log ควรรวม %X{traceId} เพื่อเชื่อมโยงบรรทัด log กับ distributed trace ใน backend observability
  • คำถามสัมภาษณ์เกี่ยวกับ observability เน้นที่ cardinality ความล้มเหลวในการส่งต่อ context และความแตกต่างระหว่าง Observation API กับการใช้ OpenTelemetry โดยตรง
ชาเลนจ์ประจำวัน

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

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

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

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

อัปเดตเมื่อ 19 กันยายน 2569

แท็ก

#spring-boot
#observability
#opentelemetry
#distributed-tracing
#micrometer

แชร์

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