DEV Community

Cover image for [Indonesia] Tracer: Open Telemetry, Golang, and Jaeger Simple Implementation
Wahyu Rudiyan Saputra
Wahyu Rudiyan Saputra

Posted on • Edited on

[Indonesia] Tracer: Open Telemetry, Golang, and Jaeger Simple Implementation

Tracer

Tracer merupakan bagian dari Observability yang mengambil peran penting dalam implementasi Microservices Architecture dan memberikan gambaran 'Jejak' dari proses yang berjalan di sebuah logic aplikasi.

Sederhananya, pada Webservice, Tracer akan memberikan gambaran mengenai seberapa lama waktu eksekusi dari suatu logic dengan memancarkan sinyal Trace. Nantinya, Tracer ini akan dapat divisualisasikan dan dilihat dalam bentuk nested span setelah memancarkan sinyal melalui Exporter ke Collector. [OpenTelemetry: Traces]

OpenTelemetry

Untuk dapat memancarkan sinyal Traces yang nantinya dapat di Collect oleh Collector, Webservice membutuhkan OpenTelemetry sebagai pustaka yang telah menjadi standar protokol Observability yang biasa disebut OpenTelemetry Protocol (OTLP). [OpenTelemetry: Language - Go]

Jaeger

Visualisasi dari Traces signal sangat dibutuhkan untuk memberikan gambaran dari proses apa saja yang terjadi pada Webservice. Jaeger merupakan Open-Source platform yang telah mendukung OTLP dengan memanfaat protokol komunikasi HTTP atau gRPC. [Jaeger]

Implementasi di Golang

Implementasi Tracer pada bahasa pemrograman Golang akan menerapkan kasus sederhana dimana Webservice hanya akan memberikan balikan data dengan durasi respons yang berbeda. Pustaka yang akan digunakan yaitu:

  • Chi: HTTP Framework
  • OpenTelemetry: Telemetry Signaling

Setup OpenTelemetry sebagai modul Telemetry

Implementasi modul Telemetry di direktori pkg/telemetry/telemetry.go:

