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.
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.
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.
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 {
type Context,
propagation,
trace,
Span,
context,
} 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.
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
.
To learn more about propagation, read the Propagators API specification.
[i18n] feedback_question
Thank you. Your feedback is appreciated!
Please let us know how we can improve this page. Your feedback is appreciated!