Distributed Tracing #

Dalam arsitektur monolitik tradisional, melacak kelambatan request (latency bottlenecks) relatif mudah karena seluruh eksekusi kode berjalan di dalam satu proses memori pada satu mesin host. Kita cukup menempelkan profiler kode atau membaca log kronologis untuk menemukan fungsi mana yang berjalan lambat. Namun, di dalam arsitektur mikroservis (microservices) yang berjalan di Kubernetes, satu request dari pengguna dapat melintasi belasan layanan berbeda, berpindah dari API Gateway ke autentikasi gRPC, memicu pemrosesan asinkron di broker pesan (Kafka/RabbitMQ), dan akhirnya menyentuh berbagai basis data terdistribusi. Tanpa adanya sistem pelacakan terdistribusi (Distributed Tracing), mendiagnosis alasan mengapa sebuah request berjalan lambat menjadi hampir mustahil. Kita memerlukan mekanisme pelacakan terpadu yang memetakan seluruh perjalanan request secara end-to-end melintasi batas-batas proses jaringan kontainer.


Konsep Dasar: Anatomi Trace dan Span #

Distributed tracing bekerja dengan menyisipkan pengenal unik (unique identifiers) ke dalam setiap aliran request. Ada dua istilah fundamental yang harus kita pahami:

  • Trace: Representasi end-to-end dari perjalanan satu request lengkap dari awal hingga akhir. Satu trace diidentifikasi oleh satu Trace ID unik yang sama di seluruh layanan.
  • Span: Unit kerja terkecil yang mewakili satu langkah atau operasi spesifik di dalam trace (misalnya kueri SQL, panggilan HTTP client, atau validasi token). Setiap span memiliki nama, stempel waktu mulai/selesai, atribut (metadata), dan Span ID unik.

Diagram Sekuens dan Propagasi Konteks (Context Propagation) #

Untuk menyatukan span-span dari berbagai kontainer aplikasi menjadi satu garis waktu trace yang utuh, kita harus meneruskan metadata trace melalui batas jaringan. Proses meneruskan metadata ini disebut dengan Context Propagation.

sequenceDiagram
    autonumber
    actor User as Client (Web Browser)
    participant Gateway as API Gateway (Ingress)
    participant Auth as Auth-Service (gRPC)
    participant Order as Order-Service (HTTP)
    participant DB as Postgres Database
    
    User->>Gateway: HTTP GET /api/orders (Tanpa Trace ID)
    Note over Gateway: "Inisialisasi Trace:<br/>Trace ID: 4bf92f3577b34da6<br/>Parent Span ID: 00f067aa0ba902b7"
    
    Gateway->>Auth: gRPC validateToken() + W3C Metadata
    Note over Auth: "Ekstraksi Context gRPC<br/>Span Anak (Child Span 1.1)"
    Auth-->>Gateway: HTTP 200 (Token Valid)
    
    Gateway->>Order: HTTP GET /order/details + traceparent Header
    Note over Order: "Ekstraksi HTTP Header<br/>Span Anak (Child Span 1.2)"
    
    Order->>DB: SQL SELECT * FROM orders
    Note over DB: "Span Database (Child Span 1.2.1)"
    DB-->>Order: Kembalikan Data Query
    
    Order-->>Gateway: Kembalikan Payload Detail Order
    Gateway-->>User: HTTP 200 (Success)

Standar propagasi kontekstual yang paling umum digunakan saat ini adalah W3C Trace Context. Standar ini mendefinisikan header HTTP traceparent dengan format sebagai berikut:

Format Header W3C traceparent:
00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
│  │                                └─ Span ID (8-byte hex)
│  └─ Trace ID (16-byte hex)
└─ Versi spesifikasi (00)

Instrumentasi Aplikasi Menggunakan OpenTelemetry #

OpenTelemetry (OTel) adalah standar terbuka di bawah Cloud Native Computing Foundation (CNCF) yang menyediakan SDK dan API seragam untuk mengumpulkan data telemetri (traces, metrics, logs). Dengan OTel, kita melakukan instrumentasi kode sekali saja, dan dapat mengirim data tersebut ke backend visualisasi mana pun (seperti Jaeger, Grafana Tempo, atau Datadog) tanpa memodifikasi kode aplikasi.

Berikut adalah perbandingan antara penanganan asinkron yang memutus trace context dengan implementasi context propagation yang aman dalam bahasa Go:

