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 databaseFastAPIInstrumentor, SQLAlchemyInstrumentor, HTTPXClientInstrumentor menangani 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 traceparent di-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.

← Sebelumnya: Alerting   Berikutnya: Grafana Dashboard →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact