Skip to content

บทที่ 3 — Distributed Tracing

← บทที่ 2 | สารบัญ | บทที่ 4 →

หลังจบบท คุณจะ:

  • เข้าใจ trace, span, context propagation
  • Instrument Spring Boot ด้วย OpenTelemetry
  • เลือก sampling strategy ที่ถูก
  • ใช้ trace debug bug ใน microservices
  • Correlate trace กับ log + metric

1. ทำไมต้อง Tracing

ใน monolith debug ด้วย stack trace เดียวก็พอ แต่ใน microservices 1 request วิ่งผ่าน 10 service สร้าง span (สแปน = ช่วงงาน — 1 unit ของ trace เช่น 1 DB query) 50 อัน log กระจายทั่ว — คำถามง่าย ๆ อย่าง "ทำไม request นี้ช้า?" ตอบไม่ได้ถ้าไม่มี tracing ที่ร้อยทุก hop เข้าด้วยกัน (ตัวอย่างจริง: 1 request ของ Netflix watching session อาจผ่าน 50+ service):

Monolith:
- 1 request → 1 stack trace
- ดู log ก็พอ

Microservices:
- 1 request → 10 service → 50 span
- Log กระจายใน 10 service
- "ทำไม request นี้ช้า?" → ตอบยาก โดยไม่มี trace

Example Bug

User report: "Page load 3 seconds — slow"

ดู log → 10 service มี log
ดู metric → wait, แต่ละ service ดู OK

→ ดู trace ของ request เดียวนั้น:
   gateway (5ms) → user service (10ms) → order service (2800ms!) → ...
   
ดูภายใน order service:
   handler → db query (50ms) → cache lookup (5ms) → external API (2700ms!)
   
→ พบสาเหตุ: external API ช้า — ไม่มีใครรู้ก่อน

→ Trace = visualize ของ 1 request


2. Core Concepts

โครงสร้างของ tracing เป็นต้นไม้ — "trace" คือเส้นทางทั้งหมดของ 1 request ประกอบด้วย "span" หลายอัน (แต่ละ span = งานหนึ่งชิ้น) span เชื่อมกันด้วย trace ID (ระบุ request เดียวกัน) + parent span ID (บอกว่าใครเป็นพ่อ) จึงประกอบเป็นลำดับชั้นได้:

Span Anatomy (โครงสร้างของ span — มีอะไรอยู่ข้างใน)

json
{
    "traceId": "abc-123-456",
    "spanId": "def-789",
    "parentSpanId": "ghi-101",
    "operationName": "GET /api/users",
    "service": "user-service",
    "startTime": "2026-05-18T10:00:00.000Z",
    "duration": "150ms",
    "status": "OK",
    "attributes": {
        "http.method": "GET",
        "http.status_code": 200,
        "user.id": "42"
    },
    "events": [
        { "time": "+50ms", "name": "db_query_start" },
        { "time": "+100ms", "name": "db_query_end" }
    ]
}

3. Context Propagation (การส่งต่อ trace context ข้าม service)

สิ่งที่ทำให้ span จากหลาย service ร้อยเป็น trace เดียวได้คือ "context propagation" — service แรกสร้าง trace context แล้วส่งต่อผ่าน HTTP header ทุก hop service ถัดไปอ่าน context เดิมมาสร้าง child span ทำให้ trace ต่อเนื่องข้ามทั้งระบบ:

Trace ไหลข้าม service ผ่าน HTTP header:

Request 1: client → gateway
  Header: (no trace context)
  Gateway: generate traceId + spanId
  
