Image for post
Image for post

Tracing in Node.js with Zipkin

When companies began moving towards Microservices architect, the need of tracing the packets between services appeared to understand and fix bugs and troubleshoot latency problems in service architectures. While logs can tell us whether a specific request failed to execute or not and metrics can help us monitor how many times this request failed and how long the failed request took, traces help us debug the reason why the request failed or took so long to execute by breaking up the execution flow and dissecting it into smaller events.

Zipkin is a distributed tracing system that does that job for you, and a lot of libraries appeared in different programming languages to support its protocol.

The Zipkin UI provides us with some basic options to analyze traced requests, but by using ELK stack; Elasticsearch can be used for long-term retention of the trace data and Kibana will allow you gain much deeper insight into the data. However, I will talk in this article about integrating Zipkin with NodeJS Express project using zipkin-js library.

Tracing middleware

The best way to use zipkin-js is to implement its initialization inside an Express middleware. In addition to zipkin-js we will need to use Zipkin-transport-http to send Zipkin trace data to a configurable HTTP endpoint (Zipkin server), and zipkin-instrumentation-express which is Express middleware and instrumentation that adds Zipkin tracing to the application.

But always its better to write our own middleware just in case we needed anytime to implement another distributed tracing system. So having an abstract middleware for tracing and different implementation will always be the best case.

Next code shows such a middleware that can be used inside Express web application:

const { Tracer, BatchRecorder, jsonEncoder, ExplicitContext } = require(‘zipkin’);const { HttpLogger } = require(‘zipkin-transport-http’);const zipkinMiddleware = require(‘zipkin-instrumentation-express’).expressMiddleware;const { ZIPKIN_SERVER_URL } = process.env;module.exports = class TracingMiddleware {
initializeMiddleware(app) {app.use(zipkinMiddleware({ tracer: this.initializeTracer() }));app.use(this.middlewareFunction);}
initializeTracer() {const ctxImpl = new ExplicitContext();const recorder = new BatchRecorder({ logger: new HttpLogger({endpoint: `${ZIPKIN_SERVER_URL}api/v2/spans`,jsonEncoder: jsonEncoder.JSON_V2,})});this.tracer = new Tracer({ctxImpl,recorder, localServiceName: “service_name”});return this.tracer;}
middlewareFunction(req, res, next) {
res.header(‘traceId’, (req && req._trace_id ? req._trace_id.traceId :‘No Trace id’));

initializeMiddleware function should be called while initializing the Express app passing that app to it as a parameter. That function will use zipkinMiddleware with a tracer initialized using initializeTracer function.

Also, I implemented a middleware function that will inject trace id and span id inside the response header.

trace id and span id stored from the middleware side inside the request object with the names (_trace_id, _span_id). Which is unclear through the library documentation.

Integrate trace id and span id in internal communications between services

One important thing in a distributed tracing system is that each microservice should pass the trace id and span id to the next one when calling it, in NodeJS to call another service you will need to use some Http client library. One of the most famous ones is Axios and luckily there is a library that does that job, zipkin-instrumentation-axios.

This library will wrap Axios and inject the trace id and span id in the headers of all requests to other services. Keep in mind that you will need to use the same previous initialized tracer inside the middleware. So we can modify the initializeTracer function to be as next:

initializeTracer() {const ctxImpl = new ExplicitContext();const recorder = new BatchRecorder({logger: new HttpLogger({endpoint: `${ZIPKIN_URL}api/v2/spans`,jsonEncoder: jsonEncoder.JSON_V2,})});this.tracer = new Tracer({ctxImpl,recorder,localServiceName: “service_name”});this.zipkinAxios = zipkinInstrumentationAxios(axios, { tracer: this.tracer, serviceName: “service_name” });return this.tracer;}

So afterward we can export that Axios instance to be used in other classes that are responsible for communications to other services.

A good way to achieve that is to implement a getter function inside the TracingMiddleware class that will get this initialized wrapped Axios instance. And you can use IoC library to initialize this middleware like addict-ioc with a singleton option so always you will get the same middleware instance whenever you are resolving it.

I hope that will help someone cause I wasted a lot of time just to configure this small thing because of the lack of documentation, while there is much more rich documentation in other programming languages like Java.

I am a Software Architect and AI engineer that have a great passion for integrating technology with businesses and human life.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store