Propagation

Context propagation for the JS SDK

With context propagation, Signals can be correlated with each other, regardless of where they are generated. Although not limited to tracing, context propagation allows traces to build causal information about a system across services that are arbitrarily distributed across process and network boundaries.

For the vast majority of use cases, libraries that natively support OpenTelemetry or instrumentation libraries will automatically propagate trace context across services for you. It is only in rare cases that you will need to propagate context manually.

To learn more, see Context propagation.

Automatic context propagation

Instrumentation libraries like @opentelemetry/instrumentation-http or @opentelemetry/instrumentation-express propagate context across services for you.

If you followed the Getting Started Guide you can create a client application that queries the /rolldice endpoint.

Start by creating a new folder called dice-client and install the required dependencies:

npm init -y
npm install typescript \
  ts-node \
  @types/node \
  undici \
  @opentelemetry/instrumentation-undici \
  @opentelemetry/sdk-node

# initialize typescript
npx tsc --init
npm init -y
npm install undici \
  @opentelemetry/instrumentation-undici \
  @opentelemetry/sdk-node

Next, create a new file called client.ts (or client.js) with the following content:

import { NodeSDK } from '@opentelemetry/sdk-node';
import {
  SimpleSpanProcessor,
  ConsoleSpanExporter,
} from '@opentelemetry/sdk-trace-node';
import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';

const sdk = new NodeSDK({
  spanProcessors: [new SimpleSpanProcessor(new ConsoleSpanExporter())],
  instrumentations: [new UndiciInstrumentation()],
});
sdk.start();

import { request } from 'undici';

request('http://localhost:8080/rolldice').then((response) => {
  response.body.json().then((json: any) => console.log(json));
});
const { NodeSDK } = require('@opentelemetry/sdk-node');
const {
  SimpleSpanProcessor,
  ConsoleSpanExporter,
} = require('@opentelemetry/sdk-trace-node');
const {
  UndiciInstrumentation,
} = require('@opentelemetry/instrumentation-undici');

const sdk = new NodeSDK({
  spanProcessors: [new SimpleSpanProcessor(new ConsoleSpanExporter())],
  instrumentations: [new UndiciInstrumentation()],
});
sdk.start();

const { request } = require('undici');

request('http://localhost:8080/rolldice').then((response) => {
  response.body.json().then((json) => console.log(json));
});

Make sure that you have the instrumented version of app.ts (or app.js) from the Getting Started running in one shell:

$ npx ts-node --require ./instrumentation.ts app.ts
Listening for requests on http://localhost:8080
$ node --require ./instrumentation.js app.js
Listening for requests on http://localhost:8080

Start a second shell and run the client.ts (or client.js):

npx ts-node client.ts
node client.js

Both shells should emit span details to the console. The client output looks similar to the following:

{
  resource: {
    attributes: {
      // ...
    }
  },
  traceId: 'cccd19c3a2d10e589f01bfe2dc896dc2',
  parentId: undefined,
  traceState: undefined,
  name: 'GET',
  id: '6f64ce484217a7bf',
  kind: 2,
  timestamp: 1718875320295000,
  duration: 19836.833,
  attributes: {
    'url.full': 'http://localhost:8080/rolldice',
    // ...
  },
  status: { code: 0 },
  events: [],
  links: []
}

Take note of the traceId (cccd19c3a2d10e589f01bfe2dc896dc2) and ID (6f64ce484217a7bf). Both can be found in the output of client as well:

{
  resource: {
    attributes: {
      // ...
  },
  traceId: 'cccd19c3a2d10e589f01bfe2dc896dc2',
  parentId: '6f64ce484217a7bf',
  traceState: undefined,
  name: 'GET /rolldice',
  id: '027c5c8b916d29da',
  kind: 1,
  timestamp: 1718875320310000,
  duration: 3894.792,
  attributes: {
    'http.url': 'http://localhost:8080/rolldice',
    // ...
  },
  status: { code: 0 },
  events: [],
  links: []
}

Your client and server application successfully report connected spans. If you send both to a backend now the visualization will show this dependency for you.

Manual context propagation

In some cases, it is not possible to propagate context automatically as outlined in the previous section. There might not be an instrumentation library that matches a library you’re using to have services communicate with one another. Or you might have requirements that these libraries can’t fulfill even if they existed.

When you must propagate context manually, you can use the context API.

Generic example

The following generic example demonstrates how you can propagate trace context manually.

First, on the sending service, you’ll need to inject the current context:

// Sending service
import { Context, propagation, trace } from '@opentelemetry/api';

// Define an interface for the output object that will hold the trace information.
interface Carrier {
  traceparent?: string;
  tracestate?: string;
}

// Create an output object that conforms to that interface.
const output: Carrier = {};

// Serialize the traceparent and tracestate from context into
// an output object.
//
// This example uses the active trace context, but you can
// use whatever context is appropriate to your scenario.
propagation.inject(context.active(), output);

// Extract the traceparent and tracestate values from the output object.
const { traceparent, tracestate } = output;

// You can then pass the traceparent and tracestate
// data to whatever mechanism you use to propagate
// across services.
// Sending service
const { context, propagation } = require('@opentelemetry/api');
const output = {};

// Serialize the traceparent and tracestate from context into
// an output object.
//
// This example uses the active trace context, but you can
// use whatever context is appropriate to your scenario.
propagation.inject(context.active(), output);

const { traceparent, tracestate } = output;
// You can then pass the traceparent and tracestate
// data to whatever mechanism you use to propagate
// across services.

On the receiving service, you’ll need to extract context (for example, from parsed HTTP headers) and then set them as the current trace context.

// Receiving service
import { Context, propagation, trace, Span } from '@opentelemetry/api';

// Define an interface for the input object that includes 'traceparent' & 'tracestate'.
interface Carrier {
  traceparent?: string;
  tracestate?: string;
}

// Assume "input" is an object with 'traceparent' & 'tracestate' keys.
const input: Carrier = {};

// Extracts the 'traceparent' and 'tracestate' data into a context object.
//
// You can then treat this context as the active context for your
// traces.
let activeContext: Context = propagation.extract(context.active(), input);

let tracer = trace.getTracer('app-name');

let span: Span = tracer.startSpan(
  spanName,
  {
    attributes: {},
  },
  activeContext,
);

// Set the created span as active in the deserialized context.
trace.setSpan(activeContext, span);
// Receiving service
import { context, propagation, trace } from '@opentelemetry/api';

// Assume "input" is an object with 'traceparent' & 'tracestate' keys
const input = {};

// Extracts the 'traceparent' and 'tracestate' data into a context object.
//
// You can then treat this context as the active context for your
// traces.
let activeContext = propagation.extract(context.active(), input);

let tracer = trace.getTracer('app-name');

let span = tracer.startSpan(
  spanName,
  {
    attributes: {},
  },
  activeContext,
);

// Set the created span as active in the deserialized context.
trace.setSpan(activeContext, span);

From there, when you have a deserialized active context, you can create spans that will be a part of the same trace from the other service.

You can also use the Context API to modify or set the deserialized context in other ways.

Custom protocol example

A common use case for when you need to propagate context manually is when you use a custom protocol between services for communication. The following example uses a basic text-based TCP protocol to send a serialized object from one service to another.

Start with creating a new folder called propagation-example and initialize it with dependencies as follows:

npm init -y
npm install @opentelemetry/api @opentelemetry/sdk-node

Next create files client.js and server.js with the following content:

// client.js
const net = require('net');
const { context, propagation, trace } = require('@opentelemetry/api');

let tracer = trace.getTracer('client');

// Connect to the server
const client = net.createConnection({ port: 8124 }, () => {
  // Send the serialized object to the server
  let span = tracer.startActiveSpan('send', { kind: 1 }, (span) => {
    const output = {};
    propagation.inject(context.active(), output);
    const { traceparent, tracestate } = output;

    const objToSend = { key: 'value' };

    if (traceparent) {
      objToSend._meta = { traceparent, tracestate };
    }

    client.write(JSON.stringify(objToSend), () => {
      client.end();
      span.end();
    });
  });
});
// server.js
const net = require('net');
const { context, propagation, trace } = require('@opentelemetry/api');

let tracer = trace.getTracer('server');

const server = net.createServer((socket) => {
  socket.on('data', (data) => {
    const message = data.toString();
    // Parse the JSON object received from the client
    try {
      const json = JSON.parse(message);
      let activeContext = context.active();
      if (json._meta) {
        activeContext = propagation.extract(context.active(), json._meta);
        delete json._meta;
      }
      span = tracer.startSpan('receive', { kind: 1 }, activeContext);
      trace.setSpan(activeContext, span);
      console.log('Parsed JSON:', json);
    } catch (e) {
      console.error('Error parsing JSON:', e.message);
    } finally {
      span.end();
    }
  });
});

// Listen on port 8124
server.listen(8124, () => {
  console.log('Server listening on port 8124');
});

Start a first shell to run the server:

$ node server.js
Server listening on port 8124

Then in a second shell run the client:

node client.js

The client should terminate immediately and the server should output the following:

Parsed JSON: { key: 'value' }

Since the example so far only took dependency on the OpenTelemetry API all calls to it are no-op instructions and the client and server behave as if OpenTelemetry is not used.

To enable OpenTelemetry and see the context propagation in action, create an additional file called instrumentation.js with the following content:

// instrumentation.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const {
  ConsoleSpanExporter,
  SimpleSpanProcessor,
} = require('@opentelemetry/sdk-trace-node');

const sdk = new NodeSDK({
  spanProcessors: [new SimpleSpanProcessor(new ConsoleSpanExporter())],
});

sdk.start();

Use this file to run both, the server and the client, with instrumentation enabled:

$ node -r ./instrumentation.js server.js
Server listening on port 8124

and

node -r ./instrumentation client.js

After the client has sent data to the server and terminated you should see spans in the console output of both shells.

The output for the client looks like the following:

{
  resource: {
    attributes: {
      // ...
    }
  },
  traceId: '4b5367d540726a70afdbaf49240e6597',
  parentId: undefined,
  traceState: undefined,
  name: 'send',
  id: '92f125fa335505ec',
  kind: 1,
  timestamp: 1718879823424000,
  duration: 1054.583,
  // ...
}

The output for the server looks like the following:

{
  resource: {
    attributes: {
      // ...
    }
  },
  traceId: '4b5367d540726a70afdbaf49240e6597',
  parentId: '92f125fa335505ec',
  traceState: undefined,
  name: 'receive',
  id: '53da0c5f03cb36e5',
  kind: 1,
  timestamp: 1718879823426000,
  duration: 959.541,
  // ...
}

Similar to the manual example the spans are connected using the traceId and the id/parentId.

Next steps

To learn more about propagation, read the Propagators API specification.