Request 2: gateway → user-service
  Header: 
    traceparent: 00-abc123-def456-01
                     │      │
                     traceId  spanId (gateway's)
  user-service: ใช้ traceId เดียวกัน, สร้าง spanId ใหม่
  
Request 3: user-service → database
  Header:
    traceparent: 00-abc123-ghi789-01

                            user-service's spanId

W3C Trace Context (W3C = องค์กรกำหนดมาตรฐาน web — กำหนดรูปแบบ trace กลางที่ vendor ทุกตัวใช้)

traceparent: 00-{traceId-32-hex}-{spanId-16-hex}-{flags-2-hex}
tracestate: vendor=value,vendor2=value2     (optional, vendor-specific)

# ตัวอย่างจริงที่ทุก service ส่งต่อกันใน header:
traceparent: 00-abc1234567890abcdef1234567890abcd-def4567890abcdef-01

→ ทุก vendor / library รองรับ — interoperable


4. OpenTelemetry — Standard

OpenTelemetry (OTel) = vendor-neutral

  • SDK สำหรับทุกภาษา
  • OTLP protocol ← OTLP = OpenTelemetry Protocol: รูปแบบ/โปรโตคอลกลางที่ OTel ใช้ส่ง telemetry (logs/metrics/traces)
  • ส่ง trace ไปไหนก็ได้ (Jaeger, Tempo, Datadog, ...)

Spring Boot Setup

xml
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-otel</artifactId>
    <!-- version มาจาก Spring Boot BOM (spring-boot-dependencies) — ไม่ต้องระบุ -->
</dependency>
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-exporter-otlp</artifactId>
    <!-- version มาจาก Spring Boot BOM; ถ้า project standalone (ไม่มี parent BOM)
         ต้องระบุ version หรือ import opentelemetry-bom เอง -->
</dependency>
yaml
management:
  tracing:
    sampling:
      probability: 1.0          # 1.0 = 100% (เก็บทุก trace, dev), 0.1 = 10% (prod), 0.01 = 1%
      # หมายเหตุ: Micrometer Tracing OTel bridge ใช้ parent-based sampler โดย default
      # — ค่านี้คือ root sampling rate ที่ ingress; service กลางจะตามตัดสินใจตาม parent flag
      # ใน traceparent header (ไม่ตัดสินใจซ้ำ → trace ไม่ขาด)
  otlp:
    tracing:
      endpoint: http://otel-collector:4318     # ไม่ต้องใส่ /v1/traces — Micrometer ต่อ path ให้เอง

→ Auto-instrument: HTTP server, HTTP client, JDBC, Redis, Kafka, ...

Spring จะใส่ traceId + spanId ใน MDC อัตโนมัติ → log มี trace context


5. Auto-Instrumentation vs Manual (instrumentation = การติดอุปกรณ์วัด — ใส่ code ที่บันทึก span/metric ลงใน app)

Auto (default ของ Spring Boot)

✅ Instrument ทุก:

  • HTTP request (server + client)
  • JDBC / JPA query
  • Redis call
  • Kafka producer/consumer
  • Spring scheduled task

→ ไม่ต้องเขียน code

Manual — custom span

java
@Service
@RequiredArgsConstructor
public class OrderService {
    
    private final Tracer tracer;
    
    public Order processOrder(Order order) {
        Span span = tracer.spanBuilder("processOrder")
            .setAttribute("order.id", order.getId())
            .setAttribute("order.total", order.getTotal().doubleValue())
            .startSpan();
        
        // try-with-resources: scope จะ close อัตโนมัติเมื่อออก block
        // → ทำให้ span นี้เป็น "current span" ระหว่างที่อยู่ใน block
        try (var scope = span.makeCurrent()) {
            // Sub-span
            Span validateSpan = tracer.spanBuilder("validate").startSpan();
            try (var s2 = validateSpan.makeCurrent()) {
                validate(order);
            } finally {
                validateSpan.end();
            }
            
            // Add event
            span.addEvent("order_validated");
            
            charge(order);
            
            span.addEvent("payment_processed");
            
            return save(order);
        } catch (Exception e) {
            span.recordException(e);
            span.setStatus(StatusCode.ERROR, e.getMessage());
            throw e;
        } finally {
            span.end();
        }
    }
}

Annotation-based (cleaner)

java
@Service
public class OrderService {
    
    // หมายเหตุ: ตัวอย่างนี้ใช้ orderId (Long) เพื่อโชว์ @SpanAttribute
    // — signature ต่างจากตัวอย่างด้านบนที่รับ Order object เต็ม ๆ
    // เลือกแบบใดแบบหนึ่งตาม use case
    @WithSpan("processOrder")
    public Order processOrder(@SpanAttribute("orderId") Long orderId) {
        // automatic span
    }
}

(ต้องเพิ่ม opentelemetry-instrumentation-annotations dependency)


6. Span Attributes Best Practices

java
// ✅ ใช้ semantic conventions (ชื่อมาตรฐาน — tool ทุกตัวรู้จัก)
// ปี 2026 ใช้ stable names จาก OTel semantic conventions v1.23+
span.setAttribute("http.request.method", "GET");         // เดิม http.method
span.setAttribute("http.response.status_code", 200);     // เดิม http.status_code
span.setAttribute("http.route", "/users/:id");           // คงเดิม
span.setAttribute("url.path", "/users/42");              // เดิม http.target
span.setAttribute("url.scheme", "https");                // เดิม http.scheme
span.setAttribute("server.address", "api.example.com");  // เดิม http.host
span.setAttribute("user.id", "42");
span.setAttribute("db.query.text", "SELECT * FROM users WHERE id = ?");  // เดิม db.statement

// ✅ ใส่ context (บริบทเพิ่ม)
span.setAttribute("order.id", orderId);
span.setAttribute("order.total", total.doubleValue());

// ❌ Don't add sensitive data
span.setAttribute("password", password);   // NO
span.setAttribute("credit_card", card);    // NO

OpenTelemetry มี semantic conventions (ความหมายมาตรฐาน — ชื่อ attribute ที่ทุก tool รู้จัก)
→ ใช้ตามนี้ — tool จะ visualize ดีขึ้น
→ ดู spec ล่าสุดที่ https://opentelemetry.io/docs/specs/semconv/


7. Cross-Service Tracing

จุดที่ tracing โดดเด่นคือการตามข้าม service — เมื่อ A เรียก B ผ่าน HTTP, Spring จะ inject traceparent header ให้อัตโนมัติ และ B ก็ extract มาสร้าง span เป็น child ของ A เอง ทำให้เห็น trace ต่อเนื่องโดยไม่ต้องเขียนเพิ่ม (ยกเว้น async ที่ต้องระวัง):

Service A → call Service B:

java
// Service A
@Service
public class OrderService {
    private final RestClient restClient;
    
    public Payment chargePayment(Order order) {
        // Trace context auto-propagate ใน HTTP header
        return restClient.post()
            .uri("http://payment-service/charges")
            .body(new ChargeRequest(order.getTotal()))
            .retrieve()
            .body(Payment.class);
    }
}

// Service B (auto-receive)
@RestController
public class PaymentController {
    @PostMapping("/charges")
    public Payment charge(@RequestBody ChargeRequest req) {
        // Span ของ B = child ของ A's span (auto)
        return paymentService.process(req);
    }
}

→ Spring auto-inject traceparent header + extract ที่ receiver

⚠️ ระวัง — Async + Trace Context

java
// ❌ @Async — context หาย
@Async
public void notifyUser(Long userId) {
    // ไม่มี trace context
}

// ✅ TaskDecorator with trace propagation
@Bean
public TaskDecorator traceDecorator() {
    return runnable -> {
        Context context = Context.current();
        return () -> {
            try (var scope = context.makeCurrent()) {
                runnable.run();
            }
        };
    };
}

8. Storage + Visualization

trace ต้องมีที่เก็บและ UI ดู — มี backend หลายตัว (Tempo เน้นถูก, Jaeger ยอดนิยม, Datadog แบบ SaaS) เลือกตาม budget และว่าจะ self-host หรือไม่ ตัวอย่างใช้ Tempo ที่เก็บ trace บน object storage ทำให้ต้นทุนต่ำ:

Backends

ToolNotes
TempoGrafana — cheap, S3 storage
JaegerCNCF, popular
ZipkinTwitter, older
Datadog APMpaid
Honeycombhigh-cardinality first
AWS X-RayAWS-native
OpenTelemetry Demoreference setup

Tempo Setup

yaml
# docker-compose.yml
services:
  tempo:
    image: grafana/tempo:latest
    ports:
      - "3200:3200"
      - "4317:4317"     # OTLP gRPC (ส่งผ่าน gRPC, binary, เร็ว)
      - "4318:4318"     # OTLP HTTP (ส่งผ่าน HTTP, JSON, debug ง่าย)
    volumes:
      - ./tempo.yml:/etc/tempo.yml
    command: ["-config.file=/etc/tempo.yml"]
  
  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    volumes:
      - ./otel-config.yml:/etc/otel/config.yml
yaml
# otel-config.yml
receivers:            # รับ trace เข้า
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317   # รับผ่าน OTLP gRPC (พอร์ต 4317)
      http:
        endpoint: 0.0.0.0:4318   # รับผ่าน OTLP HTTP (พอร์ต 4318)

processors:
  batch: {}           # รวม trace เป็น batch ก่อนส่ง (ลดจำนวนครั้งที่ส่ง = มีประสิทธิภาพ)

exporters:            # ส่ง trace ออกไปที่ไหน
  otlp/tempo:
    endpoint: tempo:4317
    tls:
      insecure: true  # dev เท่านั้น — production ควรเปิด TLS

service:
  pipelines:
    traces:                    # ท่อส่ง trace: รับ → ประมวลผล → ส่งออก
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp/tempo]

