How to Use OpenTelementry to Trace Node.js Applications

Introduction

OpenTelemetry is a set of open source libraries and tools that provide observability for distributed systems. Observability means being able to monitor and understand the behavior and performance of your system through metrics, logs, and traces. Traces are especially useful for debugging and troubleshooting complex interactions between microservices or functions.

In this blog post, I will show you how to instrument your Node.js applications with OpenTelemetry and visualize the traces with Jaeger UI. Jaeger is a popular open source tracing system that supports various protocols and backends. You will learn how to:

  • Install and configure OpenTelemetry Node SDK
  • Create and export traces to Jaeger
  • View and analyze traces in Jaeger UI

Install and configure OpenTelemetry Node SDK

The OpenTelemetry Node SDK is a collection of packages that allow you to instrument your Node.js applications with OpenTelemetry. You can install the SDK using npm or yarn:

npm install --save @opentelemetry/api @opentelemetry/node @opentelemetry/tracing @opentelemetry/exporter-jaeger

The SDK consists of four main components:

  • @opentelemetry/api: The core API that defines the interfaces and constants for OpenTelemetry.
  • @opentelemetry/node: The Node.js specific implementation of the API, which automatically instruments common modules and frameworks, such as HTTP, Express, MongoDB, etc.
  • @opentelemetry/tracing: The tracing module that provides the functionality to create and manage spans, which are the basic units of a trace.
  • @opentelemetry/exporter-jaeger: The exporter module that allows you to send your traces to Jaeger.

To configure the SDK, you need to create a file called tracer.js in your project root directory, and add the following code:

const opentelemetry = require('@opentelemetry/api');
const { NodeTracerProvider } = require('@opentelemetry/node');
const { SimpleSpanProcessor } = require('@opentelemetry/tracing');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');

// Initialize the tracer provider
const provider = new NodeTracerProvider();

// Register the provider with the global tracer registry
provider.register();

// Create a Jaeger exporter
const exporter = new JaegerExporter({
  serviceName: 'my-node-service', // Change this to your service name
});

// Add the exporter to the provider
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));

// Get the global tracer
const tracer = opentelemetry.trace.getTracer('my-node-service'); // Change this to your service name

// Export the tracer
module.exports = {
  tracer,
};

This code does the following:

  • It initializes a NodeTracerProvider, which is a class that implements the TracerProvider interface and provides access to the tracer instances.
  • It registers the provider with the global tracer registry, which is a singleton object that holds a reference to the current tracer provider.
  • It creates a JaegerExporter, which is a class that implements the SpanExporter interface and allows you to export your spans to Jaeger. You need to specify the serviceName parameter, which is the name of your service that will appear in Jaeger UI.
  • It adds the exporter to the provider using a SimpleSpanProcessor, which is a class that implements the SpanProcessor interface and exports the spans immediately after they are ended.
  • It gets the global tracer using the getTracer method, which returns a tracer instance that is associated with the given name and version. You need to use the same name as the serviceName parameter for the exporter.
  • It exports the tracer so that you can use it in your application code.

Create and export traces to Jaeger

To create and export traces to Jaeger, you need to use the tracer instance that you exported from the tracer.js file. You can use the startSpan method to create a new span, which represents a single operation or unit of work within a trace. You can also use the withSpan method to execute a function within the context of a span, which automatically propagates the span context to the child spans.

For example, suppose you have a simple Express application that has two routes: /hello and /world. You can instrument the application with OpenTelemetry as follows:

const express = require('express');
const { tracer } = require('./tracer');

const app = express();
const port = 3000;

// Define a middleware function that creates a span for each request
const traceMiddleware = (req, res, next) => {
  // Get the incoming request headers
  const headers = req.headers;

  // Get the span context from the headers
  const parentSpanContext = tracer
    .getHttpTextPropagator()
    .extract(headers, (h, k) => h[k.toLowerCase()]);

  // Start a span for the request
  const span = tracer.startSpan(req.path, {
    parent: parentSpanContext,
    kind: opentelemetry.SpanKind.SERVER,
    attributes: {
      'http.method': req.method,
      'http.url': req.url,
    },
  });

  // Set the span as the current context
  opentelemetry.context.withSpan(span, () => {
    // Call the next middleware function
    next();
  });

  // End the span when the response is finished
  res.on('finish', () => {
    span.end();
  });
};