// ANTI-PATTERN: Menjalankan goroutine asinkron tanpa meneruskan context.
// Hal ini menyebabkan Trace ID terputus, sehingga operasi internal goroutine tidak tercatat dalam trace induk.
func HandleOrder(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    go func() {
        // JANGAN: Goroutine kehilangan context induk
        db.Query("SELECT * FROM inventory") // Exec query berjalan tanpa Trace ID
    }()
}

// ==============================================================================
// BENAR: Meneruskan Context ke Goroutine Asinkron untuk Menjaga Aliran Trace.
func HandleOrder(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    
    // Tarik span aktif saat ini dari request context
    parentSpan := trace.SpanFromContext(ctx)
    
    // Salin trace context ke goroutine asinkron secara aman
    go func(asyncCtx context.Context) {
        // Buat span baru di dalam goroutine asinkron dengan parent span yang tepat
        tracer := otel.Tracer("order-service")
        childCtx, span := tracer.Start(asyncCtx, "async_inventory_check")
        defer span.End()
        
        // Operasi database kini terikat ke Trace ID yang sama
        db.QueryContext(childCtx, "SELECT * FROM inventory")
    }(trace.ContextWithSpan(context.Background(), parentSpan))
}

Kode Contoh Instrumentasi Python (FastAPI & OTel SDK) #

Berikut adalah contoh lengkap cara melakukan inisialisasi OTel Tracer Provider dan mengekspor span secara otomatis melalui protokol OTLP/gRPC di Python:

import time
from fastapi import FastAPI
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

# 1. Inisialisasi SDK Tracing Provider
provider = TracerProvider()

# 2. Kirim data trace ke OTel Collector lokal melalui port gRPC 4317
otlp_exporter = OTLPSpanExporter(endpoint="http://otel-collector.monitoring.svc:4317", insecure=True)

# 3. Gunakan BatchSpanProcessor untuk efisiensi performa (jangan kirim span satu per satu secara sinkron)
provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
trace.set_tracer_provider(provider)

# 4. Inisialisasi Tracer untuk instrumentasi manual
tracer = trace.get_tracer("order-service")

app = FastAPI()

@app.get("/process-order/{order_id}")
def process_order(order_id: str):
    # Buat span manual untuk business logic kritis
    with tracer.start_as_current_span("calculate_payment") as span:
        span.set_attribute("order.id", order_id)
        time.sleep(0.5) # Simulasi delay
        span.set_attribute("payment.status", "approved")
    return {"status": "success"}

# 5. Pasang Auto-instrumentasi FastAPI
FastAPIInstrumentor.instrument_app(app)

Arsitektur dan Deployment OpenTelemetry Collector #

OpenTelemetry Collector adalah komponen perantara (proxy agent) yang bertugas menerima, memproses, menyaring, dan mengekspor data telemetri. Kita tidak boleh mengirimkan data traces langsung dari aplikasi ke backend penyimpanan (seperti Jaeger/Tempo) karena:

  • Aplikasi akan terbebani oleh logika antrean dan retry pengiriman log.
  • Sulit melakukan penyaringan atau pengayaan metadata secara terpusat.

Di dalam Kubernetes, OTel Collector dideploy sebagai DaemonSet di setiap node worker (untuk pemrosesan lokal berlatensi rendah) atau sebagai Deployment terpusat (untuk agregasi massal).

Manifest Deployment OTel Collector #

apiVersion: apps/v1
kind: Deployment
metadata:
  name: otel-collector
  namespace: monitoring
spec:
  replicas: 2
  selector:
    matchLabels:
      app: otel-collector
  template:
    metadata:
      labels:
        app: otel-collector
    spec:
      containers:
      - name: otel-collector
        image: otel/opentelemetry-collector-contrib:0.95.0
        ports:
        - containerPort: 4317 # Port OTLP/gRPC receiver
        - containerPort: 4318 # Port OTLP/HTTP receiver
        volumeMounts:
        - name: config-volume
          mountPath: /etc/otelcol
        resources:
          limits:
            cpu: "500m"
            memory: "1Gi"
          requests:
            cpu: "200m"
            memory: "512Mi"
      volumes:
      - name: config-volume
        configMap:
          name: otel-collector-config

Konfigurasi Pipeline OTel Collector (ConfigMap) #

Konfigurasi OTel Collector menggunakan struktur pipeline yang didefinisikan dalam tiga bagian utama: receivers (penerima), processors (pemroses/filter), dan exporters (pengirim).

apiVersion: v1
kind: ConfigMap
metadata:
  name: otel-collector-config
  namespace: monitoring