package telemetry import ( "context" "errors" "time" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/trace" ) // enumeration constant for which protocol used const ( HTTP uint8 = iota GRPC ) // setup client to connect web-service with Jaeger func SetupTraceClient(ctx context.Context, protocol uint8, endpoint string) otlptrace.Client { switch protocol { case GRPC: return otlptracegrpc.NewClient(otlptracegrpc.WithEndpoint(endpoint), otlptracegrpc.WithInsecure(), otlptracegrpc.WithCompressor("gzip")) default: return otlptracehttp.NewClient(otlptracehttp.WithEndpoint(endpoint), otlptracehttp.WithInsecure(), otlptracehttp.WithCompression(otlptracehttp.NoCompression)) } } func setupTraceProvider(ctx context.Context, traceClient otlptrace.Client) (*trace.TracerProvider, error) { // set resource res, err := resource.New( ctx, resource.WithFromEnv(), ) if err != nil { return nil, err } // init trace exporter traceExporter, err := otlptrace.New(ctx, traceClient) if err != nil { return nil, err } // init trace exporter traceProvider := trace.NewTracerProvider( trace.WithBatcher( traceExporter, trace.WithBatchTimeout(time.Duration(time.Second*3)), ), trace.WithResource(res), // Discover and provide attributes from OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME environment variables. ) return traceProvider, nil } func SetupTelemetrySDK(ctx context.Context, traceClient otlptrace.Client) (func(context.Context) error, error) { var err error var shutdownFuncs []func(context.Context) error shutdown := func(ctx context.Context) error { var err error for _, fn := range shutdownFuncs { err = errors.Join(err, fn(ctx)) } shutdownFuncs = nil return err } handleErr := func(inErr error) { err = errors.Join(inErr, shutdown(ctx)) } traceProvider, err := setupTraceProvider(ctx, traceClient) if err != nil { handleErr(err) return shutdown, err } shutdownFuncs = append(shutdownFuncs, traceProvider.Shutdown) otel.SetTracerProvider(traceProvider) return shutdown, nil } 
Enter fullscreen mode Exit fullscreen mode

Kemudian, setup konfigurasi Telemetry di main function main.go:

package main import ( "context" "fmt" "net/http" "os" "os/signal" "syscall" "time" "github.com/go-chi/chi/v5" "github.com/wahyurudiyan/medium/otel-jaeger/config" "github.com/wahyurudiyan/medium/otel-jaeger/pkg/telemetry" "github.com/wahyurudiyan/medium/otel-jaeger/router" ) func SetupTelemetry(ctx context.Context, config *config.Config) (func(context.Context) error, error) { otlpCli := telemetry.SetupTraceClient(ctx, telemetry.GRPC, config.JaegerGRPCEndpoint) shutdownFn, err := telemetry.SetupTelemetrySDK(ctx, otlpCli) return shutdownFn, err } func main() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() cfg := config.Get() shutdownFn, err := SetupTelemetry(ctx, cfg) if err != nil { shutdownFn(ctx) panic(err) } r := chi.NewRouter() r.Route("/api", func(r chi.Router) { router.Router(r) }) srv := http.Server{ Addr: "0.0.0.0:8080", Handler: r, } go func() { fmt.Println("Server running at port:", srv.Addr) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { fmt.Printf("listen: %s\n", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit defer shutdownFn(ctx) fmt.Println("Server is shutting down...") if err := srv.Shutdown(context.Background()); err != nil { fmt.Println("Server forced to shutdown:", err) } fmt.Println("Server exiting") } 
Enter fullscreen mode Exit fullscreen mode

Penggunaan Tracer di handler pada file router/router.go untuk dapat memancarkan sinyal Traces:

package router import ( "encoding/json" "net/http" "time" "github.com/go-chi/chi/v5" "github.com/wahyurudiyan/medium/otel-jaeger/pkg/random" "go.opentelemetry.io/otel" ) var ( tracer = otel.Tracer("WebServer-Otel-Jaeger") ) func getUserHandler(w http.ResponseWriter, r *http.Request) { _, span := tracer.Start(r.Context(), "GetUser") defer span.End() user := struct { Name string Email string Password string }{ Name: "John Doe", Email: "john@email.com", Password: "Super5ecr3t!", } blob, _ := json.Marshal(&user) sleepDuration := time.Duration(time.Millisecond * time.Duration(random.GenerateRandNum())) time.Sleep(sleepDuration) w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write(blob) } func Router(router chi.Router) { router.Get("/user", getUserHandler) } 
Enter fullscreen mode Exit fullscreen mode

Deployment

Konfigurasi docker untuk Build Webservice ini, memanfaatkan mekanisme Multi-Stage Build Image pada Dockerfile:

FROM golang:1.23.4 AS build WORKDIR /src COPY . . RUN go get -v RUN CGO_ENABLED=0 go build -o /bin/service . FROM alpine:latest WORKDIR /app COPY --from=build /bin/service /bin/service CMD ["/bin/service"] 
Enter fullscreen mode Exit fullscreen mode

Selanjutnya, build image akan dilakukan melalui file docker-compose.yaml dengan konfigurasi berikut:

services: web-service: container_name: service build: context: . dockerfile: Dockerfile environment: OTEL_SERVICE_NAME: service-otel-jaeger JAEGER_GRPC_ENDPOINT: jaeger:4317 entrypoint: ["service"] ports: - 8080:8080 jaeger: container_name: jaeger image: jaegertracing/all-in-one:latest environment: COLLECTOR_ZIPKIN_HOST_PORT: :9411 ports: - 16686:16686 - 4317:4317 - 4318:4318 - 9411:9411 
Enter fullscreen mode Exit fullscreen mode

Pada service.jaeger.ports, port yang diekspose merupakan port untuk:

  • 16686: Jaeger Dashboard
  • 4317: Jaeger OTLP Protobuf dengan protokol gRPC
  • 4318: Jaeger OTLP Protobuf/JSON dengan protokol HTTP
  • 9411: Zipkin Collector

Menjalankan aplikasi yang telah didefinisikan pada docker-compose.yaml, dapat digunakan perintah:

docker compose up --build 
Enter fullscreen mode Exit fullscreen mode

Setelah aplikasi berjalan, dapat dicoba hit aplikasi pada endpoint http://127.0.0.1:8080/api/user, jika Webservice dan aplikasi telah terkoneksi, maka akan tampil nama service seperti pada gambar.

service_connected

span akan muncul untuk mendefinisikan berapa lama durasi yang dibutuhkan untuk menjalankan sebuah proses.

span_result

Load Test

Sekarang mari kita coba menggunakan CLI tool hey [https://github.com/rakyll/hey] untuk menjalankan load-test. Perintah berikut dapat digunakan untuk melakukan load-test sederhana:

hey -c 100 -z 10m http://127.0.0.1:8080/api/user 
Enter fullscreen mode Exit fullscreen mode

Perintah tersebut akan menjalankan load-test untuk 100 request per second (RPS) selama 10 menit. Hasil yang akan muncul pada halaman Jaeger UI akan terlihat seperti berikut.

tracing_result

Jika loadtest telah selesai dijalankan, maka akan ada report dari hasil loadtest.

Summary: Total: 600.9545 secs Slowest: 1.2674 secs Fastest: 0.1005 secs Average: 0.5553 secs Requests/sec: 179.9071 Total data: 7568120 bytes Size/request: 70 bytes Response time histogram: 0.101 [1] | 0.217 [21210] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 0.334 [10993] |■■■■■■■■■■■■■■■■■■■■ 0.451 [10719] |■■■■■■■■■■■■■■■■■■■■ 0.567 [10919] |■■■■■■■■■■■■■■■■■■■■ 0.684 [10830] |■■■■■■■■■■■■■■■■■■■■ 0.801 [10749] |■■■■■■■■■■■■■■■■■■■■ 0.917 [21675] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 1.034 [10902] |■■■■■■■■■■■■■■■■■■■■ 1.151 [113] | 1.267 [5] | Latency distribution: 10% in 0.2009 secs 25% in 0.3027 secs 50% in 0.6010 secs 75% in 0.8028 secs 90% in 0.9604 secs 95% in 1.0028 secs 99% in 1.0069 secs Details (average, fastest, slowest): DNS+dialup: 0.0000 secs, 0.1005 secs, 1.2674 secs DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs req write: 0.0000 secs, 0.0000 secs, 0.0237 secs resp wait: 0.5552 secs, 0.1005 secs, 1.2660 secs resp read: 0.0001 secs, 0.0000 secs, 0.0216 secs Status code distribution: [200] 108116 responses 
Enter fullscreen mode Exit fullscreen mode

Github Project

Bagi yang ingin mencoba atau melihat kode secara penuh, dapat melakukan klon pada repository berikut: https://github.com/wahyurudiyan/otel-jaeger.

Top comments (1)

Collapse
 
magicwarms profile image
Andhana Utama

mantap mas