Jaeger
- Spans - A span represents a logical unit of work in Jaeger that has an operation name, the start time of the operation, and the duration.
- Trace - A trace is a data/execution path through the system.
- Agent - listens for spans sent over UDP, which it batches and sends to the collector.
- Collector - receives traces from Jaeger agents and runs them through a processing pipeline.
- Query - retrieves traces from storage and hosts a UI to display them.
Jaeger Implementation¶
Implementing tracing is very straightforward and is included in the starter kit.
Let's take a look at how a microservice uses Jaeger to trace a subset of incoming requests. The code snippets below have been simplified for demonstration purposes.
First, we installed a package called jaeger-client
, which we can
use to create spans and report them to the collector. We can find this dependency listed in the package.json
file:
"dependencies": {
// other dependencies omitted for brevity
"jaeger-client": "^3.18.0",
},
Next, we instantiate a new tracer
that is configured to report spans to the Jaeger collector service, which is deployed
as a component of Istio. Istio's ingress gateways use the Zipkin format for traces,
so we'll configure our tracer to use this format as well.
const createTracer = () => {
tracer = initTracer(
{
serviceName: "marketsummary",
reporter: {
logSpans: true,
collectorEndpoint: "http://jaeger-collector.istio-system.svc.cluster.local:14268/api/traces",
},
},
{}
);
const codec = new ZipkinB3TextMapCodec({ urlEncoding: true });
tracer.registerInjector(opentracing.FORMAT_HTTP_HEADERS, codec);
tracer.registerExtractor(opentracing.FORMAT_HTTP_HEADERS, codec);
};
Our Istio ingress gateways are configured to trace a small subset of incoming requests. When the ingress gateway decides to trace a request, it generates a parent span, and attaches metadata about this span to the request headers. Our microservice needs to detect the existence of these headers in order extract the trace metadata, so a child span can be created and attached to the parent.
We can hook into the request lifecycle to do this prior to processing the request in our controller:
server.ext("onPreHandler", (request, h) => {
const { path, method } = request.route;
const parent = tracer.extract(
opentracing.FORMAT_HTTP_HEADERS,
request.headers
);
if (parent.toSpanId()) { // if the request contains trace metadata
// create new span representing the work done by the microservice
const span = tracer.startSpan("findMarketSummary", {
childOf: parent,
tags: {
"http.locale": getLocaleFromRequest(request),
"http.user-agent": request.headers["user-agent"],
"http.host": request.headers.host,
"http.path": path,
"http.method": method,
},
});
// inject the child span metadata into the http request headers
tracer.inject(
span,
opentracing.FORMAT_HTTP_HEADERS,
request.headers
);
// store the span in a map that we can access later
spans[span.context().toSpanId()] = span;
}
return h.continue;
});
After our controller has processed the request, we can hook into the request lifecycle again to finish the span we created when we received the request:
server.ext("onPreResponse", (request, h) => {
const spanContext = tracer.extract(
opentracing.FORMAT_HTTP_HEADERS,
request.headers
);
if (spanContext.toSpanId()) {
// fetch span from map
const span = spans[spanContext.toSpanId()];
if (span) {
if (boom.isBoom(request.response, 500)) { // if the response is an error
// add information about the error to the span
span.log({
error: request.response.stack,
response: request.response.output,
});
span.setTag("error", true);
}
// report the span to the jaeger collector
span.finish();
// remove the span from the map
delete spans[spanContext.toSpanId()];
}
}
return h.continue;
});