# Observability ใน Spring Boot 2026: OpenTelemetry, Distributed Tracing และคำถามสัมภาษณ์ > เรียนรู้ observability ใน Spring Boot ด้วย OpenTelemetry และ Micrometer Tracing คู่มือการตั้งค่า distributed tracing, Observation API, การส่งออก OTLP และการเตรียมตัวสัมภาษณ์งานเทคนิค - Published: 2026-09-19 - Updated: 2026-09-19 - Author: Anthony Fillion-Maillet - Tags: spring-boot, observability, opentelemetry, distributed-tracing, micrometer - Reading time: 12 min --- 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 ```java // ObservabilityConfig.java @Configuration public class ObservabilityConfig { @Bean public ObservationRegistryCustomizer 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 org.springframework.boot spring-boot-starter-actuator io.micrometer micrometer-tracing-bridge-otel io.opentelemetry opentelemetry-exporter-otlp ``` 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](https://grafana.com/oss/tempo/) และ [Jaeger](https://www.jaegertracing.io/) ใช้ 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) ```java // PaymentService.java @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 ## การส่งต่อ 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`: ```java // AsyncConfig.java @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 org.springframework.boot spring-boot-starter-aop ``` ```yaml # application.yml management: observations: annotations: enabled: true ``` ```java // InventoryService.java @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 MVC | `http.server.requests` | Request path, method, status, exception | | WebClient | `http.client.requests` | Outbound HTTP call | | RestClient | `http.client.requests` | Outbound HTTP call (Spring 6.1+) | | Spring Kafka | `spring.kafka.listener` | Consumer group, topic, partition | | Spring Data JPA | `spring.data.repository` | Repository method, query time | | Scheduled Tasks | `spring.scheduling` | Task name, execution time | [เอกสาร Spring Boot Actuator](https://docs.spring.io/spring-boot/reference/actuator/observability.html) แสดงรายการ component ทั้งหมดที่ถูก instrument อัตโนมัติ Library ของบุคคลที่สามเช่น [Datasource Micrometer](https://github.com/jdbc-observations/datasource-micrometer) เพิ่ม tracing query JDBC ## การกรอง Observation เพื่อลด Noise Health check, readiness probe และ static resource สร้าง noise ใน trace กรองออกโดยใช้ predicate: ```java // ObservationFilterConfig.java @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 %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - traceId=%X{traceId} spanId=%X{spanId} - %msg%n ``` ด้วย 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](https://opentelemetry.io/docs/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 โดยตรง --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/spring-boot/spring-boot-observability-opentelemetry-distributed-tracing