Tracing

Tracing?

Tracing adalah metode untuk memantau perjalanan sebuah permintaan melalui berbagai komponen atau layanan dalam sistem terdistribusi. Dengan tracing, Anda dapat:

  • Melacak Satu Permintaan: Mengikuti alur permintaan dari awal hingga akhir, misalnya, dari browser pengguna ke API, database, dan layanan eksternal.

  • Menganalisis Data Agregat: Memahami perilaku sistem secara keseluruhan, seperti latensi rata-rata atau titik kegagalan.

  • Mendiagnosis Masalah: Mengidentifikasi di mana masalah terjadi, seperti layanan mana yang lambat atau gagal.

Tracing sangat berguna untuk:

  • Troubleshooting: Menemukan akar masalah, misalnya, mengapa sebuah permintaan lambat.

  • Pemantauan Performa: Mengukur latensi dan ketergantungan antar layanan.

  • Analisis Perilaku Sistem: Memahami bagaimana layanan berinteraksi dalam arsitektur mikro.

Tracing menggunakan dua konsep utama: traces dan spans.

Konsep Traces dan Spans

Traces

Trace adalah objek induk yang merepresentasikan alur data atau jalur eksekusi melalui sistem. Trace mencakup semua operasi yang terkait dengan satu permintaan.

Atribut Trace:

  • Identifier: ID unik untuk mengidentifikasi trace (misalnya, trace_id).

  • Name: Deskripsi pekerjaan yang dilakukan (misalnya, "Process HTTP Request").

  • Timing Details: Timestamp awal dan akhir trace secara keseluruhan.

Spans

Span adalah unit kerja logis dalam trace, yang merepresentasikan operasi spesifik (misalnya, panggilan API, query database).

Atribut Span:

  • Trace Identifier: Menghubungkan span ke trace induk.

  • Identifier: ID unik untuk span (misalnya, span_id).

  • Parent Span Identifier: Menunjukkan hubungan dengan span induk.

  • Name: Deskripsi operasi (misalnya, "GET /api/users").

  • Timing Details: Timestamp awal dan akhir span.

Hubungan Traces dan Spans:

  • Satu trace terdiri dari beberapa span, membentuk directed acyclic graph (DAG).

  • Setiap span mencatat operasi spesifik dan waktu yang dibutuhkan, membantu mengidentifikasi bagian sistem yang lambat.

Ilustrasi: Berikut adalah visualisasi hubungan traces dan spans:

Penjelasan Ilustrasi:

  • Trace mencakup seluruh alur permintaan HTTP.

  • Span mencatat operasi seperti pemrosesan HTTP, query database, dan panggilan API eksternal.

  • Struktur DAG menunjukkan hubungan hierarkis, misalnya, span "Query Database" memiliki sub-span seperti "Fetch User Data".

Protokol Tracing

Ada beberapa protokol tracing yang umum digunakan, masing-masing dengan fitur spesifik:

  1. OTLP (OpenTelemetry Protocol):

    • Fitur: Mendukung atribut span, propagasi konteks, event span, link span, dan jenis span (span kind).

    • Kegunaan: Standar modern yang vendor-agnostic, cocok untuk sistem terdistribusi kompleks.

  2. Zipkin:

    • Fitur: Mendukung tag span, anotasi, propagasi konteks, dan jenis span.

    • Kegunaan: Populer untuk aplikasi berbasis mikro yang membutuhkan tracing sederhana.

  3. Jaeger:

    • Fitur: Mendukung tag span, log span, referensi span, dan propagasi konteks (tergantung format).

    • Catatan: Format Jaeger Proto telah dihentikan demi OTLP.

Rekomendasi: OpenTelemetry (OTLP) adalah pilihan terbaik saat ini karena standar terbuka, dukungan luas, dan fleksibilitas untuk berbagai bahasa pemrograman, termasuk Go.

Praktik Terbaik untuk Tracing

Untuk mengimplementasikan tracing dengan efektif, pertimbangkan praktik terbaik berikut:

  1. Perhatikan Performa:

    • Tracing dapat menambah overhead (latensi, memori, waktu startup).

    • Gunakan sampling untuk mengurangi jumlah span yang dikirim (misalnya, 10% dari semua trace).

    • Contoh: OpenTelemetry Collector mendukung konfigurasi sampling dari 0% hingga 100%.

  2. Kelola Biaya:

    • Tracing menghasilkan banyak data, meningkatkan biaya jaringan dan penyimpanan.

    • Terapkan sampling (hanya kirim sebagian trace), filtering (batasi trace yang disimpan), dan retention (atur durasi penyimpanan data).

  3. Pastikan Propagasi Konteks:

    • Propagasi konteks (misalnya, trace_id) antar layanan sangat penting untuk menjaga kesinambungan trace.

    • Tanpa propagasi yang benar, trace akan terpecah, mengurangi kegunaan.

  4. Gunakan Instrumentasi yang Efisien:

    • Automatic Instrumentation: Cepat diimplementasikan, tetapi kurang kontrol dan dapat menyebabkan overhead.

    • Manual Instrumentation: Lebih kompleks, tetapi memungkinkan kontrol penuh atas data yang dihasilkan.

    • Gunakan library seperti OpenTelemetry untuk menyederhanakan instrumentasi.

