# Distributed Tracing di Microservices: Menelusuri Jejak Request dengan OpenTelemetry

> Diterbitkan oleh Literally Web ID pada 21 Oct 2026
> Sumber: https://literally.web.id/articles/distributed-tracing-di-microservices-menelusuri-jejak-request-dengan-opentelemetry

---

## 1. Alarm Tengah Malam: Saat Layanan Produksi Mulai Tumbang

Pukul 02:14 WIB. Ponsel saya berdering keras dengan nada PagerDuty yang selalu sukses memicu adrenalin. Dashboard Grafana memperlihatkan pemandangan yang membuat perut mual: latensi *endpoint* utama melonjak dari rata-rata 120 milidetik menjadi 8,4 detik, sementara persentase *error 504 Gateway Timeout* merayap naik menembus angka 14%. Sebagai Senior Staff Engineer yang malam itu bertugas sebagai *on-call Commander*, saya langsung menyalakan laptop, membuka terminal, dan bersiap menghadapi badai.

Sistem kami berjalan di atas arsitektur *microservices* berbasis Go dan Node.js yang dibungkus di dalam klaster Kubernetes, di-routing melalui Nginx *ingress controller*, dilindungi oleh Cloudflare di lapisan edge, serta mengandalkan Redis dan MySQL sebagai dapur penyimpanan data utama. Dengan puluhan layanan yang saling berkomunikasi secara asinkron maupun sinkron, mencari tahu layanan mana yang menjadi pemicu kemacetan ibarat mencari jarum di dalam tumpukan jerami digital.

> [!WARNING]
> Jangan pernah berasumsi layanan utama yang menerima *request* publik adalah sumber masalah. Dalam arsitektur terdistribusi, *frontend* atau API Gateway seringkali hanya menjadi korban yang menanggung beban timeout dari layanan *downstream* yang 'tertidur'.

Tanpa visibilitas lintas layanan yang akurat, tim kami sebelumnya hanya mengandalkan log teks mentah di Kibana. Namun, dalam situasi *high concurrency*, mencocokkan *Request ID* manual antar puluhan file log terasa mustahil dan membuang waktu emas pemulihan. Kita butuh cara pandang yang lebih holistik, sebuah peta navigasi yang mampu merekonstruksi perjalanan satu paket *request* HTTP secara utuh dari ujung Cloudflare hingga ke dasar tabel MySQL. Dan di sinilah investigasi mendalam kita dimulai dengan membongkar anatomi *context propagation* menggunakan OpenTelemetry.

## 2. Jejak Digital: Menelusuri Anomali Log & Lonjakan Metrik

Langkah pertama seorang detektif infrastruktur adalah mengamankan tempat kejadian perkara. Saya langsung melompat ke terminal untuk memeriksa status Nginx dan PHP-FPM/Go backend, serta memantau antrean *queue* Redis menggunakan perintah cepat:

```bash
# Memantau koneksi aktif pada Nginx ingress controller secara real-time
sudo netstat -an | grep :443 | awk '{print $6}' | sort | uniq -c | sort -nr

# Memeriksa latensi respons pada log akses Nginx dalam 5 menit terakhir
awk '($9 ~ /504/)' /var/log/nginx/access.log | tail -n 50
```

Hasilnya langsung memperlihatkan pola: lonjakan status `504` terkonsentrasi pada layanan `/api/v1/checkout`. Layanan ini memanggil tiga layanan terpisah secara berurutan: *User Service* (Go), *Inventory Service* (Node.js), dan *Payment Gateway Integration*. Namun, log dari masing-masing layanan ini bersih dari *error stack trace*. Mereka hanya mencatat log `processing request took 8000ms`, seolah-olah tidak ada yang rusak, hanya berjalan sangat lambat.

Di sinilah pentingnya **Anatomi Context Propagation**. OpenTelemetry bekerja dengan menyisipkan metadata khusus ke dalam *HTTP Headers* (biasanya menggunakan standar W3C Trace Context: `traceparent` dan `tracestate`). Ketika Nginx menerima *request* dari Cloudflare, sebuah *Trace ID* unik dibuat. ID ini wajib diteruskan ke seluruh *downstream services* agar setiap span (unit kerja individual) dapat diikat dalam satu pohon *trace* yang utuh.

Mari kita lihat bagaimana kita mengonfigurasi *context propagation* di sisi aplikasi Go agar *trace ID* tidak terputus di tengah jalan:

