Chapter sebelumnya merakit mekanisme tracing manual: ID, pohon, header traceparent, semuanya ditulis tangan. Menerapkan pola tersebut di sepuluh layanan berarti mengulang pekerjaan yang sama dan rawan selisih perilaku. OpenTelemetry (otel) menstandarkannya menjadi satu SDK multi-bahasa dengan API yang seragam — TracerProvider, Tracer, tracer.Start, span.End — plus propagator dan exporter siap pakai. Pada chapter ini kita membangun mini-SDK yang meniru bentuk API resmi tersebut dengan standard library murni, lalu membandingkannya dengan fragmen produksi dan arsitektur collector.

Peran Setiap Komponen #

Arsitektur otel terbagi menjadi empat peran. TracerProvider adalah pusat: menciptakan tracer, menampung span selesai, dan memegang konfigurasi sampling serta exporter. Tracer dibuat per lingkup instrumentasi — biasanya satu per library atau modul — dan menciptakan span. Span hidup dari tracer.Start(ctx, nama) hingga span.End(); context yang dikembalikan Start membawa span aktif sehingga pemanggilan berikutnya otomatis menjadi anak. Propagator menyuntikkan dan mengekstrak header traceparent di batas jaringan — mekanisme persis yang kita tulis manual di chapter sebelumnya, kini tinggal dipanggil lewat propagator.Inject dan propagator.Extract.

Program Demo: Mini SDK #

Berkas otel-demo.go membangun TracerProvider, Tracer, dan Span dengan bentuk API sedekat mungkin dengan otel sungguhan, lalu menjalankan tiga lapis span bersarang yang berakhir galat. Span ID memakai penghitung berurutan agar output deterministik; produksi memakai byte acak.

go
package main

import (
	"context"
	"fmt"
	"maps"
	"slices"
	"strings"
	"sync"
)

// Kode status span, meniru go.opentelemetry.io/otel/codes.
const (
	statusOK    = "ok"
	statusError = "error"
)

// traceIDDemo bernilai tetap agar output deterministik; produksi
// membangkitkan trace ID acak unik per request.
const traceIDDemo = "4bf92f3577b34da6a3ce929d0e0e4736"

// kunciSpan adalah kunci context privat untuk span aktif.
type kunciSpan struct{}

// Span merekam satu unit kerja: dibuat Tracer.Start, ditutup span.End.
type Span struct {
	provider    *TracerProvider
	nama        string
	spanID      string
	indukID     string
	traceID     string
	statusKode  string
	statusPesan string
	attrs       map[string]string
}

// SetAttributes menyimpan pasangan kunci-nilai pada span.
func (s *Span) SetAttributes(pasangan ...string) {
	for i := 0; i+1 < len(pasangan); i += 2 {
		if s.attrs == nil {
			s.attrs = map[string]string{}
		}
		s.attrs[pasangan[i]] = pasangan[i+1]
	}
}

// SetStatus menandai span sukses atau gagal beserta deskripsinya.
func (s *Span) SetStatus(kode, pesan string) {
	s.statusKode = kode
	s.statusPesan = pesan
}

// End menutup span dan menyerahkannya ke TracerProvider — pada SDK
// sungguhan, titik ini memasukkan span ke antrean exporter.
func (s *Span) End() {
	s.provider.catat(s)
}

// Tracer membuat span dalam lingkup satu nama instrumentasi.
type Tracer struct {
	provider *TracerProvider
}

// TracerProvider adalah pusat SDK: sumber tracer dan penampung span.
type TracerProvider struct {
	mu     sync.Mutex
	spans  []*Span
	urutan int
}

// TracerProviderBaru membuat provider siap pakai.
func TracerProviderBaru() *TracerProvider {
	return &TracerProvider{}
}

// Tracer mengembalikan tracer dengan nama instrumentasi tertentu.
func (p *TracerProvider) Tracer(nama string) *Tracer {
	return &Tracer{provider: p}
}

// Start membuat span baru sebagai anak span aktif pada ctx dan
// mengembalikan context yang membawa span tersebut.
func (t *Tracer) Start(ctx context.Context, nama string) (context.Context, *Span) {
	induk := spanDariContext(ctx)

	t.provider.mu.Lock()
	t.provider.urutan++
	nomor := t.provider.urutan
	t.provider.mu.Unlock()

	s := &Span{
		provider:   t.provider,
		nama:       nama,
		spanID:     fmt.Sprintf("%016x", nomor),
		traceID:    traceIDDemo,
		statusKode: statusOK,
	}
	if induk != nil {
		s.indukID = induk.spanID
		s.traceID = induk.traceID
	}
	return context.WithValue(ctx, kunciSpan{}, s), s
}

