OpenTelemetry Fundamentals — DevOps

OpenTelemetry (OTel) OpenTelemetry adalah standar open-source untuk observability: traces, metrics, dan logs. Vendor-neutral — data bisa dikirim ke Jaeger…

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-01 ke catatan-api. Span yang dibuat catatan-api nanti punya trace ID yang sama dengan nginx, atau trace ID baru? Dan apa arti 01 di 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:

  1. SDK di dalam catatan-api membuat span, mengumpulkannya, lalu mengirimnya dengan protokol OTLP (OpenTelemetry Protocol): port 4318 untuk OTLP lewat HTTP, 4317 untuk gRPC.
  2. Collector (otel/opentelemetry-collector-contrib) menerima, memproses (misalnya mengirim per batch), lalu meneruskan.
  3. 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]

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
    }
  });
}

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

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.

Yang akan kamu pelajari