Request modern jarang ditangani satu proses: gateway memanggil layanan pesanan, layanan pesanan membaca database, lalu memanggil layanan pembayaran. Log keempat komponen itu bisa ditautkan dengan request ID, tetapi urutan sebab-akibat dan waktu yang hilang di tiap lompatan tetap tak terlihat. Distributed tracing merekam setiap tahap sebagai span — unit kerja bernama berdurasi — yang membentuk pohon trace untuk satu request penuh. Pada chapter ini kita membangun model span in-memory, lalu mempraktikkan propagasinya lewat header W3C traceparent.
Trace: Pohon Span Lintas Layanan #
Satu trace dimulai dari span root — biasanya request HTTP masuk — lalu bertumbuh tiap kali operasi memanggil operasi lain. Setiap span membawa identitas sendiri (span-id), identitas induk (parent-id), dan identitas trace yang sama untuk semua anggotanya. Dari pohon itu pertanyaan “di mana waktu request habis?” terjawab: durasi tiap cabang dibandingkan, dan status error menunjukkan titik kegagalan persisnya.
Pada contoh di bawah, SELECT stok berada dua tingkat di bawah root — database dipanggil oleh layanan pesanan, bukan oleh API. Span Tagih bermuatan error dan membuat root ikut error; status anak mengalir ke atas sepanjang jalur induk.
Program Demo: Pohon Span Manual #
Berkas tracing-demo.go memodelkan lima span dari satu request POST /pesanan, mencetak pohonnya, lalu beralih ke propagasi header. ID-nya sengaja tetap agar output dapat direproduksi.
package main
import (
"encoding/hex"
"fmt"
"strings"
)
// Span merekam satu unit kerja dalam sebuah trace.
type Span struct {
TraceID string // 32 karakter heksadesimal, sama untuk satu trace
SpanID string // 16 karakter heksadesimal, unik per span
IndukID string // SpanID induk; kosong untuk span root
Nama string
Layanan string
Durasi int // milidetik
Status string // "ok" atau "error"
}
// tampilkanSpan mencetak pohon span: anak mengikuti induknya dengan
// indentasi bertingkat, urutan mengikuti urutan slice.
func tampilkanSpan(spans []Span, indukID string, kedalaman int) {
for _, s := range spans {
if s.IndukID != indukID {
continue
}
fmt.Printf("%s- %s (%s, %dms, %s)\n",
strings.Repeat(" ", kedalaman), s.Nama, s.Layanan, s.Durasi, s.Status)
tampilkanSpan(spans, s.SpanID, kedalaman+1)
}
}
// TraceParent adalah komponen header W3C traceparent.
type TraceParent struct {
Versi string // 2 karakter heksadesimal
TraceID string // 32 karakter heksadesimal
SpanID string // 16 karakter heksadesimal
Flag string // 2 karakter heksadesimal
}
// parseTraceParent memecah header "versi-traceid-spanid-flags" dan
// memvalidasi panjang serta format heksadesimal tiap segmen.
func parseTraceParent(header string) (TraceParent, error) {
bagian := strings.Split(header, "-")
if len(bagian) != 4 {
return TraceParent{}, fmt.Errorf("jumlah segmen %d, harus 4", len(bagian))
}
for i, seg := range bagian {
if _, err := hex.DecodeString(seg); err != nil {
return TraceParent{}, fmt.Errorf("segmen %d tidak valid: %w", i, err)
}
}
if len(bagian[1]) != 32 {
return TraceParent{}, fmt.Errorf("trace-id harus 32 karakter, dapat %d", len(bagian[1]))
}
if len(bagian[2]) != 16 {
return TraceParent{}, fmt.Errorf("span-id harus 16 karakter, dapat %d", len(bagian[2]))
}
return TraceParent{Versi: bagian[0], TraceID: bagian[1], SpanID: bagian[2], Flag: bagian[3]}, nil
}
// formatTraceParent merakit kembali header dari komponennya.
func formatTraceParent(tp TraceParent) string {
return strings.Join([]string{tp.Versi, tp.TraceID, tp.SpanID, tp.Flag}, "-")
}
func main() {
const traceID = "4bf92f3577b34da6a3ce929d0e0e4736"
// ID span di bawah sengaja tetap agar demo deterministik; produksi
// memakai crypto/rand (8 byte untuk span, 16 byte untuk trace).
spans := []Span{
{TraceID: traceID, SpanID: "bb9cd33a0c521e08", Nama: "POST /pesanan", Layanan: "api", Durasi: 220, Status: "error"},
{TraceID: traceID, SpanID: "3c5a9f01b27e4d10", IndukID: "bb9cd33a0c521e08", Nama: "validasi", Layanan: "api", Durasi: 12, Status: "ok"},
{TraceID: traceID, SpanID: "7a2e88d4c1f3b529", IndukID: "bb9cd33a0c521e08", Nama: "AmbilStok", Layanan: "pesanan", Durasi: 95, Status: "ok"},
{TraceID: traceID, SpanID: "9f01a4c8e2b7d635", IndukID: "7a2e88d4c1f3b529", Nama: "SELECT stok", Layanan: "db", Durasi: 40, Status: "ok"},
{TraceID: traceID, SpanID: "e58b2c7f9a1d4e63", IndukID: "bb9cd33a0c521e08", Nama: "Tagih", Layanan: "pembayaran", Durasi: 85, Status: "error"},
}
fmt.Println("--- pohon span satu trace ---")
fmt.Printf("trace %s — %d span\n", traceID, len(spans))
tampilkanSpan(spans, "", 0)
fmt.Println("--- parse dan format header traceparent ---")
masuk := "00-" + traceID + "-00f067aa0ba902b7-01"
tp, err := parseTraceParent(masuk)
if err != nil {
panic(err)
}
fmt.Println("masuk :", masuk)
fmt.Printf("versi=%s trace-id=%s span-id=%s flag=%s\n", tp.Versi, tp.TraceID, tp.SpanID, tp.Flag)
fmt.Println("round-trip identik:", formatTraceParent(tp) == masuk)
// Layanan penerima membuat span baru; request keluar membawa span ID
// miliknya sebagai induk bagi layanan berikutnya.
keluar := formatTraceParent(TraceParent{
Versi: "00",
TraceID: tp.TraceID,
SpanID: "85e1c2b7d90f4a16",
Flag: "01",
})
fmt.Println("keluar :", keluar)
fmt.Println("--- header tidak valid ---")
_, err = parseTraceParent("00-XYZ-00f067aa0ba902b7-01")
fmt.Println("galat :", err)
}Output yang Dihasilkan:
--- pohon span satu trace ---
trace 4bf92f3577b34da6a3ce929d0e0e4736 — 5 span
- POST /pesanan (api, 220ms, error)
- validasi (api, 12ms, ok)
- AmbilStok (pesanan, 95ms, ok)
- SELECT stok (db, 40ms, ok)
- Tagih (pembayaran, 85ms, error)
--- parse dan format header traceparent ---
masuk : 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
versi=00 trace-id=4bf92f3577b34da6a3ce929d0e0e4736 span-id=00f067aa0ba902b7 flag=01
round-trip identik: true
keluar : 00-4bf92f3577b34da6a3ce929d0e0e4736-85e1c2b7d90f4a16-01
--- header tidak valid ---
galat : segmen 1 tidak valid: encoding/hex: invalid byte: U+0058 'X'Mari kita telaah baris demi baris program di atas:
- Baris 10-18 (
Span): lima field inti tracing — identitas (TraceID,SpanID,IndukID), deskripsi (Nama,Layanan), dan hasil (Durasi,Status);IndukIDkosong menandai span root. - Baris 22-31 (
tampilkanSpan): rekursi — cari span ber-induk tertentu, cetak dengan indentasi, telusuri anak-anaknya; urutan slice menentukan urutan saudara. - Baris 72-78: lima span dirakit dengan ID tetap 16 karakter heksadesimal;
SELECT stokmemakaiIndukIDmilikAmbilStok, itulah yang membuatnya mencuat dua tingkat. - Baris 80-82: header trace dicetak bersama jumlah span; pohon dirender mulai dari root (induk kosong).
- Baris 43-59 (
parseTraceParent): tiga lapis validasi — jumlah segmen harus 4, tiap segmen heksadesimal (dicekhex.DecodeString), lalutrace-idtepat 32 danspan-idtepat 16 karakter; galat dibungkus%wala chapter error wrapping . - Baris 84-92: header masuk dipecah lalu dicetak; perbandingan
formatTraceParent(tp) == masukmembuktikan parse-format menghasilkan teks identik. - Baris 96-102: layanan penerima membuat span baru (
85e1...) lalu merakit header keluar dengan span ID miliknya — trace ID tetap, span ID berganti; inilah penyambung pohon antar proses. - Baris 104-106: header palsu bersegmen
XYZditolak dengan pesan galat asli dariencoding/hex— input lintas batas layanan selalu patut divalidasi.
Propagasi lewat Header traceparent #
Header traceparent standar W3C berbentuk empat segmen heksadesimal dipisah tanda hubung:
| Segmen | Panjang | Isi |
|---|---|---|
| versi | 2 karakter | 00 untuk format saat ini |
| trace-id | 32 karakter | identitas trace, sama di seluruh lompatan |
| span-id | 16 karakter | span pemanggil — menjadi induk di sisi penerima |
| flags | 2 karakter | bit 01 berarti trace ini sampled |
Alur satu request lintas dua layanan tampak begini:
Setiap lompatan hanya membaca satu header dan menulis satu header — protokol sekecil itulah yang membuat tracing menyeberangi bahasa dan infrastruktur yang berbeda-beda.
Sampling: Menelusuri Sebagian Saja #
Volume trace penuh pada lalu lintas tinggi mahal disimpan. Sampling memutuskan trace mana yang direkam — bit 01 pada flags menandai trace terpilih. Strategi umum head-based: keputusan di span root sebelum pekerjaan dimulai, dengan laju 1-10%; trace bergalat biasanya dipaksa terekam. Trace tak tersampel tetap membawa header, hanya span-nya tak dikirim.
Tracing dan Logging Saling Melengkapi #
Trace menjawab “alurnya bagaimana”, log menjawab “detailnya apa”. Praktik terbaiknya menjatuhkan trace ID ke setiap baris log — field trace_id pada structured logging — sehingga dari satu span yang mencurigakan kita dapat grep seluruh log kejadian tersebut. Metrik lalu menghitung frekuensinya: jumlah trace error per menit adalah turunan langsung dari pilar ketiga observability ini.
Memahami anatomi trace membuat kita siap memakai perangkat produksinya. Chapter OpenTelemetry memperkenalkan SDK resmi yang mengotomatiskan seluruh mekanisme yang barusan kita rakit manual.
Referensi #
- OpenTelemetry Go: Tracing — konsep span, tracer, dan propagasi pada SDK resmi.
- Dokumentasi package encoding/hex — validasi heksadesimal parser traceparent.
- Dokumentasi package net/http
— propagasi header lewat
Header.SetdanHeader.Get.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
tracing-demo.go
Unduh berkaspackage main
import (
"encoding/hex"
"fmt"
"strings"
)
// Span merekam satu unit kerja dalam sebuah trace.
type Span struct {
TraceID string // 32 karakter heksadesimal, sama untuk satu trace
SpanID string // 16 karakter heksadesimal, unik per span
IndukID string // SpanID induk; kosong untuk span root
Nama string
Layanan string
Durasi int // milidetik
Status string // "ok" atau "error"
}
// tampilkanSpan mencetak pohon span: anak mengikuti induknya dengan
// indentasi bertingkat, urutan mengikuti urutan slice.
func tampilkanSpan(spans []Span, indukID string, kedalaman int) {
for _, s := range spans {
if s.IndukID != indukID {
continue
}
fmt.Printf("%s- %s (%s, %dms, %s)\n",
strings.Repeat(" ", kedalaman), s.Nama, s.Layanan, s.Durasi, s.Status)
tampilkanSpan(spans, s.SpanID, kedalaman+1)
}
}
// TraceParent adalah komponen header W3C traceparent.
type TraceParent struct {
Versi string // 2 karakter heksadesimal
TraceID string // 32 karakter heksadesimal
SpanID string // 16 karakter heksadesimal
Flag string // 2 karakter heksadesimal
}
// parseTraceParent memecah header "versi-traceid-spanid-flags" dan
// memvalidasi panjang serta format heksadesimal tiap segmen.
func parseTraceParent(header string) (TraceParent, error) {
bagian := strings.Split(header, "-")
if len(bagian) != 4 {
return TraceParent{}, fmt.Errorf("jumlah segmen %d, harus 4", len(bagian))
}
for i, seg := range bagian {
if _, err := hex.DecodeString(seg); err != nil {
return TraceParent{}, fmt.Errorf("segmen %d tidak valid: %w", i, err)
}
}
if len(bagian[1]) != 32 {
return TraceParent{}, fmt.Errorf("trace-id harus 32 karakter, dapat %d", len(bagian[1]))
}
if len(bagian[2]) != 16 {
return TraceParent{}, fmt.Errorf("span-id harus 16 karakter, dapat %d", len(bagian[2]))
}
return TraceParent{Versi: bagian[0], TraceID: bagian[1], SpanID: bagian[2], Flag: bagian[3]}, nil
}
// formatTraceParent merakit kembali header dari komponennya.
func formatTraceParent(tp TraceParent) string {
return strings.Join([]string{tp.Versi, tp.TraceID, tp.SpanID, tp.Flag}, "-")
}
func main() {
const traceID = "4bf92f3577b34da6a3ce929d0e0e4736"
// ID span di bawah sengaja tetap agar demo deterministik; produksi
// memakai crypto/rand (8 byte untuk span, 16 byte untuk trace).
spans := []Span{
{TraceID: traceID, SpanID: "bb9cd33a0c521e08", Nama: "POST /pesanan", Layanan: "api", Durasi: 220, Status: "error"},
{TraceID: traceID, SpanID: "3c5a9f01b27e4d10", IndukID: "bb9cd33a0c521e08", Nama: "validasi", Layanan: "api", Durasi: 12, Status: "ok"},
{TraceID: traceID, SpanID: "7a2e88d4c1f3b529", IndukID: "bb9cd33a0c521e08", Nama: "AmbilStok", Layanan: "pesanan", Durasi: 95, Status: "ok"},
{TraceID: traceID, SpanID: "9f01a4c8e2b7d635", IndukID: "7a2e88d4c1f3b529", Nama: "SELECT stok", Layanan: "db", Durasi: 40, Status: "ok"},
{TraceID: traceID, SpanID: "e58b2c7f9a1d4e63", IndukID: "bb9cd33a0c521e08", Nama: "Tagih", Layanan: "pembayaran", Durasi: 85, Status: "error"},
}
fmt.Println("--- pohon span satu trace ---")
fmt.Printf("trace %s — %d span\n", traceID, len(spans))
tampilkanSpan(spans, "", 0)
fmt.Println("--- parse dan format header traceparent ---")
masuk := "00-" + traceID + "-00f067aa0ba902b7-01"
tp, err := parseTraceParent(masuk)
if err != nil {
panic(err)
}
fmt.Println("masuk :", masuk)
fmt.Printf("versi=%s trace-id=%s span-id=%s flag=%s\n", tp.Versi, tp.TraceID, tp.SpanID, tp.Flag)
fmt.Println("round-trip identik:", formatTraceParent(tp) == masuk)
// Layanan penerima membuat span baru; request keluar membawa span ID
// miliknya sebagai induk bagi layanan berikutnya.
keluar := formatTraceParent(TraceParent{
Versi: "00",
TraceID: tp.TraceID,
SpanID: "85e1c2b7d90f4a16",
Flag: "01",
})
fmt.Println("keluar :", keluar)
fmt.Println("--- header tidak valid ---")
_, err = parseTraceParent("00-XYZ-00f067aa0ba902b7-01")
fmt.Println("galat :", err)
}