Contoh Implementasi Tracing di Go dengan OpenTelemetry

Berikut adalah contoh aplikasi RESTful sederhana menggunakan Go 1.22 (dengan routing baru dari net/http) yang mengimplementasikan tracing dengan OpenTelemetry. Aplikasi ini memiliki dua endpoint (/api/users dan /api/orders) dan mencatat trace untuk setiap permintaan.

Struktur Proyek

go-opentelemetry-tracing/
├── main.go
├── Dockerfile
├── docker-compose.yml
├── jaeger-config.yml

Kode Go

package main

import (
	"context"
	"fmt"
	"log"
	"math/rand"
	"net/http"
	"time"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
	"go.opentelemetry.io/otel/trace"
)

// Nama tracer
const tracerName = "go-tracing-example"

// Inisialisasi OpenTelemetry
func initTracer() func(context.Context) error {
	// Buat exporter untuk Jaeger (menggunakan OTLP over HTTP)
	exporter, err := otlptracehttp.New(context.Background(),
		otlptracehttp.WithEndpoint("jaeger:4318"),
		otlptracehttp.WithInsecure(),
	)
	if err != nil {
		log.Fatalf("Gagal membuat exporter: %v", err)
	}

	// Buat resource untuk mengidentifikasi aplikasi
	resource, err := resource.New(context.Background(),
		resource.WithAttributes(
			semconv.ServiceNameKey.String("go-tracing-example"),
		),
	)
	if err != nil {
		log.Fatalf("Gagal membuat resource: %v", err)
	}

	// Buat tracer provider dengan sampling 100%
	tracerProvider := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(resource),
		sdktrace.WithSampler(sdktrace.AlwaysSample()),
	)

	// Set tracer provider global
	otel.SetTracerProvider(tracerProvider)

	// Set propagator untuk konteks
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
		propagation.TraceContext{},
		propagation.Baggage{},
	))

	// Fungsi untuk shutdown tracer
	return func(ctx context.Context) error {
		return tracerProvider.Shutdown(ctx)
	}
}

func main() {
	// Inisialisasi tracer
	shutdown := initTracer()
	defer func() {
		if err := shutdown(context.Background()); err != nil {
			log.Printf("Gagal shutdown tracer: %v", err)
		}
	}()

	// Seed random untuk simulasi latensi
	rand.Seed(time.Now().UnixNano())

	// Buat router baru
	mux := http.NewServeMux()

	// Handler untuk /api/users
	mux.HandleFunc("GET /api/users", func(w http.ResponseWriter, r *http.Request) {
		// Dapatkan tracer
		tracer := otel.Tracer(tracerName)

		// Mulai span untuk permintaan
		ctx, span := tracer.Start(r.Context(), "GET /api/users")
		defer span.End()

		// Simulasi latensi (50ms hingga 500ms)
		time.Sleep(time.Duration(rand.Intn(450)+50) * time.Millisecond)

		// Simulasi operasi database
		_, dbSpan := tracer.Start(ctx, "Query Database")
		time.Sleep(time.Duration(rand.Intn(100)+20) * time.Millisecond)
		dbSpan.End()

		// Tulis respons
		fmt.Fprintln(w, `[{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]`)
	})

	// Handler untuk /api/orders
	mux.HandleFunc("GET /api/orders", func(w http.ResponseWriter, r *http.Request) {
		// Dapatkan tracer
		tracer := otel.Tracer(tracerName)

		// Mulai span untuk permintaan
		ctx, span := tracer.Start(r.Context(), "GET /api/orders")
		defer span.End()

		// Simulasi latensi (50ms hingga 500ms)
		time.Sleep(time.Duration(rand.Intn(450)+50) * time.Millisecond)

		// Simulasi panggilan API eksternal
		_, apiSpan := tracer.Start(ctx, "Call External API")
		time.Sleep(time.Duration(rand.Intn(150)+30) * time.Millisecond)
		apiSpan.End()

		// Tulis respons
		fmt.Fprintln(w, `[{"order_id": 101, "item": "Book"}, {"order_id": 102, "item": "Pen"}]`)
	})

	// Start server
	log.Println("Starting server on :8080")
	if err := http.ListenAndServe(":8080", mux); err != nil {
		log.Fatalf("Server failed: %v", err)
	}
}

