Skip to content

Commit dfeec5a

Browse files
authored
feat: Updated Context class to ensure bi-directional context propagation with opentelemetry bridge (#2962)
1 parent 258ad7d commit dfeec5a

File tree

10 files changed

+224
-7
lines changed

10 files changed

+224
-7
lines changed

lib/context-manager/context.js

+32-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
'use strict'
77
const { otelSynthesis } = require('../symbols')
8+
const FakeSpan = require('../otel/fake-span')
89

910
module.exports = class Context {
1011
constructor(transaction, segment, parentContext) {
@@ -21,12 +22,41 @@ module.exports = class Context {
2122
return this._transaction
2223
}
2324

25+
/**
26+
* Constructs a new context from segment about to be bound to context manager
27+
* along with the current transaction.
28+
*
29+
* If agent is in otel bridge mode it will also bind a FakeSpan to the otel ctx.
30+
*
31+
* @param {object} params to function
32+
* @param {TraceSegment} params.segment segment to bind to context
33+
* @param {Transaction} params.transaction active transaction
34+
* @returns {Context} a newly constructed context
35+
*/
2436
enterSegment({ segment, transaction = this._transaction }) {
25-
return new this.constructor(transaction, segment)
37+
if (transaction?.agent?.otelSpanKey) {
38+
this._otelCtx.set(transaction.agent.otelSpanKey, new FakeSpan(segment, transaction))
39+
}
40+
return new this.constructor(transaction, segment, this._otelCtx)
2641
}
2742

43+
/**
44+
* Constructs a new context from transaction about to be bound to context manager.
45+
* It uses the trace root segment as the segment in context.
46+
*
47+
* If agent is in otel bridge mode it will also bind a FakeSpan to the otel ctx.
48+
*
49+
* @param {object} params to function
50+
* @param {TraceSegment} params.segment transaction trace root segment
51+
* @param {Transaction} params.transaction transaction to bind to context
52+
* @param transaction
53+
* @returns {Context} a newly constructed context
54+
*/
2855
enterTransaction(transaction) {
29-
return new this.constructor(transaction, transaction.trace.root)
56+
if (transaction?.agent?.otelSpanKey) {
57+
this._otelCtx.set(transaction.agent.otelSpanKey, new FakeSpan(transaction.trace.root, transaction))
58+
}
59+
return new this.constructor(transaction, transaction.trace.root, this._otelCtx)
3060
}
3161

3262
/**

lib/otel/fake-span.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
/**
9+
* In order to be able to return the appropriate span context
10+
* wihtin otel bridge. We have to create fake spans for new relic
11+
* segments. The only thing needed is a method for `spanContext`
12+
* which should return the spanId(segment id) and traceId(transaction trace id).
13+
* We hardcode traceFlags to 1.
14+
*/
15+
module.exports = class FakeSpan {
16+
constructor(segment, transaction) {
17+
this.segment = segment
18+
this.transaction = transaction
19+
}
20+
21+
spanContext() {
22+
return {
23+
spanId: this.segment.id,
24+
traceId: this.transaction.traceId,
25+
traceFlags: 1
26+
}
27+
}
28+
}

lib/otel/segments/utils.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,22 @@
55

66
'use strict'
77

8+
/**
9+
* Accepts trace context payload if span has a parent. It will use the
10+
* span context to extract the traceId, traceFlags and trace state.
11+
*
12+
* @param {object} params to function
13+
* @param {Transaction} params.transaction active transaction
14+
* @param {object} params.otelSpan active span
15+
* @param {string} params.transport indicator of type of span(http, kafkajs, rabbitmq, etc)
16+
*/
817
function propagateTraceContext({ transaction, otelSpan, transport }) {
918
const spanContext = otelSpan.spanContext()
19+
const parentSpanId = otelSpan?.parentSpanId || otelSpan?.parentSpanContext?.spanId
1020

11-
if (otelSpan.parentSpanId) {
21+
if (parentSpanId) {
1222
// prefix traceFlags with 0 as it is stored as a parsed int on spanContext
13-
const traceparent = `00-${spanContext.traceId}-${otelSpan.parentSpanId}-0${spanContext.traceFlags}`
23+
const traceparent = `00-${spanContext.traceId}-${parentSpanId}-0${spanContext.traceFlags}`
1424
transaction.acceptTraceContextPayload(traceparent, spanContext?.traceState?.state, transport)
1525
}
1626
}

lib/otel/setup.js

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const createOtelLogger = require('./logger')
1414
const TracePropagator = require('./trace-propagator')
1515

1616
const { ATTR_SERVICE_NAME } = require('./constants')
17+
const interceptSpanKey = require('./span-key-interceptor')
1718

1819
module.exports = function setupOtel(agent, logger = defaultLogger) {
1920
if (agent.config.feature_flag.opentelemetry_bridge !== true) {
@@ -35,6 +36,7 @@ module.exports = function setupOtel(agent, logger = defaultLogger) {
3536
}
3637
}))
3738

39+
interceptSpanKey(agent)
3840
opentelemetry.context.setGlobalContextManager(new ContextManager(agent))
3941
opentelemetry.propagation.setGlobalPropagator(new TracePropagator(agent))
4042

lib/otel/span-key-interceptor.js

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
const otelApi = require('@opentelemetry/api')
9+
10+
/**
11+
* Called before setting up the otel bridge ContextManager.
12+
* This creates a fake context and uses the otel api to set a span.
13+
* By creating a fake `setValue` when it will give us the symbol used for enqueueing spans to the context.
14+
* This is assigned as a key on the agent.
15+
* We will use this key to enqueue our FakeSpan when we enter a segment or transaction so that when using otel API it will return the appropriate traceId and spanId.
16+
*
17+
* @param {Agent} agent instance
18+
*/
19+
module.exports = function interceptSpanKey(agent) {
20+
const fakeCtx = {
21+
spanKey: null,
22+
setValue(key) {
23+
this.spanKey = key
24+
}
25+
}
26+
27+
const fakeSpan = {}
28+
otelApi.trace.setSpan(fakeCtx, fakeSpan)
29+
agent.otelSpanKey = fakeCtx.spanKey
30+
}

lib/otel/trace-propagator.js

-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ class TraceState {
3232
* (01 = sampled, 00 = not sampled).
3333
* for example: '{version}-{traceId}-{spanId}-{sampleDecision}'
3434
* For more information see {@link https://www.w3.org/TR/trace-context/}
35-
* @param traceParen
3635
*/
3736
function parseTraceParent(traceParent) {
3837
const match = TRACE_PARENT_REGEX.exec(traceParent)

test/unit/lib/otel/fake-span.test.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
const test = require('node:test')
9+
const assert = require('node:assert')
10+
const helper = require('#testlib/agent_helper.js')
11+
const FakeSpan = require('#agentlib/otel/fake-span.js')
12+
13+
test('should create a fake span from segment and transaction', (t) => {
14+
const agent = helper.loadMockedAgent()
15+
t.after(() => {
16+
helper.unloadAgent(agent)
17+
})
18+
19+
const segment = { id: 'id' }
20+
const tx = { traceId: 'traceId' }
21+
const span = new FakeSpan(segment, tx)
22+
const spanCtx = span.spanContext()
23+
assert.deepEqual(spanCtx, {
24+
spanId: 'id',
25+
traceId: 'traceId',
26+
traceFlags: 1
27+
})
28+
})
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
const test = require('node:test')
9+
const assert = require('node:assert')
10+
const sinon = require('sinon')
11+
const helper = require('#testlib/agent_helper.js')
12+
const { propagateTraceContext } = require('#agentlib/otel/segments/utils.js')
13+
14+
test.beforeEach((ctx) => {
15+
const agent = helper.loadMockedAgent()
16+
const transaction = {
17+
acceptTraceContextPayload: sinon.stub()
18+
}
19+
ctx.nr = {
20+
agent,
21+
transaction
22+
}
23+
})
24+
25+
test.afterEach((ctx) => {
26+
helper.unloadAgent(ctx.nr.agent)
27+
})
28+
29+
test('should accept traceparent when span has parentSpanId', (t) => {
30+
const { transaction } = t.nr
31+
const otelSpan = {
32+
parentSpanId: 'parentId',
33+
spanContext() {
34+
return {
35+
traceId: 'traceId',
36+
traceFlags: 1,
37+
traceState: { state: 'state' }
38+
}
39+
}
40+
}
41+
propagateTraceContext({ transaction, otelSpan, transport: 'transport' })
42+
assert.equal(transaction.acceptTraceContextPayload.callCount, 1)
43+
assert.deepEqual(transaction.acceptTraceContextPayload.args[0], [
44+
'00-traceId-parentId-01', 'state', 'transport'
45+
])
46+
})
47+
48+
test('should accept traceparent when span has parentSpanContext.spanId', (t) => {
49+
const { transaction } = t.nr
50+
const otelSpan = {
51+
parentSpanContext: { spanId: 'parentId' },
52+
spanContext() {
53+
return {
54+
traceId: 'traceId',
55+
traceFlags: 1,
56+
traceState: { state: 'state' }
57+
}
58+
}
59+
}
60+
propagateTraceContext({ transaction, otelSpan, transport: 'transport' })
61+
assert.equal(transaction.acceptTraceContextPayload.callCount, 1)
62+
assert.deepEqual(transaction.acceptTraceContextPayload.args[0], [
63+
'00-traceId-parentId-01', 'state', 'transport'
64+
])
65+
})
66+
67+
test('should not accept traceparent when span has not parent span id', (t) => {
68+
const { transaction } = t.nr
69+
const otelSpan = { spanContext() { return {} } }
70+
propagateTraceContext({ transaction, otelSpan, transport: 'transport' })
71+
assert.equal(transaction.acceptTraceContextPayload.callCount, 0)
72+
})

test/unit/lib/otel/setup.test.js

+7
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,10 @@ test('should not create provider when `feature_flag.opentelemetry_bridge` is fal
4949
assert.equal(provider, null)
5050
assert.equal(loggerMock.warn.args[0][0], '`feature_flag.opentelemetry_bridge` is not enabled, skipping setup of opentelemetry-bridge')
5151
})
52+
53+
test('should assign span key to agent', (t) => {
54+
const { agent, loggerMock } = t.nr
55+
agent.config.feature_flag.opentelemetry_bridge = true
56+
otelSetup(agent, loggerMock)
57+
assert.ok(agent.otelSpanKey)
58+
})

test/versioned/otel-bridge/span.test.js

+13-2
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,10 @@ test.afterEach((ctx) => {
6767

6868
test('mix internal and NR span tests', (t, end) => {
6969
const { agent, api, tracer } = t.nr
70-
function main(mainSegment) {
70+
function main(mainSegment, tx) {
7171
tracer.startActiveSpan('hi', (span) => {
7272
const segment = agent.tracer.getSegment()
73+
assert.equal(tx.traceId, span.spanContext().traceId)
7374
assert.equal(segment.name, span.name)
7475
assert.equal(segment.parentId, mainSegment.id)
7576
span.end()
@@ -81,6 +82,7 @@ test('mix internal and NR span tests', (t, end) => {
8182
const parentSegment = agent.tracer.getSegment()
8283
tracer.startActiveSpan('bye', (span) => {
8384
const segment = agent.tracer.getSegment()
85+
assert.equal(tx.traceId, span.spanContext().traceId)
8486
assert.equal(segment.name, span.name)
8587
assert.equal(segment.parentId, parentSegment.id)
8688
span.end()
@@ -93,7 +95,8 @@ test('mix internal and NR span tests', (t, end) => {
9395
tx.name = 'otel-example-tx'
9496
tracer.startActiveSpan('main', (span) => {
9597
const segment = agent.tracer.getSegment()
96-
main(segment)
98+
assert.equal(tx.traceId, span.spanContext().traceId)
99+
main(segment, tx)
97100
span.end()
98101
assert.equal(span[otelSynthesis], undefined)
99102
assert.equal(segment.name, span.name)
@@ -121,6 +124,7 @@ test('client span(http) is bridge accordingly', (t, end) => {
121124
tracer.startActiveSpan('http-outbound', { kind: otel.SpanKind.CLIENT, attributes: { [ATTR_HTTP_HOST]: 'newrelic.com', [ATTR_HTTP_METHOD]: 'GET' } }, (span) => {
122125
const segment = agent.tracer.getSegment()
123126
assert.equal(segment.name, 'External/newrelic.com')
127+
assert.equal(tx.traceId, span.spanContext().traceId)
124128
span.end()
125129
const duration = hrTimeToMilliseconds(span.duration)
126130
assert.equal(duration, segment.getDurationInMillis())
@@ -152,6 +156,7 @@ test('client span(db) is bridge accordingly(statement test)', (t, end) => {
152156
tracer.startActiveSpan('db-test', { kind: otel.SpanKind.CLIENT, attributes }, (span) => {
153157
const segment = agent.tracer.getSegment()
154158
assert.equal(segment.name, 'Datastore/statement/postgresql/test/select')
159+
assert.equal(tx.traceId, span.spanContext().traceId)
155160
span.end()
156161
const duration = hrTimeToMilliseconds(span.duration)
157162
assert.equal(duration, segment.getDurationInMillis())
@@ -196,6 +201,7 @@ test('client span(db) is bridged accordingly(operation test)', (t, end) => {
196201
tracer.startActiveSpan('db-test', { kind: otel.SpanKind.CLIENT, attributes }, (span) => {
197202
const segment = agent.tracer.getSegment()
198203
assert.equal(segment.name, 'Datastore/operation/redis/hset')
204+
assert.equal(tx.traceId, span.spanContext().traceId)
199205
span.end()
200206
const duration = hrTimeToMilliseconds(span.duration)
201207
assert.equal(duration, segment.getDurationInMillis())
@@ -240,6 +246,7 @@ test('server span is bridged accordingly', (t, end) => {
240246

241247
tracer.startActiveSpan('http-test', { kind: otel.SpanKind.SERVER, attributes }, (span) => {
242248
const tx = agent.getTransaction()
249+
assert.equal(tx.traceId, span.spanContext().traceId)
243250
span.setAttribute(ATTR_HTTP_STATUS_CODE, 200)
244251
span.setAttribute(ATTR_HTTP_STATUS_TEXT, 'OK')
245252
span.end()
@@ -294,6 +301,7 @@ test('server span(rpc) is bridged accordingly', (t, end) => {
294301
tracer.startActiveSpan('http-test', { kind: otel.SpanKind.SERVER, attributes }, (span) => {
295302
span.setAttribute(ATTR_GRPC_STATUS_CODE, 0)
296303
const tx = agent.getTransaction()
304+
assert.equal(tx.traceId, span.spanContext().traceId)
297305
span.end()
298306
assert.ok(!tx.isDistributedTrace)
299307
const segment = agent.tracer.getSegment()
@@ -344,6 +352,7 @@ test('server span(fallback) is bridged accordingly', (t, end) => {
344352
const expectedHost = agent.config.getHostnameSafe('127.0.0.1')
345353
tracer.startActiveSpan('http-test', { kind: otel.SpanKind.SERVER, attributes }, (span) => {
346354
const tx = agent.getTransaction()
355+
assert.equal(tx.traceId, span.spanContext().traceId)
347356
span.end()
348357
assert.ok(!tx.isDistributedTrace)
349358
const segment = agent.tracer.getSegment()
@@ -394,6 +403,7 @@ test('producer span is bridged accordingly', (t, end) => {
394403
const expectedHost = agent.config.getHostnameSafe('localhost')
395404
tracer.startActiveSpan('prod-test', { kind: otel.SpanKind.PRODUCER, attributes }, (span) => {
396405
const segment = agent.tracer.getSegment()
406+
assert.equal(tx.traceId, span.spanContext().traceId)
397407
assert.equal(segment.name, 'MessageBroker/messaging-lib/queue/Produce/Named/test-queue')
398408
span.end()
399409
const duration = hrTimeToMilliseconds(span.duration)
@@ -434,6 +444,7 @@ test('consumer span is bridged correctly', (t, end) => {
434444
const tx = agent.getTransaction()
435445
assert.ok(!tx.isDistributedTrace)
436446
const segment = agent.tracer.getSegment()
447+
assert.equal(tx.traceId, span.spanContext().traceId)
437448
span.end()
438449
const duration = hrTimeToMilliseconds(span.duration)
439450
assert.equal(duration, segment.getDurationInMillis())

0 commit comments

Comments
 (0)