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.

User Order Service Port: 8080 Inventory Service Port: 8081 Payment Service Port: 8082 POST /orders GET /check POST /payments <text x="450" y="375" fill="var(--accent-main)" font-size: 14px; text-anchor="middle">Telemetry to Jaeger (4317)</text>

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!

POST /orders (Order Service)
GET /check (Inventory)
POST /payments (Payment)

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:

  1. Start a span.
  2. Capture arguments (like URL or SQL query).
  3. Inject/Extract context headers.
  4. 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.Context is the bus that carries the Trace ID. You must pass it to every function.

9. Troubleshooting

[!WARNING] No traces appearing?

  1. Check if Jaeger is running: docker ps.
  2. Check the logs. If you see Connection refused, ensure OTEL_EXPORTER_OTLP_ENDPOINT points to the correct host (e.g., host.docker.internal if running app locally and Jaeger in Docker).
  3. For Java, add -Dotel.javaagent.debug=true to see what the agent is doing.

10. Summary

You just:

  1. Launched a Jaeger backend.
  2. Wrote a microservice.
  3. Instrumented it with OpenTelemetry.
  4. Visualized the trace.

In the next module, we will move from “Generic Spans” to Custom Instrumentation, adding your own business logic to the trace.