→ Add Tempo data source ใน Grafana → search trace


9. Sampling Strategies

ไม่สามารถเก็บทุก trace ใน scale → ต้อง sample

1. Head-based (default)

ตัดสินใจ sample ที่ root span

Random 1% → keep
99% → drop (ทั้ง trace)
yaml
management:
  tracing:
    sampling:
      probability: 0.1     # 10% sampling

✅ Simple, predictable cost ❌ Random — might miss interesting trace (slow / error)

2. Tail-based

ต้อง OTel Collector ที่รองรับ tail-based:

yaml
processors:
  tail_sampling:
    decision_wait: 30s          # รอ 30 วิให้ trace จบก่อนตัดสินใจ keep/drop
    policies:                   # เก็บ trace ถ้าเข้าเงื่อนไขข้อใดข้อหนึ่ง
      - name: error-policy      # 1) มี error → เก็บ
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: slow-policy       # 2) ช้าเกิน 1 วินาที → เก็บ
        type: latency
        latency:
          threshold_ms: 1000
      - name: random-policy     # 3) ที่เหลือ สุ่มเก็บ 1%
        type: probabilistic
        probabilistic:
          sampling_percentage: 1

✅ Capture interesting trace ❌ Need memory (buffer all spans), more complex

3. Adaptive Sampling

