Getting Started with Jaeger: Distributed Tracing over OpenTelemetry
Getting Started with Jaeger: Distributed Tracing over OpenTelemetry
A request comes in, passes through a gateway, several services, a database, then responds. Which hop was slow? Logs scattered across machines are hard to stitch together. Distributed tracing strings every hop of that one request into a tree of spans, and Jaeger is the tool for collecting and viewing that tree. This post covers what Jaeger is, why OpenTelemetry is the key piece, how gortex’s Tracer interface plugs in, and how to run it locally with Docker.
What distributed tracing solves
Under microservices, one external request fans out into several internal calls. When something breaks, a single machine’s logs only show you a fragment. The unit of distributed tracing is the span: a named piece of work with a start and end time, such as one HTTP handler or one DB query. Spans have parent / child relationships, and strung together they form a trace, the full request tree. Read the tree and you can see where the time went and which hop threw.
The key is that trace context has to propagate across services: the upstream puts a trace ID into a header, the downstream picks it up, and only then does the chain join up. Within a process, gortex carries the span through context (see the observability post); across services it relies on OTel’s propagation.
What Jaeger is
Jaeger is a CNCF distributed tracing platform that collects, stores, and queries spans, and ships a web UI that draws each trace as a timeline waterfall. A real deployment splits into a few roles: the collector receives spans, a storage backend persists them, and query plus UI let you look.
You don’t need that split during development. There’s an official all-in-one image that packs collector, query, and UI into a single container with in-memory storage. It runs as soon as it starts, and a restart wipes it clean — exactly right for local work.
OpenTelemetry: the common wire
The keyword here is OpenTelemetry (OTel). It’s a vendor-neutral observability standard: a set of instrumentation APIs / SDKs, plus a transport protocol, OTLP. Your code only ever produces spans against OTel; where they get shipped is a separate concern.
The point is that instrumentation and backend are decoupled. Ship to Jaeger today, switch to Grafana Tempo or Zipkin tomorrow, and what changes is the exporter config, not the instrumentation scattered through your code.
Gortex builds that decoupling into the framework: it depends on a single Tracer interface and ships an OTelTracerAdapter that turns a gortex span into an OTel span (severity mapping, tags, and status all carried over). So the “gortex → OTel → Jaeger” path is there off the shelf, and your business code only ever sees the Tracer interface. Setup looks roughly like this:
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
gotel "github.com/yshengliao/gortex/observability/otel"
"github.com/yshengliao/gortex/observability/tracing"
)
// Standard OTel SDK: an OTLP exporter pointed at Jaeger's 4317
exp, _ := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("localhost:4317"),
otlptracegrpc.WithInsecure(),
)
otel.SetTracerProvider(sdktrace.NewTracerProvider(sdktrace.WithBatcher(exp)))
// Gortex side: wrap an adapter, and spans flow into OTel → Jaeger
tracer := gotel.NewOTelTracerAdapter(tracing.NewSimpleTracer(), "gortex")
The exporter is standard OTel SDK machinery, not something gortex invented; gortex only converts its own spans into OTel spans.
A quick start with Docker on OrbStack
The fastest route is compose, using a ready-made template (jaeger, running jaegertracing/all-in-one:latest). On OrbStack, docker compose up -d takes a few seconds:
git clone https://github.com/yshengliao/docker-compose-templates
cd docker-compose-templates/jaeger
docker compose up -d
The ports you’ll actually use:
16686: the Jaeger UI — openhttp://localhost:16686in a browser to query traces.4317: OTLP gRPC, and4318: OTLP HTTP — point your code’s exporter here.9411: the Zipkin-compatible endpoint.
The template already sets COLLECTOR_OTLP_ENABLED=true, so OTLP ingestion is on by default. Point the exporter above at localhost:4317, send a few requests, and you’ll watch traces land in the UI one by one.
Swapping in Grafana works the same
Don’t want the Jaeger UI and would rather see everything in Grafana? The same OTel instrumentation doesn’t move. On the Grafana side, Tempo is the trace backend, and Tempo also speaks OTLP: change the exporter’s endpoint from Jaeger to Tempo, add a Tempo data source in Grafana, and traces show up there — sitting next to your metrics and logs.
In other words, the backend is a choice: Jaeger, Grafana Tempo, Zipkin all work. What makes that choice possible is the OTel standard plus gortex’s adapter — code against the interface, swap the backend at will. That’s the payoff of interface-driven design on the operations side.
Takeaways
- The unit of distributed tracing is the span, strung into a trace (the request tree); it only joins up if trace context propagates across services.
- Jaeger is a CNCF tracing platform; the all-in-one image packs collector / query / UI into one container, ideal for local development.
- OpenTelemetry is the vendor-neutral standard (APIs/SDKs plus OTLP) that decouples instrumentation from backend; gortex plugs in via the
Tracerinterface andOTelTracerAdapter. - With the jaeger template,
docker compose up -d: UI on16686, OTLP on4317/4318, point the exporter there. - The backend swaps to Grafana Tempo (also OTLP) without touching a line of instrumentation — the payoff of the OTel standard plus the adapter pattern.
Source and more framework detail: yshengliao/gortex.
Drafted with Claude · Jaeger all-in-one + OpenTelemetry + Docker (OrbStack) · part of the 2026-06-13 blog renovation, paint still drying.