// Use the middleware function
app.use(traceMiddleware);

// Define a route handler for /hello
app.get('/hello', (req, res) => {
  // Start a span for the hello operation
  const span = tracer.startSpan('hello');

  // Simulate some work
  setTimeout(() => {
    // Set an attribute to the span
    span.setAttribute('my.attribute', 'hello');

    // End the span
    span.end();

    // Send a response
    res.send('Hello World!');
  }, 1000);
});

// Define a route handler for /world
app.get('/world', (req, res) => {
  // Start a span for the world operation
  const span = tracer.startSpan('world');

  // Simulate some work
  setTimeout(() => {
    // Set an attribute to the span
    span.setAttribute('my.attribute', 'world');

    // End the span
    span.end();

    // Send a response
    res.send('Hello World!');
  }, 2000);
});

// Start the server
app.listen(port, () => {
  console.log(`Example app listening at [7](http://localhost)`);
});

This code does the following:

  • It defines a middleware function that creates a span for each incoming request. It uses the getHttpTextPropagator method to extract the span context from the request headers, which allows you to link the spans across different services. It also sets some attributes to the span, such as the HTTP method and URL, which provide useful information for the trace. It then sets the span as the current context using the withSpan method, and ends the span when the response is finished.
  • It defines two route handlers for /hello and /world, which create child spans for the hello and world operations, respectively. It also sets some custom attributes to the spans, such as my.attribute, which can be used for filtering or searching the traces. It then ends the spans after simulating some work with setTimeout.
  • It starts the server and listens on port 3000.

View and analyze traces in Jaeger UI

To view and analyze the traces in Jaeger UI, you need to run Jaeger as a Docker container on your local machine. You can use the following command to pull and run the Jaeger image:

docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
  -p 5775:5775/udp \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14268:14268 \
  -p 14250:14250 \
  -p 9411:9411 \
  jaegertracing/all-in-one:1.22

This command will run Jaeger in an all-in-one mode, which includes the agent, collector, query, and UI components. It will also expose the necessary ports for the different protocols and endpoints. You can access the Jaeger UI at 8.

To test the application, you can use a tool like curl or Postman to send some requests to the /hello and /world routes. For example:

curl [9](http://localhost)
curl [10](http://localhost)

After sending some requests, you can go to the Jaeger UI and select your service name (my-node-service) from the dropdown menu. You can also adjust the time range and filters as you wish. You should see something like this:

!Jaeger UI showing the traces for the Node.js application

You can click on any trace to see more details, such as the span duration, attributes, logs,

and errors. You can also use the timeline view to see the span hierarchy and dependencies. For example:

!Jaeger UI showing the details of a trace for the Node.js application

You can see that the trace consists of three spans: one for the request, one for the hello operation, and one for the world operation. You can also see the attributes and logs for each span, such as the HTTP method, URL, and custom attribute. You can also see the duration and status of each span, which can help you identify performance bottlenecks or errors.

Conclusion

In this blog post, I have shown you how to use OpenTelemetry to trace Node.js applications and visualize the traces with Jaeger UI. You have learned how to:

  • Install and configure OpenTelemetry Node SDK
  • Create and export traces to Jaeger
  • View and analyze traces in Jaeger UI

Tracing is a powerful technique for observability and debugging of distributed systems. OpenTelemetry is a standard and open source solution for tracing, which supports various languages and frameworks. Jaeger is a popular and open source tracing system, which provides a user-friendly interface for viewing and analyzing traces.

I hope this blog post has helped you to understand how to use OpenTelemetry and Jaeger to trace your Node.js applications. If you want to learn more about OpenTelemetry and Jaeger, you can check out the following resources:

Happy tracing! 😊

Leave a Comment

Your email address will not be published. Required fields are marked *