```go
package main

import (
	"context"
	"net/http"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/propagation"
)

// Middleware untuk mengekstrak Traceparent dari incoming HTTP header
func OTelMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Ekstrak context dari header yang dikirim oleh upstream (Nginx/Gateway)
		ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))
		
		tracer := otel.GetTracerProvider().Tracer("checkout-service")
		ctx, span := tracer.Start(ctx, "HTTP POST /api/v1/checkout")
		defer span.End()

		// Teruskan context yang sudah diperbarui ke handler berikutnya
		r = r.WithContext(ctx)
		next.ServeHTTP(w, r)
	})
}
```

Dengan menyisipkan *propagator* ini secara konsisten, kita tidak lagi meraba-raba. Kita bisa melihat dengan tepat di baris kode mana sebuah *request* tertahan. Dan penelusuran ini membawa kita langsung ke akar masalah yang tersembunyi di balik tumpukan kode yang tampak normal.

## 3. Membongkar Biang Kerok: Akar Masalah yang Sebenarnya

Setelah *tracing* diaktifkan dan dikirimkan ke *collector*, Jaeger dashboard memberikan pencerahan visual yang luar biasa terang. Garis waktu (timeline) *trace* menunjukkan bahwa 98% waktu latensi dihabiskan pada sebuah span bernama `SQL SELECT inventory_locks`. 

Usut punya usut, *Inventory Service* yang dibangun dengan Node.js melakukan *database query* berulang kali (kategori *N+1 query problem*) di dalam *loop* transaksi MySQL, ditambah lagi dengan ketiadaan indeks pada kolom `product_uuid` yang baru saja dirilis pada *deployment* fitur pukul 21.00 WIB tadi malam. Kombinasi maut antara *missing index* dan *connection pool exhaustion* di MySQL membuat setiap *request* harus mengantre menunggu giliran koneksi dibebaskan oleh *connection pool* Redis/MySQL.

> [!TIP]
> Gunakan OpenTelemetry Collector sebagai agen perantara (daemon set) di setiap node klaster Anda. Jangan pernah mengirimkan *trace* langsung dari aplikasi ke *backend storage* (seperti Jaeger/Prometheus) untuk menghindari lonjakan beban jaringan saat terjadi insiden trafik tinggi.

Berikut adalah contoh konfigurasi **Setup OpenTelemetry Collector** (`otel-collector-config.yaml`) yang kami gunakan untuk menyaring, memproses, dan mereduksi *data telemetry* sebelum masuk ke penyimpanan analitik:

```yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 1s
    send_batch_size: 1024
  memory_limiter:
    check_interval: 1s
    limit_percentage: 80
    spike_limit_percentage: 20

exporters:
  jaeger:
    endpoint: "jaeger-collector.observability.svc.cluster.local:14250"
    tls:
      insecure: true

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

Dengan arsitektur collector ini, data aman terkumpul tanpa membebani memori layanan aplikasi utama. Namun, diagnosis saja tidak cukup; kita harus segera memadamkan api sebelum bos besar terbangun karena komplain pelanggan.

## 4. Tindakan Darurat: Menghidupkan Kembali Layanan dalam Hitungan Menit

Situasi kritis menuntut tindakan cepat tanpa ragu. Berdasarkan hasil investigasi *distributed tracing* yang menunjukkan kegagalan pada *query* basis data, saya langsung mengeksekusi *runbook* darurat untuk memulihkan stabilitas sistem dalam waktu kurang dari 60 detik.

Berikut adalah rangkaian perintah darurat yang saya jalankan langsung di terminal produksi:

```bash
# 1. Segera buat indeks database secara manual untuk memotong waktu eksekusi query
mysql -h db.internal.production -u root -p'SecureRootPassword' -e "ALTER TABLE inventories ADD INDEX idx_product_uuid (product_uuid);" production_db

# 2. Lakukan graceful restart pada Deployment Inventory Service untuk membersihkan socket yang menggantung
kubectl rollout restart deployment/inventory-service -n production