// catat menyimpan span selesai; menggantikan exporter pada demo ini.
func (p *TracerProvider) catat(s *Span) {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.spans = append(p.spans, s)
}

// Cetak menampilkan seluruh span terekam dalam urutan End.
func (p *TracerProvider) Cetak() {
	p.mu.Lock()
	defer p.mu.Unlock()
	for i, s := range p.spans {
		induk := s.indukID
		if induk == "" {
			induk = "(root)"
		}
		status := s.statusKode
		if s.statusPesan != "" {
			status += ": " + s.statusPesan
		}
		var attrs []string
		for _, k := range slices.Sorted(maps.Keys(s.attrs)) {
			attrs = append(attrs, k+"="+s.attrs[k])
		}
		fmt.Printf("%d. %s span=%s induk=%s status=%s attrs=[%s]\n",
			i+1, s.nama, s.spanID, induk, status, strings.Join(attrs, ", "))
	}
}

// spanDariContext mengambil span aktif dari ctx; nil bila belum ada span.
func spanDariContext(ctx context.Context) *Span {
	s, _ := ctx.Value(kunciSpan{}).(*Span)
	return s
}

func main() {
	provider := TracerProviderBaru()
	tracer := provider.Tracer("layanan-pesanan")

	// Pekerjaan berada di fungsi terpisah agar seluruh defer span.End()
	// selesai sebelum provider mencetak hasilnya — pada SDK sungguhan,
	// peran serupa dijalankan provider.Shutdown saat aplikasi berhenti.
	if err := tanganiRequest(tracer); err != nil {
		fmt.Println("request gagal:", err)
	}

	fmt.Println("--- span terekam, urutan End ---")
	provider.Cetak()
}

// tanganiRequest adalah span root: pola Start lalu defer End.
func tanganiRequest(tracer *Tracer) error {
	ctx, span := tracer.Start(context.Background(), "POST /pesanan")
	defer span.End()
	span.SetAttributes("http.method", "POST", "http.rute", "/pesanan")

	if err := validasiPesanan(ctx, tracer); err != nil {
		span.SetStatus(statusError, err.Error())
		return err
	}
	return nil
}

// validasiPesanan membuat span anak dan meneruskan ctx-nya.
func validasiPesanan(ctx context.Context, tracer *Tracer) error {
	ctx, span := tracer.Start(ctx, "validasiPesanan")
	defer span.End()

	span.SetAttributes("pesanan.id", "42")

	if err := cekAlamat(ctx, tracer); err != nil {
		span.SetStatus(statusError, err.Error())
		return err
	}
	return nil
}

// cekAlamat adalah span cucu: bukti ctx membawa span induknya.
func cekAlamat(ctx context.Context, tracer *Tracer) error {
	_, span := tracer.Start(ctx, "cekAlamat")
	defer span.End()

	span.SetAttributes("alamat.field", "alamat_kirim")

	err := fmt.Errorf("alamat kosong")
	span.SetStatus(statusError, err.Error())
	return err
}

Output yang Dihasilkan:

text
request gagal: alamat kosong
--- span terekam, urutan End ---
1. cekAlamat span=0000000000000003 induk=0000000000000002 status=error: alamat kosong attrs=[alamat.field=alamat_kirim]
2. validasiPesanan span=0000000000000002 induk=0000000000000001 status=error: alamat kosong attrs=[pesanan.id=42]
3. POST /pesanan span=0000000000000001 induk=(root) status=error: alamat kosong attrs=[http.method=POST, http.rute=/pesanan]

Mari kita telaah baris demi baris program di atas:

  1. Baris 13-20: konstanta status ok/error meniru otel/codes, sementara traceIDDemo bernilai tetap — SDK sungguhan membangkitkan trace ID acak unik.
  2. Baris 22-35 (kunciSpan, Span): kunci context privat kembali dipakai; struct Span memuat identitas, status, dan atribut.
  3. Baris 37-51 (SetAttributes, SetStatus): atribut masuk sebagai pasangan variadik; status menyimpan kode dan pesan galat.
  4. Baris 53-57 (End): penutupan span menyerahkan dirinya ke provider — persis titik di mana SDK asli memasukkan span ke antrean exporter.
  5. Baris 81-103 (Start): span induk diambil dari ctx, nomor urut menjadi span ID 16-karakter heksadesimal, dan trace ID diwarisi dari induk — root memakai nilai demo.
  6. Baris 112-132 (Cetak): menggantikan exporter: mencetak span dalam urutan End dengan atribut terurut kunci lewat slices.Sorted(maps.Keys(...)).
  7. Baris 140-153 (main): pekerjaan dipisah ke fungsi sendiri agar seluruh defer span.End() selesai sebelum hasil dicetak — pada SDK asli peran ini dipegang provider.Shutdown yang mem-batch dan mengirim span tersisa.
  8. Baris 155-166 (tanganiRequest): pola idiomatik root: ctx, span := tracer.Start(...), defer span.End(), atribut HTTP, dan status galat dijalur akhir.
  9. Baris 168-192: dua lapis di bawahnya meneruskan ctx; cekAlamat gagal, statusnya error, lalu setiap induk menandai dirinya error juga — galat mengalir ke atas sepanjang pohon.

