Using Jaeger with OpenTelemetry in Go: A Step-by-Step Guide
In the world of microservices, monitoring and tracing are essential to understand the interactions between services and diagnose issues effectively. OpenTelemetry and Jaeger are popular tools that help in achieving this. This blog post will guide you through setting up Jaeger with OpenTelemetry in a Go application using Docker Compose, along with example implementations for service-a
, service-b
, and service-c
.
Prerequisites
Before we begin, ensure you have the following installed:
Docker
Docker Compose
Go
Setting Up the Environment
We'll use Docker Compose to set up our environment. The provided docker-compose.yml
file includes configurations for three services (service-a
, service-b
, and service-c
), Jaeger, and Elasticsearch (for storing traces).
Here's a breakdown of the docker-compose.yml
file:
services:
service-a:
build:
context: .
dockerfile: Dockerfile
networks:
- service-jaeger
ports:
- "8081:8081"
environment:
- OUTBOUND_B_HOST_PORT=service-b:8082
- OUTBOUND_C_HOST_PORT=service-c:8083
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger-collector:4317
- OTEL_EXPORTER_OTLP_INSECURE=true
- OTEL_TRACES_EXPORTER=jaeger
service-b:
build:
context: .
dockerfile: Dockerfile
networks:
- service-jaeger
environment:
- OUTBOUND_C_HOST_PORT=service-c:8083
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger-collector:4317
- OTEL_EXPORTER_OTLP_INSECURE=true
depends_on:
- jaeger-collector
service-c:
build:
context: .
dockerfile: Dockerfile
networks:
- service-jaeger
environment:
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger-collector:4317
- OTEL_EXPORTER_OTLP_INSECURE=true
depends_on:
- jaeger-collector
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:6.3.1
networks:
- service-jaeger
ports:
- "127.0.0.1:9200:9200"
- "127.0.0.1:9300:9300"
restart: on-failure
environment:
- cluster.name=jaeger-cluster
- discovery.type=single-node
- http.host=0.0.0.0
- transport.host=127.0.0.1
- ES_JAVA_OPTS=-Xms512m -Xmx512m
- xpack.security.enabled=false
volumes:
- esdata:/usr/share/elasticsearch/data
jaeger-collector:
image: jaegertracing/jaeger-collector:1.57
ports:
- "14269:14269"
- "14268:14268"
- "14267:14267"
- "9411:9411"
networks:
- service-jaeger
restart: on-failure
environment:
- SPAN_STORAGE_TYPE=elasticsearch
command: [
"--es.server-urls=http://elasticsearch:9200",
"--es.num-shards=1",
"--es.num-replicas=0",
"--log-level=error"
]
depends_on:
- elasticsearch
jaeger-query:
image: jaegertracing/jaeger-query:1.57
environment:
- SPAN_STORAGE_TYPE=elasticsearch
- no_proxy=localhost
ports:
- "16686:16686"
- "16687:16687"
networks:
- service-jaeger
restart: on-failure
command: [
"--es.server-urls=http://elasticsearch:9200",
"--span-storage.type=elasticsearch",
"--log-level=debug",
]
volumes:
esdata:
driver: local
networks:
service-jaeger:
driver: bridge
Setting Up OpenTelemetry in Go
To instrument your Go application with OpenTelemetry, follow these steps:
1. Install Dependencies:
go get go.opentelemetry.io/otel go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp go get go.opentelemetry.io/otel/sdk/trace go get go.opentelemetry.io/otel/sdk/resource go get go.opentelemetry.io/otel/semconv/v1.4.0
2. Create service-a.go
: Here is the code for service-a
:
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"github.com/albertteoh/jaeger-go-example/lib/ping"
"github.com/albertteoh/jaeger-go-example/lib/tracing"
)
const thisServiceName = "service-a"
func main() {
ctx := context.Background()
log.Println("pre-init")
tracer := tracing.Init(ctx, thisServiceName)
log.Println("post-init")
outboundBHostPort, ok := os.LookupEnv("OUTBOUND_B_HOST_PORT")
if !ok {
log.Println("using default host and port service-b")
outboundBHostPort = "localhost:8082"
}
outboundCHostPort, ok := os.LookupEnv("OUTBOUND_C_HOST_PORT")
if !ok {
log.Println("using default host and port service-c")
outboundCHostPort = "localhost:8083"
}
http.HandleFunc("/ping", func(writer http.ResponseWriter, r *http.Request) {
log.Printf("handling request: %+v", tracer)
ctx, span := tracer.Start(r.Context(), "/ping-both")
defer span.End()
responseB, err := ping.Ping(ctx, outboundBHostPort, tracer)
if err != nil {
log.Fatalf("Error occurred on ping: %s", err)
}
if _, err = writer.Write([]byte(fmt.Sprintf("%s -> %s", thisServiceName, responseB))); err != nil {
log.Fatalf("Error occurred on write: %s", err)
}
responseC, err := ping.Ping(ctx, outboundCHostPort, tracer)
if err != nil {
log.Fatalf("Error occurred on ping: %s", err)
}
if _, err = writer.Write([]byte(fmt.Sprintf("%s -> %s", thisServiceName, responseC))); err != nil {
log.Fatalf("Error occurred on write: %s", err)
}
})
http.HandleFunc("/ping-next", func(writer http.ResponseWriter, r *http.Request) {
log.Printf("handling request: %+v", tracer)
ctx, span := tracer.Start(r.Context(), "/ping-next")
defer span.End()
responseB, err := ping.PingNext(ctx, outboundBHostPort, tracer)
if err != nil {
log.Fatalf("Error occurred on ping: %s", err)
}
if _, err = writer.Write([]byte(fmt.Sprintf("%s -> %s", thisServiceName, responseB))); err != nil {
log.Fatalf("Error occurred on write: %s", err)
}
})
log.Printf("Listening on localhost:8081")
log.Fatal(http.ListenAndServe(":8081", nil))
}
3. Create Dockerfile
: Use this Dockerfile for service-a
, service-b
, and service-c
:
ARG port=8081
ARG app=service-a
FROM golang as builder
ARG app
WORKDIR /app
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY ${app}/ ${app}
COPY lib/ lib
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/${app} ./${app}
# final stage
FROM public.ecr.aws/docker/library/golang:1.22-alpine
ARG app
ARG port
COPY --from=builder /out/${app} /app/
EXPOSE ${port}
ENTRYPOINT ["/app/service-a"]
4. Build and Run the Services: Ensure you have Docker running and execute the following command in the directory containing your docker-compose.yml
file:
docker-compose up --build
Accessing Jaeger UI
After starting the services, you can access the Jaeger UI at http://localhost:16686. Here, you can visualize traces collected from your services.
Conclusion
In this blog post, we covered how to set up Jaeger with OpenTelemetry in a Go application using Docker Compose. By following these steps, you can easily trace and monitor your microservices, making it easier to diagnose and resolve issues.