# 3. Flush cache Redis yang berpotensi menyimpan data invalid lock
redis-cli -h redis.internal.production -a 'RedisSecretKey' FLUSHDB
```

> [!IMPORTANT]
> Perintah `ALTER TABLE` pada basis data produksi berukuran besar dapat mengunci tabel (*table lock*). Pastikan Anda menggunakan fitur *Online Schema Change* (seperti pt-online-schema-change atau gh-ost) jika tabel memiliki jutaan baris data, kecuali dalam kondisi darurat ekstrem di mana layanan sudah 100% *down*.

Seketika setelah indeks ditambahkan dan *pods* direstart, grafik latensi di Grafana terjun bebas dari 8,4 detik kembali normal ke angka 115 milidetik. *Error rate* langsung menyentuh 0%. Alarm PagerDuty berhenti berdering. Tarikan napas panjang memenuhi ruangan *war room* virtual kami.

## 5. Kebijakan Retrospektif: Menambal Celah agar Tak Pernah Terulang

Insiden pukul 02:00 pagi ini memberikan pelajaran mahal. Masalahnya bukan sekadar *missing index*, melainkan kurangnya standarisasi pengujian *load test* dan ketidaktahuan kita terhadap kesehatan rantai *microservices* sebelum rilis ke publik.

Sebagai langkah preventif jangka panjang, kami menerapkan kebijakan arsitektur defensif berikut:
1. **Wajib OpenTelemetry di Setiap Layanan Baru:** Tidak ada *service* baru yang boleh masuk ke *staging* apalagi produksi tanpa menyertakan inisialisasi OTel SDK dan *propagator* HTTP.
2. **Peningkatan CI/CD Pipeline Gate:** Menambahkan *static code analysis* khusus untuk mendeteksi potensi *N+1 query* pada ORM (seperti Prisma atau GORM) sebelum kode di-merge ke *branch* utama.
3. **Query Optimization Review:** Setiap perubahan skema basis data wajib melalui tinjauan dari *Database Administrator* (DBA) atau Senior Engineer, terlepas seberapa kecil perubahan tersebut.

Visualisasi yang tajam di Jaeger terbukti menjadi penyelamat malam itu. Mari kita intip bagaimana implementasi visualisasi ini diatur agar mudah diakses oleh seluruh anggota tim teknik:

```yaml
# Cuplikan konfigurasi deployment Jaeger Query di Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jaeger-query
  namespace: observability
spec:
  replicas: 2
  selector:
    matchLabels:
      app: jaeger-query
  template:
    metadata:
      labels:
        app: jaeger-query
    spec:
      containers:
      - name: query
        image: jaegertracing/jaeger-query:1.50
        env:
          - name: SPAN_STORAGE_TYPE
            value: elasticsearch
          - name: ES_SERVER_URLS
            value: http://elasticsearch.observability.svc.cluster.local:9200
        ports:
          - containerPort: 16686
            name: ui
```

Dengan antarmuka Jaeger UI di port `16686`, setiap engineer kini dapat melihat dengan mata kepala sendiri grafik air terjun (*waterfall chart*) dari setiap *request*, mengidentifikasi span mana yang memakan waktu paling lama, dan mendeteksi anomali sebelum berdampak buruk ke pengguna akhir.

## 6. Pasang Alarm: Otomasi Deteksi Dini untuk Tim On-Call

Agar kejadian serupa tidak lagi membuat kita terbangun tengah malam dalam keadaan panik, otomatisasi deteksi dini adalah harga mati. Kita tidak boleh menunggu pengguna melapor atau alarm PagerDuty berbunyi karena *service* sudah mati total.

Kami mengonfigurasi *alerting rule* berbasis Prometheus yang memantau durasi *trace span* secara proaktif menggunakan metrik OpenTelemetry yang diexport:

```yaml
groups:
- name: distributed-tracing-alerts
  rules:
  - alert: HighTraceLatency
    expr: histogram_quantile(0.99, sum(rate(otel_span_duration_milliseconds_bucket{service_name="checkout-service"}[5m])) by (le)) > 2000
    for: 2m
    labels:
      severity: critical
    annotations:
      summary: "Latensi tinggi terdeteksi pada checkout-service (P99 > 2 detik)"
      description: "Distribusi tracing menunjukkan adanya hambatan pada downstream service atau database query dalam 5 menit terakhir."

  - alert: TraceErrorRateSpike
    expr: sum(rate(otel_span_duration_milliseconds_count{status_code="STATUS_CODE_ERROR"}[5m])) / sum(rate(otel_span_duration_milliseconds_count[5m])) * 100 > 5
    for: 1m
    labels:
      severity: warning
    annotations:
      summary: "Lonjakan error trace melebihi 5%"
      description: "Periksa Jaeger dashboard untuk melihat jejak error pada transaksi microservices."

  - alert: OTelCollectorMemoryHigh
    expr: process_resident_memory_bytes{job="otel-collector"} / 1024 / 1024 > 1024
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "Penggunaan memori OpenTelemetry Collector melebihi 1GB"
      description: "Collector mungkin mengalami penumpukan antrean batch exporter."
```

Dengan aturan *alerting* yang presisi ini, sistem kini mampu mendeteksi gejala awal degradasi performa bahkan sebelum lonjakan *error* dirasakan oleh pelanggan setia kita. Pengalaman malam ini mengajarkan kita bahwa infrastruktur yang tangguh bukan yang tidak pernah gagal, melainkan yang tahu persis ke mana harus melihat saat kegagalan itu datang.