Jaeger is a distributed tracing system, and integrating it with Linkerd gives you visibility into how requests flow across your Kubernetes services.
Let’s see it in action. Imagine a user requests a profile page. This request might hit a frontend service, which then calls a user-service, which in turn calls a profile-service. Without tracing, if the request is slow, you’re blind. You don’t know if the frontend is slow, if user-service is taking too long, or if profile-service is the bottleneck.
With Linkerd and Jaeger, each hop in this request chain generates a trace. When the frontend calls user-service, Linkerd injects trace context into the request headers. The user-service receives this context, continues the trace, and passes it along when it calls profile-service. Jaeger collects these individual trace spans and stitches them together into a single, end-to-end view of the request.
Here’s a simplified example of what that looks like in a Kubernetes manifest for deploying Jaeger:
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
name: my-jaeger-instance
spec:
strategy: allInOne # For simplicity, using all-in-one deployment
allInOne:
image: jaegertracing/all-in-one:latest
options:
log-level: debug
storage:
type: memory # Using in-memory storage for demonstration
ingress:
enabled: true
options:
hosts:
- jaeger.example.com
Linkerd, by default, is configured to automatically propagate trace headers. When Linkerd sees an incoming request, it checks for specific trace headers (like traceparent or b3). If they exist, it forwards them to the destination service. If they don’t exist, and tracing is enabled, Linkerd can initiate a new trace. To ensure Linkerd is sending traces to your Jaeger instance, you typically configure its tracing component. This might involve setting the collector endpoint.
For instance, in your Linkerd configuration (often managed via linkerd-config ConfigMap or during linkerd install/linkerd upgrade), you’d specify something like this under the tracing section:
apiVersion: v1
kind: ConfigMap
metadata:
name: linkerd-config
namespace: linkerd
data:
config.yaml: |
proxy:
# ... other proxy settings
controller:
# ... other controller settings
tracing:
sampling: 100.0 # Sample 100% of traces
collector:
host: my-jaeger-instance-collector.default.svc.cluster.local # Replace with your Jaeger collector service name
port: 14268 # Default Jaeger gRPC port
The sampling: 100.0 means every single request will be traced. For high-traffic environments, you might reduce this to save resources. The host and port tell Linkerd where to send the trace data.
When a request comes into your mesh, Linkerd’s data plane proxy intercepts it. If tracing is enabled, the proxy will:
- Check for incoming trace headers.
- If found, it continues the trace by adding its own span and forwarding the headers.
- If not found, and if it’s the start of a new logical operation (e.g., an external request entering the mesh), it can start a new trace.
- It then sends the generated span data (information about the request, latency, source, destination, etc.) to the configured Jaeger collector.
The magic Linkerd performs is automatically injecting and propagating these trace headers. You don’t need to modify your application code to pass trace IDs between services. Linkerd handles it at the network level. The allInOne Jaeger deployment is convenient for development or small clusters. For production, you’d typically deploy Jaeger with separate components for the collector, query service, and storage (e.g., Cassandra or Elasticsearch).
The most surprising thing is how little application code modification is required. Linkerd’s proxies act as transparent intermediaries, understanding and propagating tracing context without your application needing to be aware of it. This is achieved by the proxies conforming to established tracing header formats like W3C Trace Context or B3. When a request arrives, the proxy inspects the headers. If it finds traceparent or x-b3-traceid, it knows a trace is already in progress. It then adds its own span information and forwards these headers to the next service. If no trace headers are present, and the request is deemed to be the start of a new operation (based on Linkerd’s configuration and heuristics), it will generate a new trace ID and initiate the tracing process.
Once Jaeger has the data, you can access its UI (often via kubectl port-forward or an Ingress) to visualize the traces. You’ll see a waterfall diagram showing each service involved in a request, the time spent in each, and any errors. This makes identifying performance bottlenecks or debugging distributed system failures significantly easier.
The next concept you’ll likely explore is configuring sampling rates to manage the volume of trace data sent to Jaeger, especially in production environments.