

0 / 2 embers
0 / 3000 xp
click for more info
Complete a lesson to start your streak
click for more info
Difficulty: 10
click for more info
Not enough gems
Cost: 6 gems
1: Tracing
incomplete
2: Installing Jaeger
incomplete
3: Request Tracing
incomplete
4: Instrumenting Traces
incomplete
5: Adding Spans
incomplete
6: Reading Traces
incomplete
7: Distributed Tracing
incomplete
Back
ctrl+,
Next
ctrl+.
This lesson's interactive features are locked, please to keep using them
We now technically have tracing in our application, as you proved by viewing traces in Jaeger. But they're not very useful traces yet. Each trace only has a single span. All this really tells us is how many HTTP requests we're getting, and how long each one takes.
Traces really start to shine when they expose multiple, nested spans. You're responsible for deciding when and where to add spans, but they generally belong around any "interesting" operations.
We could add a span for each function call, but this quickly becomes both tedious and noisy. Imagine a span for our logging middleware. It would add a new child span to every request that mirrors the current root span in virtually every way – it would just be a few ms shorter. Not useful!
I like to add a span for each handler. We only have a few of those in this application, so it's not a big chore. And it starts to give us useful information. We can see how each handler behaves in the real world:
First, we need a tracer. A tracer is a lightweight object that creates spans. A simple option is to use a package-level variable and initialize it in your initTracing function.
You'll need to add the "go.opentelemetry.io/otel/trace" import to your tracing.go file:
import "go.opentelemetry.io/otel/trace"
var tracer trace.Tracer
func initTracing(ctx context.Context) (func(context.Context) error, error) {
// ... existing setup ...
otel.SetTracerProvider(tp)
tracer = tp.Tracer("example.com/myservice") // this is added
return tp.Shutdown, nil
}
The string "example.com/myservice" is a scope name – it groups spans by source. A single scope name per service is typical, though larger applications might use separate tracers for different components (e.g. "boot.dev/linko/handlers" or "boot.dev/linko/store").
With the tracer in place, adding a span to a handler is straightforward:
func quoteHandler(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "calculate_quote")
defer span.End()
// do work with ctx...
}
This little piece of boilerplate code will be repeated all over your codebase, so it's good to understand exactly what it does.
ctx, span := tracer.Start(r.Context(), "calculate_quote")
The tracer.Start method starts a new span, which contains three things:
tracer objectotelhttp.NewHandler middleware we set up in the last section.defer span.End()
This is where the span is ended. You will typically defer this function immediately after calling tracer.Start().
When span.End() is called, the elapsed time between tracer.Start() and span.End() calls is calculated, and the span is "closed".
When the root span finally closes (in this example, that means when the otelhttp.NewHandler middleware returns, and the request is served), that signals that the trace is complete!
handlerIndex = "handler.index"handlerLogin = "handler.login"handlerShortenLink = "handler.shorten_link"OTEL_EXPORTER_OTLP_TRACES_INSECURE=true OTEL_SERVICE_NAME=linko go run .
Run and submit the CLI tests from the root of the Linko repo.