Sampling rate ปรับตาม traffic:
- Low traffic: 100%
- High traffic: 1%

Honeycomb, some tools รองรับ

Recommendations

Dev / staging:    100% sampling
Production (low traffic):    100% หรือ 50%
Production (high traffic):   1-10% head-based + tail-based 100% error/slow
Cost-constrained:  1% head only

Context Propagation ข้าม async + queue

Trace context (traceparent + tracestate) propagate ผ่าน HTTP header อัตโนมัติ แต่ ข้าม queue/async ต้องทำมือ


Kafka — แนบ trace ใน record header

🟡 ขั้นสูง — ข้ามได้ถ้ายังไม่ใช้ Kafka/queue

Section นี้สอนวิธี propagate trace context ข้าม Kafka producer/consumer ด้วยมือ basic ที่ใช้ Spring Boot REST API service-to-service: ข้ามได้ — Spring Boot 3 + Micrometer Tracing auto-propagate trace context ผ่าน HTTP header traceparent อัตโนมัติแล้ว ไม่ต้องเขียน inject/extract เอง

กลับมาอ่านเมื่อ:

  • เริ่มใช้ Kafka/RabbitMQ/SQS (async messaging)
  • debug trace ที่ "ขาดตอน" ที่ queue boundary

สำหรับ Kafka + Spring Kafka 3.1+: เปิดผ่าน config ก็พอ — spring.kafka.template.observation-enabled=true + spring.kafka.listener.observation-enabled=true · Micrometer ใส่ traceparent ให้ใน Kafka header เอง · ต้องมี ObservationRegistry bean (Spring Boot 3.x auto-config ให้แล้วเมื่อมี Micrometer Observation บน classpath)

java
import static java.nio.charset.StandardCharsets.UTF_8;

// Producer
ProducerRecord<String, String> record = new ProducerRecord<>("orders", payload);
// Inject trace context เข้า header
W3CTraceContextPropagator.getInstance().inject(
    Context.current(), record.headers(),
    (headers, key, value) -> headers.add(key, value.getBytes(UTF_8))
);
producer.send(record);

// Consumer
Context extracted = W3CTraceContextPropagator.getInstance().extract(
    Context.current(), record.headers(),
    (headers, key) -> {
        var h = headers.lastHeader(key);
        return h != null ? new String(h.value(), UTF_8) : null;
    }
);
try (Scope s = extracted.makeCurrent()) {
    // process — trace ต่อจาก producer span
}

Micrometer Observation + Spring Kafka 3.1+ ทำได้ผ่าน kafkaTemplate.setObservationEnabled(true) (setter) หรือใช้ property spring.kafka.template.observation-enabled=true (recommended)

SQS / RabbitMQ — pattern เดียวกัน

ใส่ traceparent เป็น message attribute (SQS) หรือ header (RabbitMQ)

Parent-based sampling consistency

Inconsistent sampling = ปัญหาบ่อย: service A sample 10%, service B sample 50% → trace ที่ A drop จะมี B ลอยเดี่ยว ๆ ไม่มี root span = useless

Parent-based sampler: decision ที่ root service เท่านั้น — service ลึกตาม decision จาก traceparent flag

bash
# กรณีใช้ OTel Java Agent / vanilla OTel SDK — ตั้งผ่าน env var หรือ -D system property ตอน boot
# (ไม่ใช่ application.yml — properties นี้ไม่ใช่ Spring property)
export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1     # 10% ที่ root, ลึก ๆ ตาม parent

# หรือ
java -Dotel.traces.sampler=parentbased_traceidratio \
     -Dotel.traces.sampler.arg=0.1 \
     -jar app.jar
yaml
# กรณีใช้ Spring Boot + Micrometer Tracing — parent-based เป็น default แล้ว
# ตั้งแค่ probability พอ (เป็น root sampling rate)
management:
  tracing:
    sampling:
      probability: 0.1     # 10% ที่ root, service ลึก ๆ จะตามตาม parent อยู่แล้ว

W3C Baggage — ส่ง context ที่ไม่ใช่ trace

baggage header (ต่างจาก traceparent) ส่ง key-value ผ่าน trace chain เช่น user.tier=premium — ใช้ใน metric หรือ feature flag ใน service ลึก ๆ


เก็บ trace แล้วต้องหาเจอด้วย — ค้นได้หลายแบบ: ตาม trace ID (ได้จาก log หรือ response header), ตาม service+operation, หรือตาม attribute ผ่าน query language เช่น TraceQL (เช่นหา trace ที่ช้าเกิน 1 วินาทีและ error):

By Trace ID

Tempo / Jaeger: search by traceId
- Get from log:
  log.info(...) → automatically include traceId in MDC
- Get from response header:
  response: X-Trace-Id: abc123

By Service + Operation

