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.
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:
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:
- Baris 13-20: konstanta status
ok/errormeniruotel/codes, sementaratraceIDDemobernilai tetap — SDK sungguhan membangkitkan trace ID acak unik. - Baris 22-35 (
kunciSpan,Span): kunci context privat kembali dipakai; structSpanmemuat identitas, status, dan atribut. - Baris 37-51 (
SetAttributes,SetStatus): atribut masuk sebagai pasangan variadik; status menyimpan kode dan pesan galat. - Baris 53-57 (
End): penutupan span menyerahkan dirinya ke provider — persis titik di mana SDK asli memasukkan span ke antrean exporter. - 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. - Baris 112-132 (
Cetak): menggantikan exporter: mencetak span dalam urutanEnddengan atribut terurut kunci lewatslices.Sorted(maps.Keys(...)). - Baris 140-153 (
main): pekerjaan dipisah ke fungsi sendiri agar seluruhdefer span.End()selesai sebelum hasil dicetak — pada SDK asli peran ini dipegangprovider.Shutdownyang mem-batch dan mengirim span tersisa. - Baris 155-166 (
tanganiRequest): pola idiomatik root:ctx, span := tracer.Start(...),defer span.End(), atribut HTTP, dan status galat dijalur akhir. - Baris 168-192: dua lapis di bawahnya meneruskan ctx;
cekAlamatgagal, statusnyaerror, lalu setiap induk menandai dirinyaerrorjuga — 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:
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:
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 #
| Pendekatan | Cara kerja | Contoh |
|---|---|---|
| Manual | tracer.Start ditulis di kode bisnis | demo di atas, span domain |
| Otomatis | library terinstrumentasi membuat span sendiri | otelmux, otelhttp, wrapper driver database |
| Zero-code | agen luar mengobservasi tanpa mengubah kode | alat 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 #
- OpenTelemetry untuk Go — dokumentasi resmi SDK Go beserta panduan instrumentasi.
- Package go.opentelemetry.io/otel — referensi API tracer, span, dan attribute.
- Distributed Tracing — konsep trace dan span yang mendasari SDK ini.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
otel-demo.go
Unduh berkaspackage 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
}