Auto Instrumentation

In this step, we will auto-instrument our Node.js application to collect traces from our services and send them to the console for now. In a later step, we will update the application to send the traces to Cloud Observability.

Add auto-instrumentation

  1. Edit the package.json file for each of the services (/users/package.json and /web/package.json). Update the "dependencies:" to add the OpenTelemetry libraries for auto-instrumentation:

    "@opentelemetry/api": "^1.7.0",
    "@opentelemetry/auto-instrumentations-node": "^0.40.3",
    "@opentelemetry/sdk-node": "^0.46.0",
    
  2. Create a folder named /opentelemetry and then another folder named /src inside of it.

    folder hierarchy

  3. In the /opentelemetry/src folder, create a file named instrumentation.js with the code below:

    const opentelemetry = require("@opentelemetry/sdk-node");
    const {
      getNodeAutoInstrumentations,
    } = require("@opentelemetry/auto-instrumentations-node");
    
    const sdk = new opentelemetry.NodeSDK({
      traceExporter: new opentelemetry.tracing.ConsoleSpanExporter(),
      instrumentations: [getNodeAutoInstrumentations()],
    });
    
    sdk.start();
    

    This code imports the OpenTelemetry libraries and configures the auto-instrumentation.

  4. Edit the docker-compose.yml file to add this instrumentation.js file to each of our service containers. In the volumes section for both the web: and users: containers, add the line below:

    - ./opentelemetry/src/instrumentation.js:/usr/src/app/src/instrumentation.js:z
    
  5. Edit the nodemon.json file for each of the services to add instrumentation.js as a requirement.

    Replace

    "exec": "node ./src/index.js"
    

    with

    "exec": "node --require ./src/instrumentation.js ./src/index.js"
    

Rebuild and Test

We need to rebuild the application containers to apply and test these changes. Run the following command in your terminal:

docker-compose up --build

Now test your application by going to http://localhost:4000 (Web) and http://localhost:4000/api/data (Users) in your browser. For the Web service, you should see a Hello world! message in the browser. For the Users service, you should see some JSON data.

Go back to your console and look at the output. You should see JSON trace data for each request you just made to your services. These will look something like this:

web         | {
web         |   traceId: 'cda9ba9751d3e4b5d1fd869b82e2a805',
web         |   parentId: '40b48ad3c6d6f4ab',
web         |   name: 'HTTP GET',
web         |   id: 'f1bb0349d731999e',
web         |   kind: 2,
web         |   timestamp: 1666214938759446,
web         |   duration: 57222,
web         |   attributes: {
web         |     'http.url': 'http://service/api/data',
web         |     'http.method': 'GET',
web         |     'http.target': '/api/data',
web         |     'net.peer.name': 'service',
web         |     'net.peer.ip': '172.30.0.4',
web         |     'net.peer.port': 80,
web         |     'http.host': 'service:80',
web         |     'http.response_content_length_uncompressed': 11227,
web         |     'http.status_code': 200,
web         |     'http.status_text': 'OK',
web         |     'http.flavor': '1.1',
web         |     'net.transport': 'ip_tcp'
web         |   },
web         |   status: { code: 1 },
web         |   events: []
web         | }
...         | ...
users     | {
users     |   traceId: 'cda9ba9751d3e4b5d1fd869b82e2a805',
users     |   parentId: '74ff7c45f9f3769e',
users     |   name: 'middleware - query',
users     |   id: '0144a51838269b81',
users     |   kind: 0,
users     |   timestamp: 1666214938771876,
users     |   duration: 481,
users     |   attributes: {
users     |     'http.route': '/',
users     |     'express.name': 'query',
users     |     'express.type': 'middleware'
users     |   },
users     |   status: { code: 0 },
users     |   events: []
users     | }

Each of those objects is a span and together they make up a single trace. Look closely at each span and you will see they have different names and attribute values. This data is being captured automatically by the auto-instrumentation for each operation in the lifecycle of the request.

With just a few lines of code, you are now able to collect a significant amount of telemetry data from your application. This data allows you to much better understand the state of your application as requests flow through it. But auto instrumentation doesn’t know your business logic and doesn’t know all the details related to the context of each request. This is where manual instrumentation should be used to enrich your data and reduce or eliminate blind spots. Let’s add some manual instrumentation to our services next

next: Manual Instrumentation