[Bahasa] Tracer: Open Telemetry, Golang, and Jagger 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 } 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)

Jan 16, 2025 - 07:01
[Bahasa] Tracer: Open Telemetry, Golang, and Jagger 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
}

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.

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

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

Github Project

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