Jam tiga pagi, API kamu mendadak lambat. Kamu buka log, semua request tercatat 200 OK, tapi nggak ada satu baris pun yang menjelaskan request mana yang nyangkut dan di fungsi mana dia buang waktu. Tracing memperbaiki kebuntuan ini: alih-alih membaca puluhan baris log terpisah, kamu melihat satu request sebagai satu rangkaian langkah utuh, lengkap dengan durasi tiap langkah. Saya pakai FastAPI di sini, tapi pola yang sama berlaku untuk Flask, Django, atau service Node.js.
Log, metrik, dan trace: tiga alat dengan tugas berbeda
Log menceritakan apa yang terjadi di satu titik waktu — cocok buat debugging isu yang sudah kamu duga. Metrik memberi angka agregat: rata-rata latensi, jumlah error per menit. Tapi keduanya nggak bisa menjawab pertanyaan yang paling sering bikin lembur: request si user ini lewat jalur mana, dan di titik mana dia menghabiskan 400 ms? Trace menjawabnya. OpenTelemetry (OTel) adalah standar de facto untuk menghasilkan trace, dan dia bisa jalan berdampingan dengan Prometheus dan Grafana yang kamu pakai buat metrik.
Siapkan project
Buat virtual environment lalu pasang FastAPI dan Uvicorn. Kalau kamu baru pertama kali nyentuh framework ini, ada panduan Cara Membuat REST API dengan FastAPI yang lebih mendasar.
mkdir otel-demo && cd otel-demo
python -m venv .venv && source .venv/bin/activate
pip install fastapi uvicorn
# main.py
import time
from fastapi import FastAPI
app = FastAPI()
@app.get("/orders/{order_id}")
def get_order(order_id: str):
# simulasi query database yang lambat
time.sleep(0.05)
return {"order_id": order_id, "status": "shipped"}
Jalankan dengan uvicorn main:app --reload, lalu buka http://localhost:8000/orders/42. Endpoint ini sengaja saya buat pura-pura lambat lewat time.sleep(0.05) — nanti kita pakai trace untuk melihat jeda itu.
Instrumentasi otomatis: tanpa menyentuh kode
OTel punya modus auto-instrumentation yang menyuntik tracing lewat dependency yang sudah terpasang. Kamu cukup pasang distro-nya, lalu jalankan aplikasi lewat perintah opentelemetry-instrument.
pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install
opentelemetry-instrument --traces_exporter console uvicorn main:app --reload
Panggil endpoint tadi sekali, lalu lihat terminal. Kamu bakal dapat output span berformat JSON: nama span, trace_id, span_id, dan durasinya. Itu dia — request kamu sudah tertracing tanpa satu baris kode pun diubah.
opentelemetry-bootstrap -a install mengecek paket Python yang terpasang lalu menambahkan instrumentation library yang cocok. Nanti kalau kamu tambah dependency baru (misalnya Redis atau psycopg), jalankan ulang perintah ini biar instrumentasinya ikut terpasang.
Span manual: kasih nama ke bagian yang lambat
Instrumentasi otomatis hanya memberi span di level framework — satu span per request. Padahal time.sleep(0.05) di fungsi kamu itu nggak punya identitas. Kita tambah span sendiri biar jelas bagian mana yang makan waktu.
# main.py
import time
from fastapi import FastAPI
from opentelemetry import trace
app = FastAPI()
tracer = trace.get_tracer("orders-service")
@app.get("/orders/{order_id}")
def get_order(order_id: str):
with tracer.start_as_current_span("query-database") as span:
span.set_attribute("order.id", order_id)
time.sleep(0.05)
span.add_event("row-fetched", {"rows": 1})
return {"order_id": order_id, "status": "shipped"}
set_attribute menempelkan metadata ke span (muncul sebagai kolom di UI tracing), sementara add_event mencatat kejadian ber-timestamp di dalam span. Keduanya bikin trace nggak cuma bilang lambat, tapi lambat di query-database dengan order.id=42.
Kirim trace ke Jaeger lewat OTLP
Console exporter cuma buat pengembangan. Di produksi, trace dikirim ke backend seperti Jaeger, Grafana Tempo, atau Datadog lewat protokol OTLP. Jalankan Jaeger sekali dengan Docker.
docker run -d --name jaeger \
-p 16686:16686 \
-p 4317:4317 \
jaegertracing/all-in-one:latest
Port 4317 menerima OTLP via gRPC. Sekarang arahkan exporter ke sana, entah lewat flag CLI atau setup programmatic. Saya tunjukkan versi programmaticnya karena kamu jadi paham apa yang sebenarnya terjadi di balik opentelemetry-instrument.
pip install httpx opentelemetry-instrumentation-httpx
# main.py
import time
import httpx
from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
app = FastAPI()
resource = Resource.create({"service.name": "orders-service"})
provider = TracerProvider(resource=resource)
provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317"))
)
trace.set_tracer_provider(provider)
FastAPIInstrumentor.instrument_app(app)
HTTPXClientInstrumentor().instrument()
tracer = trace.get_tracer("orders-service")
@app.get("/orders/{order_id}")
def get_order(order_id: str):
with tracer.start_as_current_span("query-database") as span:
span.set_attribute("order.id", order_id)
time.sleep(0.05)
span.add_event("row-fetched", {"rows": 1})
return {"order_id": order_id, "status": "shipped"}
Jalankan uvicorn main:app --reload, panggil endpoint beberapa kali, lalu buka http://localhost:16686. Di tab Search, klik Find Traces — trace kamu muncul dengan nama service orders-service. Klik salah satu trace dan kamu lihat waterfall span: GET /orders/{order_id} membungkus query-database beserta durasi masing-masing.
Propagasi context ke service lain
Nilai sebenarnya dari tracing baru kelihatan waktu satu request melompat antar service. Misal service orders memanggil service inventory. Karena HTTPXClientInstrumentor sudah aktif, setiap request httpx otomatis membawa header traceparent berisi trace_id dan span_id induknya.
# inventory/main.py — service kedua
from fastapi import FastAPI
app = FastAPI()
@app.get("/stock/{order_id}")
def stock(order_id: str):
return {"order_id": order_id, "available": True}
# main.py — ubah endpoint di orders service
@app.get("/orders/{order_id}")
def get_order(order_id: str):
with tracer.start_as_current_span("fetch-order") as span:
r = httpx.get(f"http://localhost:8001/stock/{order_id}")
r.raise_for_status()
stock = r.json()
return {"order_id": order_id, **stock}
Jalankan inventory di port 8001 (uvicorn main:app --port 8001), pastikan dia juga di-instrumentasi, lalu panggil orders. Di Jaeger, span dari dua service kini menyatu dalam satu trace dengan trace_id yang sama — kamu bisa lihat persis berapa lama orders menunggu balasan inventory.
Jangan menimpa header traceparent di kode kamu. Kalau ada proxy atau reverse proxy di depan, pastikan dia meneruskan header ini apa adanya, supaya trace nggak putus di tengah jalan.
Langkah berikutnya
Trace bukan pengganti log. Kalau kode produksimu masih mengandalkan print(), bereskan dulu lewat panduan logging Python yang benar, lalu lapisi dengan OTel. Setelah itu, dua hal yang paling berdampak: pasang sampling (head sampling via OTEL_TRACES_SAMPLER) supaya volume trace terkendali, dan tambahkan exporter metrik OTel biar trace dan metrikmu satu sumber data.