Penjelasan Kode:

  • OpenTelemetry Setup:

    • Menggunakan otlptracehttp untuk mengirim trace ke Jaeger melalui OTLP over HTTP.

    • Mengatur TracerProvider dengan sampling 100% (AlwaysSample) untuk mencatat semua trace.

    • Mengatur propagasi konteks untuk memastikan trace_id diteruskan antar layanan.

  • Tracing:

    • Endpoint /api/users mencatat span untuk permintaan HTTP dan simulasi query database.

    • Endpoint /api/orders mencatat span untuk permintaan HTTP dan simulasi panggilan API eksternal.

    • Setiap span mencatat waktu mulai dan selesai, membantu mengidentifikasi latensi.

  • Simulasi Latensi: Menggunakan rand untuk mensimulasikan latensi realistis.

Konfigurasi Docker

Dockerfile

# Use official Go 1.22 image
FROM golang:1.22-alpine

# Set working directory
WORKDIR /app

# Copy source code
COPY main.go .

# Install dependencies
RUN go mod init go-opentelemetry-tracing
RUN go get go.opentelemetry.io/otel
RUN go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
RUN go get go.opentelemetry.io/otel/sdk/trace
RUN go get go.opentelemetry.io/otel/semconv/v1.17.0
RUN go get go.opentelemetry.io/otel/propagation

# Build the application
RUN go build -o app main.go

# Expose port
EXPOSE 8080

# Run the application
CMD ["./app"]

docker-compose.yml

version: '3.8'
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    networks:
      - tracing

  jaeger:
    image: jaegertracing/all-in-one:1.58
    ports:
      - "16686:16686" # Jaeger UI
      - "4318:4318"   # OTLP HTTP
    environment:
      - COLLECTOR_OTLP_ENABLED=true
    networks:
      - tracing

networks:
  tracing:
    driver: bridge

Cara Menjalankan

  1. Buat Direktori Proyek:

    mkdir go-opentelemetry-tracing
    cd go-opentelemetry-tracing

    Simpan file main.go, Dockerfile, dan docker-compose.yml.

  2. Jalankan dengan Docker Compose:

    docker-compose up --build
    • Aplikasi Go berjalan di http://localhost:8080.

    • Jaeger UI berjalan di http://localhost:16686.

  3. Uji Endpoint:

    curl http://localhost:8080/api/users
    curl http://localhost:8080/api/orders
  4. Lihat Trace:

    • Buka http://localhost:16686 di browser.

    • Pilih layanan go-tracing-example dan cari trace berdasarkan trace_id.

    • Anda akan melihat DAG dari span, misalnya:

      • Span "GET /api/users" dengan sub-span "Query Database".

      • Span "GET /api/orders" dengan sub-span "Call External API".

Contoh Visualisasi di Jaeger:

  • Trace untuk /api/users akan menunjukkan waktu total dan kontribusi latensi dari "Query Database".

  • Anda dapat mengidentifikasi jika "Query Database" memakan waktu lama, menunjukkan potensi bottleneck.

Praktik Terbaik dalam Kode

  1. Gunakan Nama Span yang Deskriptif: Misalnya, "GET /api/users" atau "Query Database".

  2. Terapkan Sampling: Untuk produksi, gunakan ParentBased(TraceIDRatioBased(0.1)) untuk mengurangi overhead.

  3. Propagasi Konteks: Pastikan trace_id diteruskan antar layanan menggunakan header HTTP (otomatis dengan OpenTelemetry).

  4. Tambahkan Atribut: Tambahkan metadata seperti http.status_code atau user.id ke span untuk analisis lebih mendalam.

Kesimpulan

Tracing adalah alat penting dalam observability untuk melacak permintaan dan memahami perilaku sistem terdistribusi. Dengan konsep traces dan spans, protokol seperti OpenTelemetry, dan praktik terbaik seperti sampling dan propagasi konteks, Anda dapat mendiagnosis masalah dengan cepat dan meningkatkan performa sistem. Contoh implementasi di Go dengan OpenTelemetry menunjukkan bagaimana tracing dapat diintegrasikan dalam aplikasi RESTful sederhana, dengan visualisasi di Jaeger untuk analisis. Dengan mengikuti praktik terbaik, tracing dapat memberikan wawasan berharga tanpa mengorbankan performa atau biaya.

Last updated