This Quick Start shows you how to use OpenTelemetry in your Node.js app to:

  • Configure a tracer
  • Generate trace data
  • Propagate context over HTTP
  • Export to Lightstep to view trace data

The full code for the example in this guide can be found here.

Requirements

Node.js version 12 or newer

Setup

To use OpenTelemetry, you need to install the API, SDK, span processor and exporter packages. The version of the SDK and API used in this guide is 0.5, the most current version as of writing.

1
2
3
4
npm install @opentelemetry/api \
  @opentelemetry/node \
  @opentelemetry/plugin-http \
  @opentelemetry/tracing

Collect Trace Data

You need to configure a TracerProvider to collect tracing information. A tracer is an object that tracks the currently active span and allows you to create (or activate) new spans. As spans are created and completed, the tracer dispatches them to an exporter that can send the spans to a backend system for analysis.

In this first example, the TracerProvider is configured using ConsoleSpanExporter, which prints tracing information to the console.

  1. Import OpenTelemetry and create a tracer configured to send data to the console, saving it as first-trace.js.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    const opentelemetry = require("@opentelemetry/api");
    const { NodeTracerProvider } = require("@opentelemetry/node");
    const { SimpleSpanProcessor, ConsoleSpanExporter } = require("@opentelemetry/tracing");
    
    // Create an exporter for sending span data
    const exporter = new ConsoleSpanExporter();
    
    // Create a provider for activating and tracking spans
    const tracerProvider = new NodeTracerProvider({
      plugins: {
        http: {
          enabled: true,
          path: "@opentelemetry/plugin-http"
        }
      }
    });
    
    // Configure a span processor for the tracer
    tracerProvider.addSpanProcessor(new SimpleSpanProcessor(exporter));
    
    // Register the tracer
    tracerProvider.register();
    
  2. Now create a Span object. A span is the building block of a trace and is a named, timed operation that represents a piece of the workflow in the distributed system. Multiple spans are pieced together to create a trace.

    The only required parameter is the span’s name. But often, more information about the span is needed so that you can effectively debug and monitor in your backend system. Attributes allow you to add name/value pairs to describe the span. Events represent an event that occurred at a specific time within a span’s workload.

    Along with the name, add attributes for the platform and version and an event.

    1
    2
    3
    4
    5
    6
    
    const tracer = opentelemetry.trace.getTracer();
    
    const span = tracer.startSpan("foo");
    span.setAttribute("platform", "osx");
    span.setAttribute("version", "1.2.3");
    span.addEvent("event in foo");
    
  3. Add a child span to the existing span. To create a relationship between spans we pass the parent span to the child span on creation.

    1
    2
    3
    
    const childSpan = tracer.startSpan("bar", {
      parent: span
    });
    
  4. Operations that are represented by spans are not considered complete until the span is ended. You need to call end on the parent and child span objects in order to signal to the exporter that the tracing information is ready to be sent to its destination.

    1
    2
    
    childSpan.end();
    span.end();
    
  5. Run the service and view the data in your console. node first-tracer.js

    The output on the console should show us something like this:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
     {
    traceId: 'b80ea2f259f04a66d351dc15a3bf98e1',
    parentId: 'deccb275750a9b90',
    name: 'bar',
    id: 'f845d919d7fab0fd',
    kind: 0,
    timestamp: 1584633330763944,
    duration: 40,
    attributes: {},
    status: { code: 0 },
    events: []
    }
    {
    traceId: 'b80ea2f259f04a66d351dc15a3bf98e1',
    parentId: undefined,
    name: 'foo',
    id: 'deccb275750a9b90',
    kind: 0,
    timestamp: 1584633330763662,
    duration: 3485,
    attributes: { platform: 'osx', version: '1.2.3' },
    status: { code: 0 },
    events: [ { name: 'event in foo', attributes: undefined, time: [Array] } ]
    }
    

Propagate the Context Over HTTP

