Distributed Tracing #
Ketika sebuah request lambat di aplikasi monolith, kamu tinggal cek profiler atau log satu proses. Tapi ketika request melewati 10 microservice sebelum sampai ke pengguna, log dari satu service saja tidak cukup untuk menemukan bottleneck. Distributed tracing memungkinkan kamu melihat perjalanan lengkap sebuah request — dari entry point sampai database query terakhir — lengkap dengan durasi setiap langkah, sehingga bottleneck bisa diidentifikasi dengan tepat.
Konsep Dasar: Trace dan Span #
Sebuah request masuk ke API Gateway:
Trace: satu request end-to-end (satu trace ID yang sama di semua service)
Trace ID: 4bf92f3577b34da6
Spans (langkah-langkah dalam trace):
├── [Span 1] api-gateway: route_request 0ms - 5ms (5ms)
│ ├── [Span 2] auth-service: validate_token 2ms - 8ms (6ms)
│ └── [Span 3] api-server: handle_request 5ms - 95ms (90ms)
│ ├── [Span 4] user-service: get_user 10ms - 30ms (20ms)
│ │ └── [Span 5] db: SELECT users 12ms - 28ms (16ms) ← bottleneck
│ └── [Span 6] order-service: get_orders 30ms - 90ms (60ms) ← bottleneck
│ └── [Span 7] db: SELECT orders 31ms - 88ms (57ms) ← query lambat!
Total: 95ms
Bottleneck: order-service database query (57ms dari 95ms total)
OpenTelemetry: Standar Instrumentasi #
OpenTelemetry (OTel) adalah standar terbuka untuk instrumentasi yang didukung semua major vendor (Jaeger, Zipkin, Datadog, Grafana, dll). Instrumentasi sekali, kirim ke backend manapun.
# Python: instrumentasi dengan OpenTelemetry
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
# Setup provider
provider = TracerProvider()
exporter = OTLPSpanExporter(endpoint="http://otel-collector:4317")
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
# Auto-instrumentasi framework (tidak perlu modifikasi kode bisnis)
FastAPIInstrumentor.instrument_app(app) # otomatis trace semua HTTP handler
HTTPXClientInstrumentor.instrument() # otomatis trace semua outbound HTTP
SQLAlchemyInstrumentor.instrument() # otomatis trace semua query database
# Manual span untuk business logic yang perlu di-trace
tracer = trace.get_tracer(__name__)
def process_payment(order_id: str, amount: float):
with tracer.start_as_current_span("process_payment") as span:
span.set_attribute("order.id", order_id)
span.set_attribute("payment.amount", amount)
span.set_attribute("payment.currency", "USD")
try:
result = payment_gateway.charge(amount)
span.set_attribute("payment.status", "success")
return result
except PaymentError as e:
span.set_status(trace.StatusCode.ERROR, str(e))
span.record_exception(e)
raise
// Go: OpenTelemetry dengan auto-instrumentasi
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
// Wrap HTTP handler
http.Handle("/api/orders", otelhttp.NewHandler(orderHandler, "handle_order"))
// Manual span
tracer := otel.Tracer("order-service")
ctx, span := tracer.Start(ctx, "fetch_inventory")
defer span.End()
span.SetAttributes(attribute.String("product.id", productID))
OpenTelemetry Collector #
OTel Collector adalah komponen yang menerima, memproses, dan mengeksport telemetry data. Ia berjalan sebagai Deployment atau DaemonSet di cluster:
# OTel Collector sebagai DaemonSet (satu per node)
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: otel-collector
namespace: monitoring
spec:
template:
spec:
containers:
- name: otel-collector
image: otel/opentelemetry-collector-contrib:0.90.0
ports:
- containerPort: 4317 # gRPC receiver
- containerPort: 4318 # HTTP receiver
volumeMounts:
- name: config
mountPath: /etc/otelcol/
volumes:
- name: config
configMap:
name: otel-collector-config
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch: # batch sebelum export untuk efisiensi
timeout: 5s
memory_limiter:
limit_mib: 512
resource: # tambah metadata Kubernetes ke semua span
attributes:
- action: insert
key: k8s.cluster.name
value: "production"
exporters:
otlp: # kirim ke Jaeger atau Tempo
endpoint: "jaeger-collector.monitoring:4317"
tls:
insecure: true
logging: # debug: print ke stdout
verbosity: normal
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch, resource]
exporters: [otlp]
Backend: Jaeger dan Grafana Tempo #
Jaeger:
✓ UI yang lengkap untuk visualisasi trace
✓ Search berdasarkan service, operation, tag, duration
✓ Dependency graph antar service
✓ Mature, battle-tested di production skala besar
Storage: Elasticsearch atau Cassandra untuk produksi
Grafana Tempo:
✓ Integrasi seamless dengan Grafana dashboard
✓ TraceQL untuk query yang powerful
✓ Tidak memerlukan index (cost-efficient)
✓ Integrasi dengan Loki (log-to-trace linking) dan Prometheus (exemplar)
Storage: S3, GCS, atau Azure Blob
Cocok jika sudah menggunakan Grafana stack
Context Propagation #
Untuk distributed tracing berfungsi, setiap service harus meneruskan trace context ke service berikutnya via HTTP headers:
Trace context propagation:
Service A buat request ke Service B:
GET /api/products HTTP/1.1
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
tracestate: rojo=00f067aa0ba902b7
Service B membaca header ini dan meneruskan ke Service C:
→ Semua span terhubung dalam satu trace
OTel SDK secara otomatis melakukan propagation jika menggunakan instrumentasi HTTP client yang sudah di-instrument. Tidak perlu kode tambahan.
Sampling Strategy #
Tracing setiap request di sistem high-throughput bisa sangat mahal. Sampling mengontrol berapa persen trace yang disimpan:
from opentelemetry.sdk.trace.sampling import (
TraceIdRatioBased,
ParentBased,
ALWAYS_ON
)
# Head-based sampling: keputusan dibuat saat request dimulai
# Simpan 10% dari semua trace
sampler = ParentBased(root=TraceIdRatioBased(0.1))
# Tail-based sampling (di OTel Collector): keputusan dibuat setelah selesai
# Selalu simpan trace dengan error, sample sisanya
# Konfigurasi di OTel Collector:
# Tail-based sampling di OTel Collector
processors:
tail_sampling:
decision_wait: 10s # tunggu 10 detik sebelum memutuskan
policies:
- name: error-traces
type: status_code
status_code: {status_codes: [ERROR]} # selalu simpan error
- name: slow-traces
type: latency
latency: {threshold_ms: 1000} # selalu simpan yang > 1 detik
- name: rate-limiting
type: rate_limiting
rate_limiting: {spans_per_second: 100} # max 100 spans/detik untuk sisanya
Ringkasan #
- OpenTelemetry adalah standar, bukan vendor — instrumentasi sekali dengan OTel, kirim ke Jaeger, Tempo, Datadog, atau backend lain tanpa ubah kode aplikasi.
- Auto-instrumentasi untuk HTTP dan database —
FastAPIInstrumentor,SQLAlchemyInstrumentor,HTTPXClientInstrumentormenangani instrumentasi framework tanpa ubah kode bisnis.- OTel Collector sebagai buffer dan router — jangan kirim langsung dari aplikasi ke backend tracing; gunakan Collector untuk batch, filter, dan route ke berbagai backend.
- Context propagation otomatis dengan OTel SDK — header
traceparentdi-inject dan dibaca otomatis oleh instrumentasi HTTP; tidak perlu kode manual untuk propagation.- Tail-based sampling untuk efficiency — selalu simpan trace dengan error dan trace lambat; sample sisanya secara acak; jauh lebih berguna dari head-based sampling yang acak.
- Grafana Tempo + Loki + Prometheus = satu dashboard — dari trace bisa jump ke log di waktu yang sama; dari metric anomaly bisa jump ke trace yang relevan via exemplar.