Tempo:
  service.name = "order-service"
  span.name = "POST /orders"

By Attribute

TraceQL (Tempo) — ใช้ && สำหรับ AND, || สำหรับ OR:
{ resource.service.name="order-service" && span.http.status_code >= 500 }

# หา trace ที่ช้าเกิน 1 วินาที + error
{ duration > 1s && status = error }

→ Find specific issue trace


11. Trace Analysis Patterns

trace ช่วยวินิจฉัยปัญหาได้หลายแบบที่ log/metric อย่างเดียวทำไม่ได้ — รวม pattern ที่พบบ่อย: หา span ที่ช้าสุด, ระบุ service ที่ throw error ก่อน, จับ N+1 query, เห็น cascade failure, และค้นพบ dependency ที่ทีมไม่รู้ว่ามี:

Pattern 1: Slow Trace

"ทำไม request ช้า?"

ดู trace → identify slowest span:
- DB query slow → index missing?
- External API slow → switch provider?
- Sequential calls → parallelize?

Pattern 2: Error Trace

"Error เกิดที่ไหน?"

Trace แสดง error span (ตัวที่ first throw):
- Identify root cause service
- Click trace → see logs of that service
- See stack trace + context

Pattern 3: N+1 Query (เอ็น-บวก-หนึ่ง — anti-pattern ที่ทำ 1 query แล้ว loop ทำ N query เพิ่ม รวม N+1)

Trace แสดง:
  controller (200ms)
    handler (190ms)
      query 1 (1ms)
      query 2 (1ms)
      query 3 (1ms)
      ... (1000 queries)

→ Classic N+1 — fix with JOIN FETCH

Pattern 4: Cascade Failure

Trace แสดง:
  service A (1s)
    service B (1s, retried 3 times)
      service C (300ms, timeout)
        ...

→ Service C ช้า → B retry → A wait
→ Fix: add circuit breaker

Pattern 5: Hidden Dependency

Trace แสดง span "external-api.example.com" — แต่ทีมไม่รู้ว่าใช้!

→ Audit dependency → remove or monitor

12. Trace + Log Correlation

เชื่อม trace กับ log เข้าด้วยกันทำให้สลับมุมมองได้ทันที — ถ้าทุก log มี traceId (Spring Boot ใส่ให้อัตโนมัติ) ก็คลิกจาก log ไป trace หรือจาก span ไปดู log ของ span นั้นได้ (navigation สองทาง) เห็นทั้ง "เกิดอะไร" และ "ช้าตรงไหน":

java
log.info("Processing order");
// Spring Boot auto-include traceId + spanId in MDC
// Log output (JSON):
// { "message": "Processing order", "traceId": "abc123", "spanId": "def456" }

Grafana Setup

Loki data source → derived field:
  Name: traceId
  Regex: traceId=(\w+)
  Internal link: → Tempo

Click traceId in log → jump to trace in Tempo

Tempo → Logs

Tempo span → "Logs for this span" button → Loki query with traceId

→ Bidirectional navigation ระหว่าง logs + traces


13. Trace + Metric Correlation — Exemplars

ขั้นต่อไปคือเชื่อม metric กับ trace ด้วย exemplar — histogram ของ Prometheus แนบ traceId ของ request ตัวอย่างไว้ พอเห็น latency spike (เช่น p99 พุ่ง) ใน Grafana ก็คลิกจุด outlier นั้นแล้วกระโดดไปดู trace จริงได้เลย ไม่ต้องไล่หาเอง:

Exemplar (เอ็กเซมพลาร์ = "ตัวอย่างตัวแทน") = จุดตัวอย่างใน metric ที่แนบ traceId ไว้ → link จาก metric กลับไป trace

promql
# In Prometheus — exemplar attached to histogram bucket
# histogram_quantile = function ของ Prometheus (ดูบทที่ 2 §6) ที่คำนวณ percentile จาก histogram
histogram_quantile(0.95, rate(http_server_requests_seconds_bucket[5m]))

# In Grafana — click p99 spike → see traces ที่อยู่ใน bucket นั้น

Setup:

yaml
management:
  tracing:
    sampling:
      probability: 1.0
  metrics:
    distribution:
      percentiles-histogram:
        http.server.requests: true

→ Click outlier point → see actual trace


14. Common Span Conventions

OpenTelemetry กำหนด semantic conventions (ความหมายมาตรฐาน — ชื่อ attribute ที่ทุก tool รู้จัก เช่น http.method, db.statement) สำหรับงานแต่ละประเภท (HTTP, DB, messaging) ถ้าตั้ง attribute ตามนี้ tool จะ visualize และ query ได้ดีขึ้น (เพราะรู้จัก field มาตรฐาน) ควรใช้ตามแทนที่จะตั้งชื่อเอง:

