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 }
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") }
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) }
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"]
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
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
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.
span
akan muncul untuk mendefinisikan berapa lama durasi yang dibutuhkan untuk menjalankan sebuah proses.
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
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.
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
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)
mantap mas