data:
  otel-collector-config.yaml: |
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
          http:
            endpoint: 0.0.0.0:4318

    processors:
      # 1. Batasi memori collector agar tidak memicu OOMKilled di Kubernetes
      memory_limiter:
        check_interval: 1s
        limit_percentage: 80
        spike_limit_percentage: 20

      # 2. Gabungkan span ke dalam batch untuk efisiensi jaringan
      batch:
        send_batch_size: 1024
        timeout: 5s

      # 3. Sisipkan metadata kluster ke setiap span
      resourcedetection:
        detectors: [env, system]

    exporters:
      # Kirim trace ke backend Grafana Tempo
      otlp:
        endpoint: "tempo-distributor.monitoring.svc.cluster.local:4317"
        tls:
          insecure: true

    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [memory_limiter, batch, resourcedetection]
          exporters: [otlp]    

Strategi Pengambilan Sampel (Sampling Strategies) #

Dalam sistem bertrafik tinggi (misalnya memproses 20.000 request per detik), merekam 100% data trace akan membebani bandwidth jaringan internal kluster dan membutuhkan kapasitas penyimpanan (storage) yang sangat mahal. Oleh karena itu, kita harus menerapkan teknik Sampling.

1. Head-Based Sampling #

Keputusan untuk merekam atau mengabaikan trace diambil di awal siklus request (biasanya di API Gateway atau SDK client).

  • Kelebihan: Sangat hemat CPU dan bandwidth karena trace yang tidak terpilih langsung diabaikan sejak hulu.
  • Kekurangan: Kita bisa kehilangan data berharga secara acak. Jika terjadi error di pertengahan request, ada kemungkinan request tersebut tidak terekam karena tidak lolos sampling di awal.
# Konfigurasi Head-Based Sampler di SDK: Simpan hanya 10% request secara acak
from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased
sampler = ParentBased(root=TraceIdRatioBased(0.10))

2. Tail-Based Sampling #

Keputusan diambil setelah request selesai diproses secara penuh. OTel Collector mengumpulkan seluruh span dari satu trace terlebih dahulu di memori buffer, menganalisis hasilnya, lalu mengambil keputusan.

  • Kelebihan: Sangat presisi. Kita dapat membuat kebijakan: “Selalu simpan 100% trace yang menghasilkan error status 5xx atau memakan waktu latensi > 2 detik, dan hanya simpan 1% request yang sukses.”
  • Kekurangan: Membutuhkan alokasi memori RAM yang cukup besar pada OTel Collector karena harus menampung data trace sementara sebelum diputuskan.
# Tambahkan konfigurasi tail_sampling pada processors di OTel Collector
processors:
  tail_sampling:
    decision_wait: 10s # Tunggu selama 10 detik agar seluruh span terkumpul
    num_traces: 10000
    expected_new_traces_per_sec: 2000
    policies:
      # Aturan A: Selalu simpan jika status span bernilai ERROR
      - name: error-policy
        type: status_code
        status_code: {status_codes: [ERROR]}
      # Aturan B: Selalu simpan jika durasi request > 1.5 detik
      - name: latency-policy
        type: latency
        latency: {threshold_ms: 1500}
      # Aturan C: Simpan 5% request normal sisanya secara acak
      - name: probabilistic-policy
        type: probabilistic
        probabilistic: {sampling_percentage: 5.0}

Integrasi Tracing dengan Broker Pesan (Kafka/RabbitMQ) #

Kesalahan umum lainnya adalah terputusnya Trace ID saat request masuk ke dalam sistem antrean broker pesan (message queue). Distributed tracing melintasi broker pesan memerlukan penyuntikan konteks ke dalam metadata header pesan.

Berikut adalah contoh cara menyuntikkan trace context ke dalam pesan RabbitMQ (AMQP) menggunakan bahasa Go:

// Mengirim pesan RabbitMQ dengan Trace Context
func PublishOrderEvent(ctx context.Context, ch *amqp.Channel, event OrderEvent) {
    // Buat span khusus untuk aktivitas penerbitan event
    tracer := otel.Tracer("order-service")
    ctx, span := tracer.Start(ctx, "publish_order_event")
    defer span.End()

    // Map penampung header RabbitMQ
    headers := amqp.Table{}

    // Suntikkan traceparent context dari context.Context ke headers map
    otel.GetTextMapPropagator().Inject(ctx, propagation.MapCarrier(headers))

    body, _ := json.Marshal(event)

    // Kirim pesan dengan headers yang telah diperkaya Trace ID
    ch.Publish(
        "",
        "order-queue",
        false,
        false,
        amqp.Publishing{
            Headers:     headers, // Wajib menyertakan header propagasi
            ContentType: "application/json",
            Body:        body,
        },
    )
}