ตัวอย่างด้านล่างอ้างอิง OTel Semantic Conventions v1.23+ (stable) — Nov 2023+ โดย legacy names (เช่น http.method, http.target, db.statement) ถูกแทนด้วย stable names (http.request.method, url.path, db.query.text) — ดู https://opentelemetry.io/docs/specs/semconv/

HTTP Server

operationName: "GET /users/:id"
attributes:
  http.request.method: "GET"           # เดิม http.method
  url.scheme: "https"                   # เดิม http.scheme
  server.address: "api.example.com"     # เดิม http.host
  url.path: "/users/42"                 # เดิม http.target
  http.route: "/users/:id"
  http.response.status_code: 200        # เดิม http.status_code
  user_agent.original: "..."            # เดิม http.user_agent

HTTP Client

operationName: "POST"
attributes:
  http.request.method: "POST"
  url.full: "https://payment-api/charges"   # เดิม http.url
  http.response.status_code: 200

Database

operationName: "SELECT users"
attributes:
  db.system: "postgresql"
  db.name: "myapp"
  db.query.text: "SELECT * FROM users WHERE id = ?"   # เดิม db.statement
  db.operation.name: "SELECT"                          # เดิม db.operation

Messaging

operationName: "process order-events"
attributes:
  messaging.system: "kafka"
  messaging.destination.name: "order-events"   # เดิม messaging.destination
  messaging.operation.type: "receive"          # เดิม messaging.operation

15. Performance Impact

คนมักกังวลว่า tracing จะทำให้ระบบช้า — จริง ๆ overhead ต่ำมาก (CPU/latency < 1%, memory ไม่กี่ KB ต่อ request) ยอมรับได้กับ app ทั่วไป จะเริ่มมีผลก็ต่อเมื่อ ultra-low latency หรือ span เยอะมากต่อ request ซึ่งแก้ด้วย sampling:

Overhead per span:
- Memory: ~1 KB
- CPU: ~10 microseconds
- Network: ~500 bytes (compressed)

Typical request:
- 5-20 spans
- ~10 KB memory
- ~50 microseconds CPU
- 5-10 KB network (ส่ง batch)

Total:
- < 1% CPU overhead
- < 1% latency added
- Few MB / hour bandwidth

→ Acceptable for most app

⚠️ High-volume service (> 10K req/s) อาจเห็น overhead 2-5% + memory pressure จาก BatchSpanProcessor — ตัว context propagation เองก็ allocate object ใน hot path ทุก request · แก้: ใช้ BatchSpanProcessor + tune queue size + sampling เป็นหลัก (head 1-10% + tail-based 100% error/slow)

When Overhead Matter

  • ultra-low latency (HFT, < 1ms p99)
  • High span count per request (1000+)

→ Sample more aggressively, batch export


16. Span Limits

เพื่อป้องกัน span ที่บวมจนกิน memory (เช่น loop เพิ่ม event ไม่หยุด) OTel ตั้ง limit ได้ — จำกัดจำนวน attribute/event/link ต่อ span และความยาวของค่า เป็น safety net กัน bug ในการ instrument ทำระบบล่ม:

yaml
otel:
  span:
    attribute:
      count-limit: 128            # default ของ OTel spec
      value-length-limit: 8192    # ตั้ง limit ที่นี่ก็ดี (default = unlimited) — กัน attribute ยาวมาก ๆ
    event:
      count-limit: 128
    link:
      count-limit: 128

→ Prevent runaway span (e.g., infinite loop adding events) → Production แนะนำตั้ง value-length-limit ชัด ๆ (เช่น 8192) เพราะ default ของ spec ไม่จำกัดความยาว — risk: 1 attribute ขนาดใหญ่ทำ payload บวม


17. Custom Instrumentation Examples

auto-instrumentation ครอบคลุม call ทั่วไป แต่งานเฉพาะ (async job, business logic) อาจต้องสร้าง span เอง — ตัวอย่างนี้แสดงวิธีที่ถูกต้อง: เปิด span พร้อม attribute, ทำงานใน scope, บันทึก exception/status, แล้วปิด span ใน finally เสมอ:

Async Job

java
@Component
@RequiredArgsConstructor
public class EmailWorker {
    private final Tracer tracer;
    
    @KafkaListener(topics = "email-events")
    public void process(EmailEvent event) {
        Span span = tracer.spanBuilder("email.send")
            .setAttribute("email.template", event.getTemplate())
            .setAttribute("email.recipient", maskEmail(event.getTo()))
            .startSpan();
        
        try (var scope = span.makeCurrent()) {
            emailService.send(event);
        } catch (Exception e) {
            span.recordException(e);
            span.setStatus(StatusCode.ERROR);
            throw e;
        } finally {
            span.end();
        }
    }
}

Custom Resource

java
@Bean
public Resource otelResource() {
    return Resource.create(
        Attributes.builder()
            .put("service.name", "order-service")
            .put("service.version", "1.2.3")
            .put("deployment.environment", "production")
            .put("service.namespace", "my-team")
            .build()
    );
}

