← Semua pembelajaran / Go Nol → Enterprise
Fase 9 · Performa & Observability

OpenTelemetry — trace, metrik & korelasi

Log memberi tahu apa yang terjadi; metrik memberi tahu seberapa sering; trace memberi tahu di mana waktunya habis. Untuk sistem yang punya beberapa lapisan, ketiganya harus bisa saling dirujuk.

Sumber asli opentelemetry.io Resmi Rangkuman ~8 menit baca

Intisari

  • Trace untuk satu permintaan, metrik untuk agregat, log untuk detail. Ketiganya dihubungkan trace_id.
  • Instrumentasi otomatis (otelhttp, otelsql) menutup sebagian besar kebutuhan tanpa mengubah kode bisnis.
  • Sampling wajib di produksi — 100% trace pada trafik tinggi berarti biaya penyimpanan yang tidak masuk akal.
  • Ukur p95 dan p99, bukan rata-rata. Rata-rata menyembunyikan satu dari sepuluh pengguna yang menunggu tiga detik.
  • Nama span dan label metrik harus memakai pola rute, bukan URL mentah.

Menyiapkannya

func SiapkanOtel(ctx context.Context, cfg config.Config) (func(context.Context) error, error) {
	res, err := resource.New(ctx,
		resource.WithAttributes(
			semconv.ServiceName("toko-api"),
			semconv.ServiceVersion(versi),
			semconv.DeploymentEnvironment(cfg.Env),
		))
	if err != nil {
		return nil, err
	}

	eksportir, err := otlptracegrpc.New(ctx,
		otlptracegrpc.WithEndpoint(cfg.OtelEndpoint))
	if err != nil {
		return nil, err
	}

	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(eksportir),
		sdktrace.WithResource(res),
		// Ambil 10% trace, TAPI kalau induknya sudah diambil, ikut diambil —
		// supaya satu permintaan tidak terpotong separuh.
		sdktrace.WithSampler(
			sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))),
	)
	otel.SetTracerProvider(tp)
	otel.SetTextMapPropagator(propagation.TraceContext{})

	return tp.Shutdown, nil    // dipanggil saat shutdown (Fase 5)
}

tp.Shutdown harus benar-benar dipanggil saat aplikasi berhenti. Eksportir batch menahan span di memori sampai batch penuh atau waktunya habis — tanpa shutdown, trace dari beberapa detik terakhir sebelum deploy (justru yang paling menarik saat menyelidiki masalah deploy) hilang.

Instrumentasi otomatis

// Server HTTP: satu pembungkus, semua permintaan ter-trace
handler := otelhttp.NewHandler(mux, "server",
	otelhttp.WithSpanNameFormatter(func(_ string, r *http.Request) string {
		// POLA rute, bukan URL mentah — kalau tidak, kamu dapat
		// satu juta nama span unik.
		return r.Method + " " + chi.RouteContext(r.Context()).RoutePattern()
	}))

// Klien HTTP: konteks trace ikut ke layanan tujuan
klien := &http.Client{
	Transport: otelhttp.NewTransport(tr),
	Timeout:   10 * time.Second,
}

// Database: setiap kueri jadi span sendiri
db, err := otelsql.Open("pgx", dsn,
	otelsql.WithAttributes(semconv.DBSystemPostgreSQL))

Span manual untuk bagian yang penting

func (l *Layanan) Beli(ctx context.Context, id int64, jumlah int) error {
	ctx, span := tracer.Start(ctx, "produk.Beli",
		trace.WithAttributes(
			attribute.Int64("produk.id", id),
			attribute.Int("jumlah", jumlah),
		))
	defer span.End()

	if err := l.simpan.Kurangi(ctx, id, jumlah); err != nil {
		span.RecordError(err)
		span.SetStatus(codes.Error, "gagal mengurangi stok")
		return err
	}

	span.SetAttributes(attribute.Bool("stok.cukup", true))
	return nil
}

Jangan menaruh data pribadi di atribut span. Trace tersimpan di sistem observability yang sering punya kontrol akses berbeda dari database — dan bertahan berhari-hari. ID internal boleh; email, nama, dan nomor kartu tidak (Fase 5).

Menghubungkan log dengan trace

// Handler slog yang menarik trace_id dari context (Fase 5)
func (h handlerKonteks) Handle(ctx context.Context, r slog.Record) error {
	if sc := trace.SpanContextFromContext(ctx); sc.IsValid() {
		r.AddAttrs(
			slog.String("trace_id", sc.TraceID().String()),
			slog.String("span_id", sc.SpanID().String()))
	}
	return h.Handler.Handle(ctx, r)
}
Alur investigasi yang jadi mungkin:

  Alarm p95 naik
    → dasbor: endpoint mana?
    → trace: di mana waktunya habis? (kueri X 4,2 detik)
    → filter log dengan trace_id itu → detail lengkap permintaannya
    → EXPLAIN kueri X → index hilang

Metrik yang benar-benar dipakai

MetrikLabelUntuk
Durasi permintaan (histogram)pola rute, metode, statusp95/p99 per endpoint
Laju permintaanpola rute, statusTrafik dan laju error
Kueri database (histogram)operasiMenemukan kueri lambat
Pool database—WaitCount, InUse (Fase 4)
Rasio hit cachejenisEfektivitas cache
Kedalaman antreanjenis jobWorker cukup atau tidak (Fase 5)
Goroutine, heap—Kebocoran

Label bervariasi tinggi adalah cara tercepat meledakkan biaya observability. Label path="/produk/58213" menghasilkan satu deret waktu per produk; user_id menghasilkan satu per pengguna. Aturannya: label hanya untuk nilai yang jumlahnya terbatas dan diketahui — pola rute, metode, kode status, nama job. Sisanya masuk ke atribut span atau log.

Sampling

StrategiKapan
100%Pengembangan, staging
Rasio tetap (1–10%)Produksi bertrafik tinggi
ParentBasedSelalu — supaya trace lintas layanan tidak terpotong
Berbasis ekor (tail sampling)Simpan semua yang error atau lambat, sampel sisanya. Butuh collector

Tail sampling adalah yang paling berguna sekaligus paling sering dilewatkan. Dengan sampling 10% biasa, sembilan dari sepuluh permintaan yang gagal tidak punya trace — persis yang kamu butuhkan saat menyelidiki. Tail sampling di OpenTelemetry Collector menyimpan semua trace yang berakhir error atau melebihi ambang durasi, dan menyampel sisanya.

Alarm yang layak dipasang

  1. Laju error 5xx > 1% selama 5 menit.
  2. p95 latensi > 2× baseline selama 10 menit.
  3. Pool database: WaitCount > 0 secara berkelanjutan.
  4. Kedalaman antrean atau umur job tertua melewati ambang.
  5. Memori task > 85% dari batas.
  6. Rasio hit CDN turun tiba-tiba — biasanya deploy yang mengubah header.
  7. Isi DLQ > 0.

Latihan: pasang otelhttp dan otelsql, jalankan collector lokal, dan buka satu trace dari endpoint yang menjalankan tiga kueri. Buktikan ketiga kueri muncul sebagai span terpisah dengan durasinya. Lalu tambahkan trace_id ke log dan telusuri satu permintaan dari trace ke barisnya di log.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.