[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)
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.