→ ทุก span ของ service นี้มี attribute เหล่านี้


18. Anti-Patterns

❌ ทำ span ทุก method

java
@WithSpan
private void utilityMethod() { ... }   // ทุก call → span

@WithSpan
private boolean isEven(int n) { return n % 2 == 0; }   // ทุกครั้ง!

→ Trace flood (น้ำท่วม trace — span ล้น) — sample เฉพาะที่สำคัญพอ

❌ Sensitive Data in Attribute

java
span.setAttribute("password", password);
span.setAttribute("credit_card", card);

→ Trace visible to anyone with access → leak

❌ Forget to End Span

java
Span span = tracer.spanBuilder("op").startSpan();
try {
    // work
} 
// ❌ Forgot span.end() → leak
java
// ✅ Try-finally
try { ... } finally { span.end(); }

// ✅✅ Best — make scope
try (var scope = span.makeCurrent()) {
    // work
} finally {
    span.end();
}

❌ 100% Sampling in Production

1000 req/sec × 20 spans = 20000 spans/sec
× 1 KB each = 20 MB/sec
= 1.7 TB / day
→ storage cost explosion

→ Tail-based 100% error + 1-10% normal

❌ Don't Trace 3rd party SDK

3rd party SDK ไม่มี OTel → span chain broken

→ Use OTel auto-instrumentation agent (Java) ที่ instrument bytecode

bash
# ต้องระบุ endpoint ของ OTel Collector — default คือ http://localhost:4317 (OTLP gRPC)
# ถ้าไม่มี collector running จะ silent fail (ไม่เห็น trace, ไม่ error)
java -javaagent:opentelemetry-javaagent.jar \
     -Dotel.exporter.otlp.endpoint=http://otel-collector:4317 \
     -Dotel.service.name=order-service \
     -jar app.jar

→ Instrument 100+ library อัตโนมัติ (Spring, Kafka, Redis, AWS SDK, ...)


19. Debugging with Trace — Step-by-step

มาดูกระบวนการ debug ด้วย trace จริงแบบทีละขั้น — ตั้งแต่ได้ traceId (จาก response header หรือ log), เปิด trace, หา span ที่ช้า/error, drill down ดู attribute + log ที่เกี่ยวข้อง, แก้ แล้ว verify ด้วย trace ใหม่ เป็น workflow ที่ใช้ได้กับทุก incident:

Problem: User เห็น 504 timeout

Step 1: Get traceId
- User report → X-Trace-Id ใน response, หรือ
- Search log by user_id + timestamp

Step 2: Open trace in Tempo
- See span tree

Step 3: Identify culprit
- Find span ที่ duration ใหญ่สุด หรือ status = error
- ลูกที่ first throw

Step 4: Drill down
- Click span → see attributes
- Click "Logs for this span" → see related log
- Read DB query, external API URL, etc.

Step 5: Fix
- เพิ่ม timeout, index, cache, circuit breaker, ...

Step 6: Verify
- Re-run → new trace should be fast
- Setup alert ถ้า recur

20. Profile-Guided Optimization

Tracing บอกว่า function ไหนช้า — แต่ไม่บอกทำไม

→ Add profile (CPU / memory flame graph)

Span: orderProcessor.process — 200ms

Profile (Pyroscope): 
   - 80% CPU in JSON serialization
   - 15% in DB query
   - 5% in app code

→ Optimize JSON serialization first

Tools: Pyroscope, Parca, Grafana Cloud Profiling


21. ⚠️ Common Pitfalls

รวมข้อผิดพลาดเรื่อง tracing ที่พบบ่อยพร้อมวิธีที่ถูกต้อง — เช่น sample 100% ใน prod (เปลือง), สร้าง span ทุก method (flood), ใส่ข้อมูลลับใน attribute (leak), ลืม end span (memory leak) ใช้ตารางนี้เป็น checklist ก่อน deploy tracing จริง:

100% sampling in prod1-10% head + tail-based for error/slow
Manual span everywhereuse auto-instrumentation
Trace each util methodonly meaningful operation
Sensitive data in attromit or mask
Forget to end spantry-finally or makeCurrent scope
Async + lost contextTaskDecorator
Custom protocol (not HTTP/gRPC)manual propagate context
No trace + log correlationuse OTel + MDC integration

22. Roadmap

ถ้าเริ่มจากศูนย์ ควรสร้าง tracing เป็นลำดับ — เริ่มจาก setup + auto-instrument service เดียว, ขยายเป็น cross-service, เพิ่ม sampling, แล้ว integrate กับ log/metric สุดท้ายค่อยทำ custom span + profile แผนนี้ทำให้เห็นผลเร็วก่อนลงลึก:

Week 1: Setup
- OpenTelemetry + Tempo (or Jaeger)
- Auto-instrumentation
- Single service traces

Week 2: Cross-service
- Multi-service propagation
- Trace your whole flow