Di sisi konsumen (consumer), aplikasi penerima harus membaca header tersebut dan mengekstraknya menggunakan otel.GetTextMapPropagator().Extract(ctx, propagation.MapCarrier(headers)) sebelum memulai pemrosesan baru agar Trace ID tetap terhubung secara linier.


Anti-Pattern dalam Praktik Distributed Tracing #

Hindari kebiasaan buruk berikut dalam merancang tracing kluster:

1. Polusi Manual Span (Manual Span Pollution) #

Membuat span manual untuk setiap fungsi internal kecil (seperti fungsi pemformatan string atau perkalian matematika). Ini membanjiri memori aplikasi, menghasilkan grafik trace yang sangat padat dan tidak terbaca di Jaeger, serta membebani CPU secara sia-sia. Batasi span manual hanya untuk operasi jaringan, kueri database, atau alur logika bisnis kritis.

2. Membiarkan Kegagalan (Exceptions) Senyap Tanpa Penandaan #

Menangkap error di dalam kode bisnis menggunakan blok try-catch namun tidak mendaftarkan status error tersebut ke dalam span aktif. Akibatnya, visualisasi trace di dashboard tetap terlihat berwarna hijau (sukses), menyulitkan tim triage mendeteksi lokasi kegagalan kode.

# ANTI-PATTERN: Menangkap error tanpa menandai status span.
with tracer.start_as_current_span("process_payment") as span:
    try:
        charge_credit_card()
    except Exception as e:
        # JANGAN: Tracing akan menganggap transaksi ini sukses (hijau)
        handle_error_gracefully(e)

# ==============================================================================
# BENAR: Daftarkan exception dan set status span menjadi ERROR.
with tracer.start_as_current_span("process_payment") as span:
    try:
        charge_credit_card()
    except Exception as e:
        span.set_status(trace.StatusCode.ERROR, str(e))
        span.record_exception(e) # Catat stacktrace error di dalam metadata span
        raise e

Checklist Implementasi Distributed Tracing #

Pastikan seluruh item pemeriksaan berikut telah terpenuhi sebelum meluncurkan sistem distributed tracing ke produksi:

INSTRUMENTASI KODE & PROPAGASI:
  □ Aplikasi diinstrumentasikan menggunakan standar OpenTelemetry API & SDK.
  □ Menerapkan autoinstrumentasi untuk framework HTTP (FastAPI, Gin, Express) dan DB driver (SQLAlchemy, Gorm).
  □ Seluruh goroutine/async thread meneruskan context induk secara aman (tidak ada lost context).
  □ Header W3C 'traceparent' disisipkan pada setiap outgoing HTTP client call.
  □ Kredensial context disisipkan ke dalam record headers broker pesan (Kafka/RabbitMQ).
  □ Seluruh block error handling mendaftarkan exception dan menandai status span sebagai ERROR.

KONFIGURASI OTEL COLLECTOR & BACKEND:
  □ OTel Collector dideploy menggunakan limit resource memory_limiter yang tepat.
  □ OTel Collector menggunakan Batch Processor sebelum mengirimkan span ke database.
  □ Menerapkan strategi tail-based sampling untuk menyimpan 100% error dan 5% request normal.
  □ Eksplorasi jump logs-to-traces dan traces-to-metrics dikonfigurasi di tingkat Grafana dashboard.

Ringkasan #

  • OpenTelemetry adalah Standar Industri — OTel memisahkan kode instrumentasi dari vendor backend; kita dapat memigrasikan backend visualisasi tanpa menyentuh kode aplikasi.
  • Context Propagation Menyambung Span — Pastikan header traceparent dikirim melintasi batas jaringan HTTP, gRPC, maupun antrean antrean pesan untuk menjaga Trace ID tetap terhubung.
  • Batasi Pembuatan Span Manual — Fokuskan pelacakan span pada operasi I/O jaringan, kueri database, dan antrean pesan; polusi span manual membuat visualisasi menjadi tidak terbaca.
  • Gunakan Tail-Based Sampling di Kolektor — Simpan hanya data penting (error dan latensi lambat) secara penuh untuk menghemat kapasitas penyimpanan tanpa kehilangan visibilitas kegagalan.
  • Daftarkan Exception ke Span — Selalu panggil fungsi record_exception dan set status span ke ERROR saat menangkap kegagalan kode agar visualisasi trace di dashboard berwarna merah.
  • Gunakan OTel Collector sebagai Buffer — Deploy OTel Collector di dalam kluster untuk melakukan batching dan pembersihan data sebelum traces disimpan ke database permanen.

← Sebelumnya: Alerting   Berikutnya: Grafana Dashboard →

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