You can instrument a request from a client to a server which allows you to propagate the context across process boundaries. This ensures that you can correlate events that happen in separate processes into a single trace.

  1. Instrument the server, saving it as server.js

    1
    2
    3
    
    npm install express \
      axios \
      @opentelemetry/plugin-express
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    
    const opentelemetry = require("@opentelemetry/api");
    const { NodeTracerProvider } = require("@opentelemetry/node");
    const {
      SimpleSpanProcessor,
      ConsoleSpanExporter
    } = require("@opentelemetry/tracing");
    // Create an exporter for sending spans data
    const exporter = new ConsoleSpanExporter({ serviceName: "demo-service" });
    
    // Create a provider for activating and tracking spans
    const tracerProvider = new NodeTracerProvider({
      plugins: {
        express: {
          enabled: true,
          path: "@opentelemetry/plugin-express"
        }
        http: {
          enabled: true,
          path: "@opentelemetry/plugin-http"
        }
      }
    });
    
    // Configure a span processor for the tracer
    tracerProvider.addSpanProcessor(new SimpleSpanProcessor(exporter));
    
    // Register the tracer
    tracerProvider.register();
    
    const tracer = opentelemetry.trace.getTracer();
    
    // --- Simple Express app setup
    const express = require("express");
    const port = 3000;
    const app = express();
    
    // Attach a mock middleware, automatically picked up by the express-plugin
    app.use(function mockMiddleware(req, res, next) {
      console.log("Mock middleware");
      next();
    });
    
    // Mount our demo route
    app.get("/demo", (req, res) => {
      const span = tracer.startSpan("handler");
    
      // Annotate our span to capture metadata about our operation
      span.addEvent("doing work");
    
      mockAdditionalWork(span).then(() => {
        // Be sure to end the span!
        span.end();
    
        res.send("OpenTelemetry Fun 🎉");
      });
    });
    
    // Start the server
    app.listen(port, () => console.log(`Example app listening on port ${port}!`));
    
    // --- Mock function, demonstrates creating child spans
    
    async function mockAdditionalWork(parentSpan) {
      // Start a child span using the passed parent span to maintain context
      const span = tracer.startSpan("mockAdditionalWork()", {
        parent: parentSpan
      });
    
      // Additional metadata can be attached to spans
      span.setAttribute("key", "value");
      span.addEvent(
        "Additional work happening, eg calling a database or making a request to another service"
      );
    
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          // Be sure to end the child span!
          span.end();
          resolve();
        }, 500);
      });
    }
    
  2. Instrument a client, saving it as client.js The @opentelemetry/node and @opentelemetry/plugin-http packages in the client and server will automatically configure the correct W3C headers to propagate context across the wire.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    
     const opentelemetry = require("@opentelemetry/api");
     const { NodeTracerProvider } = require("@opentelemetry/node");
     const { SimpleSpanProcessor, ConsoleSpanExporter } = require("@opentelemetry/tracing");
    
     // --- Setup the tracer for the client
    
     const tracerProvider = new NodeTracerProvider({
     plugins: {
         http: {
         enabled: true,
         path: "@opentelemetry/plugin-http"
         }
     }
     });
    
     const exporter = ConsoleSpanExporter({ serviceName: "demo-client" });
    
     tracerProvider.addSpanProcessor(
     new SimpleSpanProcessor(exporter)
     );
     tracerProvider.register();
    
     // --- Make a request to the example service
    
     const api = require("@opentelemetry/api");
     const axios = require("axios");
    
     const tracer = opentelemetry.trace.getTracer("node-opentelemetry-example");
    
     function clientDemoRequest() {
     console.log("Starting client demo request");
    
     const span = tracer.startSpan("clientDemoRequest()", {
         parent: tracer.getCurrentSpan(),
         kind: api.SpanKind.CLIENT
     });
    
     tracer.withSpan(span, async () => {
         await axios.get("http://localhost:3000/demo");
         span.setStatus({ code: api.CanonicalCode.OK });
    
         span.end();
    
         // The process must remain alive for the duration of the exporter flush
         // timeout or spans might be dropped
         console.log("Client request complete, waiting to ensure spans flushed...");
         setTimeout(() => {
         console.log("Done 🎉");
         }, 2000);
     });
     }
    
     clientDemoRequest();
    
  3. Launch the server in a terminal: node server.js
  4. Open a second terminal to run the client. node client.js

    The output from running those scripts looks like this:

    Client terminal output

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    
     {
         traceId: '88cca14e747ea59556e8917cf5cffa15',
         parentId: '2b8b0b082a9f55b3',
         name: 'GET /demo',
         id: 'dc4f74124705c792',
         kind: 2,
         timestamp: 1584634202722768,
         duration: 525753,
         attributes: {
             component: 'http',
             'http.url': 'http://localhost:3000/demo',
             'http.method': 'GET',
             'http.target': '/demo',
             'net.peer.name': 'localhost',
             'net.peer.ip': '127.0.0.1',
             'net.peer.port': 3000,
             'http.host': 'localhost:3000',
             'http.status_code': 200,
             'http.status_text': 'OK',
             'http.flavor': '1.1',
             'net.transport': 'IP.TCP'
         },
         status: { code: 0 },
         events: []
     }
     {
         traceId: '88cca14e747ea59556e8917cf5cffa15',
         parentId: undefined,
         name: 'clientDemoRequest()',
         id: '2b8b0b082a9f55b3',
         kind: 2,
         timestamp: 1584634202719909,
         duration: 530975,
         attributes: {},
         status: { code: 0 },
         events: []
     }
    

    Server terminal output

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    
     {
         traceId: '88cca14e747ea59556e8917cf5cffa15',
         parentId: '26c1005f71c22bb2',
         name: 'middleware - mockMiddleware',
         id: '24d0c64204469e4a',
         kind: 0,
         timestamp: 1584634202736289,
         duration: 100,
         attributes: {
             component: 'express',
             'http.route': '/',
             'express.name': 'mockMiddleware',
             'express.type': 'middleware'
         },
         status: { code: 0 },
         events: []
     }
     {
         traceId: '88cca14e747ea59556e8917cf5cffa15',
         parentId: '26c1005f71c22bb2',
         name: 'request handler',
         id: 'f10040d9ada251e2',
         kind: 0,
         timestamp: 1584634202736667,
         duration: 466,
         attributes: {
             component: 'express',
             'http.route': '/demo',
             'express.name': '/demo',
             'express.type': 'request_handler'
         },
         status: { code: 0 },
         events: []
     }
     {
         traceId: '88cca14e747ea59556e8917cf5cffa15',
         parentId: '3ee231dde605c32b',
         name: 'mockAdditionalWork()',
         id: 'bc5648771ab6f0e0',
         kind: 0,
         timestamp: 1584634202736997,
         duration: 502869,
         attributes: { key: 'value' },
         status: { code: 0 },
         events: [
             {
                 name: 'Additional work happening, eg calling a databse or making a request to another service',
                 attributes: undefined,
                 time: [Array]
             }
         ]
     }
     {
         traceId: '88cca14e747ea59556e8917cf5cffa15',
         parentId: '26c1005f71c22bb2',
         name: 'handler',
         id: '3ee231dde605c32b',
         kind: 0,
         timestamp: 1584634202736802,
         duration: 504350,
         attributes: {},
         status: { code: 0 },
         events: [ { name: 'doing work', attributes: undefined, time: [Array] } ]
     }
     {
         traceId: '88cca14e747ea59556e8917cf5cffa15',
         parentId: 'dc4f74124705c792',
         name: 'GET /demo',
         id: '26c1005f71c22bb2',
         kind: 1,
         timestamp: 1584634202732617,
         duration: 512848,
         attributes: {
             'http.url': 'http://localhost:3000/demo',
             'http.host': 'localhost:3000',
             'net.host.name': 'localhost',
             'http.method': 'GET',
             'http.target': '/demo',
             'http.route': '/demo',
             'http.user_agent': 'axios/0.19.2',
             'http.flavor': '1.1',
             'net.transport': 'IP.TCP',
             component: 'http',
             'net.host.ip': '::ffff:127.0.0.1',
             'net.host.port': 3000,
             'net.peer.ip': '::ffff:127.0.0.1',
             'net.peer.port': 64454,
             'http.status_code': 200,
             'http.status_text': 'OK'
         },
         status: { code: 0 },
         events: []
     }
    

    Looking at the output, you can see that the same "traceId": "88cca14e747ea59556e8917cf5cffa15" is propagated across the spans from the client and the server.

Export and Explore Trace Data in Lightstep

To make use of the trace data being generated, exporters send the data to various supported collectors. The ConsoleSpanExporter is really useful when debugging while instrumenting. But now, you’ll use lightstep-opentelemetry-exporter to send tracing data to Lightstep’s public Satellites.

  1. Install the exporter library. npm install lightstep-opentelemetry-exporter

  2. Update the exporter configuration in client.js and server.js to use the LightstepExporter. You’ll need to provide a name for your service and a Lightstep access token.

    If you don’t already have a Lightstep account, you can create a free account here.

    1
    2
    3
    4
    5
    6
    
     const { LightstepExporter } = require('lightstep-opentelemetry-exporter');
    
     const exporter = new LightstepExporter({
     serviceName: 'exporter-demo-server',
     token: <YOUR_ACCESS_TOKEN_HERE>,
     });
    
  3. Restart server.js and run client.js again. Tracing information is now available in your Lightstep project!

  4. Click Explorer in the left navigation bar to see the trace data.

  5. Click on any span in the Trace Analysis table to view the full trace.