Week 3: Sampling
- Head-based 10%
- Tail-based for error/slow

Week 4: Integration
- Trace + log correlation
- Exemplars (trace + metric)

Month 2:
- Custom spans for business operations
- Profile-guided optimization
- Trace-based SLO measurement

23. Real-world Example

ตัวอย่าง trace จริงของ checkout ที่แสดงพลังของ tracing — เห็น span tree ทั้งหมดพร้อม duration ของแต่ละ hop ทำให้ระบุได้ทันทีว่า payment service ช้าเพราะรอ Stripe API (580ms) ซึ่งเป็นจุดที่ log/metric แยกกันมองไม่เห็น:

⚠️ = span ที่ช้าผิดปกติ (warning emoji ใช้บอก) — payment.charge — 600ms ⚠️ เป็น bottleneck ของ trace นี้ ส่วน auth.validate — 5ms / response.serialize — 5ms เป็น span เร็วปกติ

Issue: Stripe API slow (580ms) Action:

  • Show user "Processing..." better
  • Async webhook for confirmation
  • Stripe retry config

24. Checkpoint

ลองทำ checkpoint เหล่านี้เพื่อพิสูจน์ว่าเข้าใจ tracing จริง — ไล่จาก trace service เดียว, cross-service, เชื่อม log+trace, ไปจนถึงจับ N+1 ด้วย trace แต่ละข้อต่อยอดจากข้อก่อนหน้า ทำครบแล้วจะใช้ tracing debug ระบบจริงได้:

🛠️ Checkpoint 3.1 — Single Service Trace

📋 ต้องมีก่อน:Spring Boot app ที่รันได้ (ดู Spring Boot บท 1) · ✅ Docker + docker compose สำหรับรัน Tempo + Grafana (ดู Docker บท 0) · ✅ dependency OTel ใน §4 ของบทนี้

  • Setup Spring Boot + OpenTelemetry + Tempo
  • Send request → see trace in Grafana

🛠️ Checkpoint 3.2 — Cross Service

  • 2 services (A calls B)
  • Verify trace shows both services in 1 view
  • Add custom span ใน B

🛠️ Checkpoint 3.3 — Log + Trace Correlation

  • Configure log to include traceId
  • Click traceId in Loki → jump to trace
  • Click span in Tempo → jump to logs

🛠️ Checkpoint 3.4 — N+1 Trace

  • Reproduce N+1 query (e.g., load posts + lazy comments)
  • See in trace — 100 DB span
  • Fix with JOIN FETCH
  • Re-trace — 1 span

🛠️ Checkpoint 3.5 — Tail Sampling

  • Configure OTel Collector with tail_sampling
  • Send 100 request (95 normal + 5 slow)
    • วิธีทำ "slow request": ใส่ Thread.sleep(2000) ใน controller สำหรับ 5 request แรก หรือใช้ k6/JMeter load test (script ให้ 5% ของ request hit endpoint ที่ delay)
  • Verify all 5 slow + sample of 95 normal kept

25. คำศัพท์บทนี้ (Glossary)

คำความหมาย
traceเส้นทางทั้งหมดของ 1 request ข้ามทุก service
spanงานหนึ่งชิ้นใน trace (เช่น 1 DB query, 1 HTTP call) — span ต่อกันเป็นต้นไม้
traceId / spanIdid ของ trace ทั้งเส้น / id ของ span แต่ละอัน
context propagationการส่งต่อ trace context ข้าม service ผ่าน HTTP header (traceparent)
OTLPOpenTelemetry Protocol — โปรโตคอลกลางที่ OTel ใช้ส่ง telemetry
head-based / tail-based samplingตัดสินใจ sample ตอนเริ่ม (head) / ตอนจบ trace (tail — เก็บ error/slow ได้)
exemplarจุดตัวอย่างใน metric ที่แนบ traceId — คลิกแล้วกระโดดจาก metric ไป trace
semantic conventionsชื่อ attribute มาตรฐานของ OTel (เช่น http.method) ที่ tool รู้จัก
Tempo / Jaegerbackend เก็บ + ดู trace (Tempo ของ Grafana, เก็บบน object storage = ถูก)

26. สรุปบท

Distributed tracing = visualize 1 request ผ่าน N service
Trace = root span + child spans with parent-child relationship
W3C Trace Context (traceparent header) = standard
OpenTelemetry = vendor-neutral instrumentation
Auto-instrumentation = ครอบคลุม HTTP, JDBC, Redis, Kafka, ...
Manual span สำหรับ business operation สำคัญ
Sampling: head-based (simple) + tail-based (smart, keep error/slow)
Tempo / Jaeger = storage. Grafana = visualization
Trace + log correlation via traceId in MDC
Exemplars = jump from metric → trace
✅ Async + trace context → TaskDecorator
✅ Add profile สำหรับ "ทำไม span นี้ช้า"


← บทที่ 2 | บทที่ 4 → Dashboard Design