Fragmen Produksi #

API resmi memiliki bentuk yang hampir sama dengan demo di atas — hanya sumber paketnya berbeda. Fragmen berikut membutuhkan dependency go.opentelemetry.io/otel sehingga tidak diletakkan sebagai berkas di bundle:

go
import (
	"context"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
)

var tracer = otel.Tracer("layanan-pesanan")

func tanganiRequest(ctx context.Context) error {
	ctx, span := tracer.Start(ctx, "POST /pesanan")
	defer span.End()

	span.SetAttributes(
		attribute.String("http.method", "POST"),
		attribute.String("http.rute", "/pesanan"),
	)

	if err := validasiPesanan(ctx); err != nil {
		span.SetStatus(codes.Error, err.Error())
		return err
	}
	return nil
}

Exporter yang paling umum mengirim span lewat protokol OTLP/HTTP ke sebuah collector, biasanya di localhost:4318:

go
exporter, err := otlptracehttp.New(ctx,
	otlptracehttp.WithEndpoint("localhost:4318"),
	otlptracehttp.WithInsecure(),
)
provider := sdktrace.NewTracerProvider(
	sdktrace.WithBatcher(exporter),
)
otel.SetTracerProvider(provider)

WithBatcher mengumpulkan span dan mengirimnya berkala — jangan lupa memanggil provider.Shutdown saat aplikasi berhenti agar batch terakhir tidak hilang.

Manual, Otomatis, dan Zero-Code #

PendekatanCara kerjaContoh
Manualtracer.Start ditulis di kode bisnisdemo di atas, span domain
Otomatislibrary terinstrumentasi membuat span sendiriotelmux, otelhttp, wrapper driver database
Zero-codeagen luar mengobservasi tanpa mengubah kodealat berbasis eBPF

Kombinasi yang lazim: otomatis untuk lapisan HTTP dan SQL (span infrastruktur gratis), manual untuk tahapan bisnis yang bermakna seperti validasiPesanan atau prosesPembayaran.

Arsitektur Collector #

Collector berdiri di antara aplikasi dan backend, menerima OTLP dari semua layanan lalu meneruskan ke tujuan berbeda-beda:

Satu aplikasi hanya perlu tahu satu alamat collector; pergantian backend tracing tidak menyentuh kode aplikasi sama sekali. Collector juga menjadi tempat menyaring, menambah, dan men-sample ulang data sebelum data mencapai penyimpanan.

Dengan tracing lengkap, sisa terakhir dari section ini adalah pertanyaan paling operasional: apakah proses ini hidup, dan siapkah ia menerima traffic? Chapter penutup memisahkan keduanya lewat endpoint health, readiness, dan liveness.

Referensi #

Berkas Sumber

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

otel-demo.go

Unduh berkas
go
package main

import (
	"context"
	"fmt"
	"maps"
	"slices"
	"strings"
	"sync"
)

// Kode status span, meniru go.opentelemetry.io/otel/codes.
const (
	statusOK    = "ok"
	statusError = "error"
)

// traceIDDemo bernilai tetap agar output deterministik; produksi
// membangkitkan trace ID acak unik per request.
const traceIDDemo = "4bf92f3577b34da6a3ce929d0e0e4736"

// kunciSpan adalah kunci context privat untuk span aktif.
type kunciSpan struct{}

// Span merekam satu unit kerja: dibuat Tracer.Start, ditutup span.End.
type Span struct {
	provider    *TracerProvider
	nama        string
	spanID      string
	indukID     string
	traceID     string
	statusKode  string
	statusPesan string
	attrs       map[string]string
}

// SetAttributes menyimpan pasangan kunci-nilai pada span.
func (s *Span) SetAttributes(pasangan ...string) {
	for i := 0; i+1 < len(pasangan); i += 2 {
		if s.attrs == nil {
			s.attrs = map[string]string{}
		}
		s.attrs[pasangan[i]] = pasangan[i+1]
	}
}

// SetStatus menandai span sukses atau gagal beserta deskripsinya.
func (s *Span) SetStatus(kode, pesan string) {
	s.statusKode = kode
	s.statusPesan = pesan
}

