OpenTelemetry (disingkat OTel) adalah standar terbuka untuk menghasilkan dan mengirim data observability, yaitu traces, metrics, dan logs, dengan satu format yang sama, apa pun vendor tujuannya. Bayangin kontainer peti kemas di pelabuhan. Ukurannya standar, jadi kapal mana pun, derek mana pun, dan truk mana pun bisa mengangkutnya. Pabrik cukup mengisi kontainer sekali. Mau dikirim ke Surabaya atau Singapura, isi kontainernya gak dibongkar ulang, yang diganti cuma alamat tujuannya. Di OTel, aplikasimu mengisi "kontainer" (format OTLP), collector adalah pelabuhannya, dan backend seperti Jaeger, Grafana Tempo, atau Datadog adalah kota tujuannya.
Di pelajaran Structured Logging Patterns, nginx dan catatan-api disambungkan lewat request_id. OTel melakukan hal yang mirip untuk trace, dengan standar yang dipakai semua bahasa dan vendor.
🧪 Tebakan cepat: nginx meneruskan header
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01ke catatan-api. Span yang dibuat catatan-api nanti punya trace ID yang sama dengan nginx, atau trace ID baru? Dan apa arti01di ujungnya? Jawabannya ada di bagian "traceparent: konteks yang ikut di header".
Tiga sinyal: traces, metrics, logs
OTel menyebut tiga jenis data observability sebagai sinyal:
| Sinyal | Isinya | Pertanyaan yang dijawab |
|---|---|---|
| Traces | perjalanan satu request, dipecah jadi operasi berdurasi | "Request ini lambat di bagian mana?" |
| Metrics | angka yang dihitung terus-menerus: request per detik, error rate, latency p95 | "Berapa error rate 5 menit terakhir?" |
| Logs | kejadian dengan konteks, satu baris per kejadian | "Apa pesan error persisnya?" |
Ketiganya saling melengkapi. Metrics bilang "ada yang lambat", trace menunjukkan di mana, log menjelaskan kenapa. Satu baris log mencatat total durasi request, tapi gak memecahnya per operasi. Throughput dan error rate menunjukkan gejalanya, bukan bagian yang lambat. Untuk mencari bagian yang makan waktu paling lama, kamu butuh traces.
Trace dan span
Span adalah satu operasi dengan waktu mulai dan durasi: satu HTTP request, satu query database, satu fungsi penting. Trace adalah kumpulan span dari satu request, disambung oleh trace ID yang sama. Tiap span punya span ID sendiri dan menyimpan parent span ID, jadi span-span itu membentuk pohon. Ini trace Sari menyimpan catatan di Lab:
trace 4bf92f3577b34da6a3ce929d0e0e4736
└─ nginx POST /catatan 48 ms span 00f067aa0ba902b7
└─ catatan-api POST /catatan 45 ms span 53995c3f42cd8ad8, parent 00f067aa0ba902b7
├─ render-markdown 6 ms span e1f04a7c9b2d3568 (manual)
└─ INSERT catatan 31 ms span 8c3b6d0e5f2a1974 (otomatis, dari driver pg)
Contoh lain dari toko online: trace POST /api/orders berisi span API Gateway (2 ms), auth middleware (5 ms), OrderController.create (150 ms), dan di dalam controller ada span DB INSERT orders (30 ms), DB UPDATE inventory (20 ms), serta HTTP POST payment-service (80 ms). Tanpa membaca kode pun langsung terlihat: panggilan ke payment-service yang paling lama. Satu trace berisi banyak span, tiap span satu operasi.
Dari SDK ke collector ke backend
Data OTel lewat tiga tempat:
- SDK di dalam catatan-api membuat span, mengumpulkannya, lalu mengirimnya dengan protokol OTLP (OpenTelemetry Protocol): port 4318 untuk OTLP lewat HTTP, 4317 untuk gRPC.
- Collector (
otel/opentelemetry-collector-contrib) menerima, memproses (misalnya mengirim per batch), lalu meneruskan. - Backend menyimpan dan menampilkan: Jaeger atau Grafana Tempo untuk traces, Prometheus untuk metrics, Loki untuk logs, atau layanan berbayar seperti Datadog.
Config collector di web-1:
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
processors:
batch:
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp/jaeger]
receiversmembuka pintu masuk OTLP.0.0.0.0supaya container lain bisa mengirim.processors: batchmengumpulkan span sebentar lalu mengirimnya sekaligus, lebih hemat daripada satu per satu.exportersmenentukan tujuan.otlp/jaegerartinya exporter tipeotlpbernamajaeger.service.pipelinesmenyambungkan ketiganya untuk sinyaltraces.
Tahun depan tim pindah ke Datadog? Ganti exporter-nya jadi datadog (dengan api.key dari environment), dan kode catatan-api gak berubah satu baris pun. Instrumentasinya gak terikat ke vendor mana pun, yang diganti cukup tujuan pengirimannya. Di Lab, ganti backend-nya dan lihat: yang berubah cuma blok exporters dan nama exporter di pipeline, kode aplikasinya 0 baris.
Instrumentasi otomatis dan manual
Instrumentasi adalah kode yang membuat span dan metric. Untuk Node.js, satu file tracing.js sudah cukup untuk instrumentasi otomatis:
// tracing.js: dimuat SEBELUM kode aplikasi
const { NodeSDK } = require("@opentelemetry/sdk-node");
const { getNodeAutoInstrumentations } = require("@opentelemetry/auto-instrumentations-node");
const { OTLPTraceExporter } = require("@opentelemetry/exporter-trace-otlp-http");
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter(), // alamat dari OTEL_EXPORTER_OTLP_ENDPOINT
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
OTEL_SERVICE_NAME=catatan-api
OTEL_RESOURCE_ATTRIBUTES=service.version=1.8.0,deployment.environment.name=production
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
Jalankan dengan node --require ./tracing.js server.js. --require memuat tracing.js lebih dulu, supaya modul http, express, dan pg sudah "disadap" sebelum aplikasi memakainya. Hasilnya otomatis: span POST /catatan untuk tiap request dan span INSERT catatan untuk tiap query, tanpa mengubah kode route.
Environment variable di atas mengisi resource attributes: label yang menempel di semua data dari proses ini. service.name menjawab "siapa yang mengirim", service.version dan deployment.environment.name membantu membandingkan versi dan lingkungan. Tanpa OTEL_SERVICE_NAME, semua span-mu muncul sebagai unknown_service:node.
Untuk logika bisnis yang gak dikenal library mana pun, tambahkan span manual:
const { trace, SpanStatusCode } = require("@opentelemetry/api");
const tracer = trace.getTracer("catatan-api");
function renderMarkdown(catatan) {
return tracer.startActiveSpan("render-markdown", (span) => {
try {
span.setAttribute("catatan.panjang", catatan.isi.length);
return markdownKeHtml(catatan.isi);
} catch (err) {
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
throw err;
} finally {
span.end(); // tanpa end(), span gak pernah dikirim
}
});
}
startActiveSpanmembuat span baru sebagai anak dari span yang sedang aktif (POST /catatan).setAttributemenempelkan data ke span, misalnya panjang catatan.SpanStatusCodedi-import dari@opentelemetry/api, dan dipakai untuk menandai span yang gagal.span.end()difinallymemastikan span ditutup, sukses maupun gagal.
traceparent: konteks yang ikut di header
Supaya span di catatan-api tersambung ke span nginx, konteks trace harus ikut di request. Standarnya W3C Trace Context, lewat header traceparent dengan empat bagian yang dipisah tanda minus:
| Bagian | Nilai di Lab | Artinya |
|---|---|---|
| version | 00 |
versi format, sekarang 00 |
| trace-id | 4bf92f3577b34da6a3ce929d0e0e4736 |
32 karakter hex: ID trace, sama di semua service |
| parent-id | 00f067aa0ba902b7 |
16 karakter hex: span ID milik pemanggil (span nginx) |
| trace-flags | 01 |
bit terakhir 1 = sampled, trace ini direkam |
SDK di catatan-api membaca header ini lalu membuat span POST /catatan dengan trace ID yang sama, parent 00f067aa0ba902b7, dan span ID baru miliknya sendiri 53995c3f42cd8ad8. 01 artinya nginx memutuskan trace ini direkam, dan catatan-api mengikuti keputusan itu. Itu jawaban tebakannya. Kalau catatan-api memanggil service lain lewat HTTP, ia mengirim traceparent baru: trace ID tetap, parent-id diganti span ID milik catatan-api (span HTTP keluarnya).
Kalau flags-nya 00, catatan-api (dengan sampler bawaan, parentbased_always_on) juga gak merekam span, tapi tetap meneruskan konteksnya. Kalau header-nya gak ada atau rusak (huruf besar, panjang salah, trace-id nol semua), header itu diabaikan dan catatan-api memulai trace baru, sehingga trace-nya terputus dari nginx.
⚠️ Jebakan yang sering ketemu
tracing.jsdimuat setelah aplikasi.expressdanpgsudah terlanjur di-load tanpa disadap, jadi span HTTP dan database gak muncul. Pakainode --require ./tracing.js.- Lupa
span.end(). Span manual yang gak ditutup gak pernah dikirim ke collector. Taruh difinally. OTEL_SERVICE_NAMEgak diisi. Semua service muncul sebagaiunknown_service:nodedan gak bisa dibedakan.- Header
traceparentditulis sendiri dengan format salah. Huruf besar atau panjang yang salah membuat header diabaikan. Biarkan SDK yang membuat dan membaca header-nya.
TL;DR: OpenTelemetry adalah standar vendor-neutral untuk tiga sinyal: traces (perjalanan satu request, menjawab "lambat di mana"), metrics (angka seperti error rate dan latency), dan logs (kejadian dengan konteks). Satu trace berisi banyak span, tiap span satu operasi dengan durasi, disambung oleh trace ID dan parent span ID. Alurnya SDK di aplikasi, lalu OTLP (port 4318 HTTP, 4317 gRPC) ke collector, lalu ke backend seperti Jaeger, Tempo, atau Datadog. Ganti vendor cukup ganti exporter di collector, kode aplikasi tetap. Instrumentasi otomatis lewat tracing.js yang dimuat paling dulu, instrumentasi manual dengan startActiveSpan dan span.end(), dan OTEL_SERVICE_NAME plus resource attributes menandai siapa pengirimnya. Konteks berpindah lewat header traceparent: version-traceid-parentid-flags, dengan flags 01 = sampled.