โหมดมืด
บทที่ 3 — Distributed Tracing
หลังจบบท คุณจะ:
- เข้าใจ 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 นี้ช้า?" → ตอบยาก โดยไม่มี traceExample 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 spanIdW3C 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); // NOOpenTelemetry มี 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
| Tool | Notes |
|---|---|
| Tempo ⭐ | Grafana — cheap, S3 storage |
| Jaeger | CNCF, popular |
| Zipkin | Twitter, older |
| Datadog APM | paid |
| Honeycomb | high-cardinality first |
| AWS X-Ray | AWS-native |
| OpenTelemetry Demo | reference 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.ymlyaml
# 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 onlyContext 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 เอง · ต้องมีObservationRegistrybean (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.jaryaml
# กรณีใช้ 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 ลึก ๆ
10. Trace Search
เก็บ 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: abc123By 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 + contextPattern 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 FETCHPattern 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 breakerPattern 5: Hidden Dependency
Trace แสดง span "external-api.example.com" — แต่ทีมไม่รู้ว่าใช้!
→ Audit dependency → remove or monitor12. 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 TempoTempo → 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_agentHTTP Client
operationName: "POST"
attributes:
http.request.method: "POST"
url.full: "https://payment-api/charges" # เดิม http.url
http.response.status_code: 200Database
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.operationMessaging
operationName: "process order-events"
attributes:
messaging.system: "kafka"
messaging.destination.name: "order-events" # เดิม messaging.destination
messaging.operation.type: "receive" # เดิม messaging.operation15. 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() → leakjava
// ✅ 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 ถ้า recur20. 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 firstTools: 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 prod | 1-10% head + tail-based for error/slow |
| Manual span everywhere | use auto-instrumentation |
| Trace each util method | only meaningful operation |
| Sensitive data in attr | omit or mask |
| Forget to end span | try-finally or makeCurrent scope |
| Async + lost context | TaskDecorator |
| Custom protocol (not HTTP/gRPC) | manual propagate context |
| No trace + log correlation | use 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 measurement23. 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)
- วิธีทำ "slow request": ใส่
- 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 / spanId | id ของ trace ทั้งเส้น / id ของ span แต่ละอัน |
| context propagation | การส่งต่อ trace context ข้าม service ผ่าน HTTP header (traceparent) |
| OTLP | OpenTelemetry 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 / Jaeger | backend เก็บ + ดู 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 นี้ช้า"