// End menutup span dan menyerahkannya ke TracerProvider — pada SDK
// sungguhan, titik ini memasukkan span ke antrean exporter.
func (s *Span) End() {
	s.provider.catat(s)
}

// Tracer membuat span dalam lingkup satu nama instrumentasi.
type Tracer struct {
	provider *TracerProvider
}

// TracerProvider adalah pusat SDK: sumber tracer dan penampung span.
type TracerProvider struct {
	mu     sync.Mutex
	spans  []*Span
	urutan int
}

// TracerProviderBaru membuat provider siap pakai.
func TracerProviderBaru() *TracerProvider {
	return &TracerProvider{}
}

// Tracer mengembalikan tracer dengan nama instrumentasi tertentu.
func (p *TracerProvider) Tracer(nama string) *Tracer {
	return &Tracer{provider: p}
}

// Start membuat span baru sebagai anak span aktif pada ctx dan
// mengembalikan context yang membawa span tersebut.
func (t *Tracer) Start(ctx context.Context, nama string) (context.Context, *Span) {
	induk := spanDariContext(ctx)

	t.provider.mu.Lock()
	t.provider.urutan++
	nomor := t.provider.urutan
	t.provider.mu.Unlock()

	s := &Span{
		provider:   t.provider,
		nama:       nama,
		spanID:     fmt.Sprintf("%016x", nomor),
		traceID:    traceIDDemo,
		statusKode: statusOK,
	}
	if induk != nil {
		s.indukID = induk.spanID
		s.traceID = induk.traceID
	}
	return context.WithValue(ctx, kunciSpan{}, s), s
}

// catat menyimpan span selesai; menggantikan exporter pada demo ini.
func (p *TracerProvider) catat(s *Span) {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.spans = append(p.spans, s)
}

// Cetak menampilkan seluruh span terekam dalam urutan End.
func (p *TracerProvider) Cetak() {
	p.mu.Lock()
	defer p.mu.Unlock()
	for i, s := range p.spans {
		induk := s.indukID
		if induk == "" {
			induk = "(root)"
		}
		status := s.statusKode
		if s.statusPesan != "" {
			status += ": " + s.statusPesan
		}
		var attrs []string
		for _, k := range slices.Sorted(maps.Keys(s.attrs)) {
			attrs = append(attrs, k+"="+s.attrs[k])
		}
		fmt.Printf("%d. %s span=%s induk=%s status=%s attrs=[%s]\n",
			i+1, s.nama, s.spanID, induk, status, strings.Join(attrs, ", "))
	}
}

// spanDariContext mengambil span aktif dari ctx; nil bila belum ada span.
func spanDariContext(ctx context.Context) *Span {
	s, _ := ctx.Value(kunciSpan{}).(*Span)
	return s
}

func main() {
	provider := TracerProviderBaru()
	tracer := provider.Tracer("layanan-pesanan")

	// Pekerjaan berada di fungsi terpisah agar seluruh defer span.End()
	// selesai sebelum provider mencetak hasilnya — pada SDK sungguhan,
	// peran serupa dijalankan provider.Shutdown saat aplikasi berhenti.
	if err := tanganiRequest(tracer); err != nil {
		fmt.Println("request gagal:", err)
	}

	fmt.Println("--- span terekam, urutan End ---")
	provider.Cetak()
}

// tanganiRequest adalah span root: pola Start lalu defer End.
func tanganiRequest(tracer *Tracer) error {
	ctx, span := tracer.Start(context.Background(), "POST /pesanan")
	defer span.End()
	span.SetAttributes("http.method", "POST", "http.rute", "/pesanan")

	if err := validasiPesanan(ctx, tracer); err != nil {
		span.SetStatus(statusError, err.Error())
		return err
	}
	return nil
}

// validasiPesanan membuat span anak dan meneruskan ctx-nya.
func validasiPesanan(ctx context.Context, tracer *Tracer) error {
	ctx, span := tracer.Start(ctx, "validasiPesanan")
	defer span.End()

	span.SetAttributes("pesanan.id", "42")

	if err := cekAlamat(ctx, tracer); err != nil {
		span.SetStatus(statusError, err.Error())
		return err
	}
	return nil
}

// cekAlamat adalah span cucu: bukti ctx membawa span induknya.
func cekAlamat(ctx context.Context, tracer *Tracer) error {
	_, span := tracer.Start(ctx, "cekAlamat")
	defer span.End()

	span.SetAttributes("alamat.field", "alamat_kirim")

	err := fmt.Errorf("alamat kosong")
	span.SetStatus(statusError, err.Error())
	return err
}