Zero to Tracing in 15 Minutes
[!IMPORTANT] By the end of this module, you will have a running microservices cluster with full distributed tracing, implemented in both Java and Go.
Imagine your e-commerce site is slow. Customers are complaining that checkout takes 5 seconds. You check your Order Service logs—everything looks fine, it responds in 50ms. You check the Database—fast as lightning.
Where is the latency?
Without Distributed Tracing, you are debugging in the dark. You have isolated islands of logs but no map of the journey. Tracing turns the lights on. It connects the dots between services, showing you exactly where time is spent.
1. What We’re Building
We will build a classic microservices E-commerce flow. A user places an order, which triggers an inventory check and a payment process.
2. Prerequisites
Before we start, ensure you have:
- Docker & Docker Compose: To run the Jaeger backend.
- Java 17+: For the Java examples.
- Go 1.21+: For the Go examples.
- 15 Minutes: Of focused time.
3. Step 1: Set Up Jaeger
We need a backend to receive and visualize our traces. Jaeger is the industry standard for this.
Create a docker-compose.yml:
version: '3.8'
services:
jaeger:
image: jaegertracing/all-in-one:1.53
container_name: jaeger
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
environment:
- COLLECTOR_OTLP_ENABLED=true
Run it:
docker-compose up -d
Open http://localhost:16686. You should see the Jaeger UI. It’s empty for now—let’s fill it up.
4. Step 2: The Service Code
We need some code to trace. We will write a simple Order Service that calls an Inventory Service.
Order Service (Java)
For Java, we don’t need to change any code! The OpenTelemetry Java Agent will automatically instrument our application at runtime.
Order Handler (Go)
In Go, we use otelgin middleware to automatically start spans for incoming requests.
// main.go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func main() {
// 1. Initialize Tracer (Boilerplate hidden for brevity, see setup step)
shutdown := initTracer("order-service")
defer shutdown(context.Background())
r := gin.Default()
// 2. Add OpenTelemetry Middleware
// This starts a span for every request
r.Use(otelgin.Middleware("order-service"))
r.POST("/orders", func(c *gin.Context) {
// 3. Propagate Context downstream
// We must use otelhttp to instrument the HTTP client
client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
// Call Inventory
req, _ := http.NewRequestWithContext(c.Request.Context(), "GET", "http://localhost:8081/inventory/check", nil)
resp, _ := client.Do(req)
defer resp.Body.Close()
c.JSON(http.StatusOK, gin.H{"status": "CONFIRMED"})
})
r.Run(":8080")
}
Note: Go requires explicit context passing (c.Request.Context()) and client instrumentation (otelhttp), unlike Java’s complete magic.
5. Step 3: Running with Instrumentation
This is where the magic happens.
Java Agent (Auto-Instrumentation)
Download the agent jar and run your application with the -javaagent flag.
# Download the agent
wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
# Run the Order Service
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=order-service \
-Dotel.exporter.otlp.endpoint=http://localhost:4317 \
-jar order-service.jar
Go SDK (Explicit Setup)
Go applications compile to machine code, so we can’t inject a JAR. We must compile the OTel SDK into our binary.
You need to set environment variables to tell the SDK where to send data:
export OTEL_SERVICE_NAME=order-service
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_TRACES_SAMPLER=always_on
go run main.go
The initTracer function in our code reads these variables to configure the exporter.
6. Step 4: Configuration Generator
Stop guessing environment variables. Use this generator to get the exact flags you need.
Java Agent Arguments
-javaagent:opentelemetry-javaagent.jar \ -Dotel.service.name=my-service \ -Dotel.exporter.otlp.endpoint=http://localhost:4317
Environment Variables (Go/Docker)
export OTEL_SERVICE_NAME=my-service export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
7. Step 5: Visualize the Trace
Once your data hits Jaeger, it looks like this. Click on the spans below to see what data is captured!
Select a span to view details
Click on the colored bars above.
8. How It Works: The Magic Explained
How does “Zero Code” actually work?
Java: Bytecode Manipulation
The Java Agent uses the Java Instrumentation API. When your application starts, the agent intercepts the loading of classes (like RestTemplate or HttpServlet). It inserts bytecode before and after critical methods to:
- Start a span.
- Capture arguments (like URL or SQL query).
- Inject/Extract context headers.
- End the span and record duration.
Go: Compile-Time Wrappers
Go is compiled to machine code, so we can’t change it at runtime. Instead, we use Library Instrumentation.
- Middleware: Wraps your HTTP handler.
- Context: Go’s
context.Contextis the bus that carries the Trace ID. You must pass it to every function.
9. Troubleshooting
[!WARNING] No traces appearing?
- Check if Jaeger is running:
docker ps.- Check the logs. If you see
Connection refused, ensureOTEL_EXPORTER_OTLP_ENDPOINTpoints to the correct host (e.g.,host.docker.internalif running app locally and Jaeger in Docker).- For Java, add
-Dotel.javaagent.debug=trueto see what the agent is doing.
10. Summary
You just:
- Launched a Jaeger backend.
- Wrote a microservice.
- Instrumented it with OpenTelemetry.
- Visualized the trace.
In the next module, we will move from “Generic Spans” to Custom Instrumentation, adding your own business logic to the trace.