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.

go
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:

text
--- 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:

  1. Baris 10-18 (Span): lima field inti tracing — identitas (TraceID, SpanID, IndukID), deskripsi (Nama, Layanan), dan hasil (Durasi, Status); IndukID kosong menandai span root.
  2. Baris 22-31 (tampilkanSpan): rekursi — cari span ber-induk tertentu, cetak dengan indentasi, telusuri anak-anaknya; urutan slice menentukan urutan saudara.
  3. Baris 72-78: lima span dirakit dengan ID tetap 16 karakter heksadesimal; SELECT stok memakai IndukID milik AmbilStok, itulah yang membuatnya mencuat dua tingkat.
  4. Baris 80-82: header trace dicetak bersama jumlah span; pohon dirender mulai dari root (induk kosong).
  5. Baris 43-59 (parseTraceParent): tiga lapis validasi — jumlah segmen harus 4, tiap segmen heksadesimal (dicek hex.DecodeString), lalu trace-id tepat 32 dan span-id tepat 16 karakter; galat dibungkus %w ala chapter error wrapping .
  6. Baris 84-92: header masuk dipecah lalu dicetak; perbandingan formatTraceParent(tp) == masuk membuktikan parse-format menghasilkan teks identik.
  7. 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.
  8. Baris 104-106: header palsu bersegmen XYZ ditolak dengan pesan galat asli dari encoding/hex — input lintas batas layanan selalu patut divalidasi.

Propagasi lewat Header traceparent #

Header traceparent standar W3C berbentuk empat segmen heksadesimal dipisah tanda hubung:

SegmenPanjangIsi
versi2 karakter00 untuk format saat ini
trace-id32 karakteridentitas trace, sama di seluruh lompatan
span-id16 karakterspan pemanggil — menjadi induk di sisi penerima
flags2 karakterbit 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

tracing-demo.go

Unduh berkas
go
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)
}