Skip to content

Commit 258ad7d

Browse files
authored
feat: Added ability to propagate traceparent and tracestate on incoming server/consumer spans and outgoing client http and producer spans (#2958)
1 parent 8943672 commit 258ad7d

26 files changed

+677
-172
lines changed

lib/otel/constants.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,14 @@ module.exports = {
111111
*
112112
* @example 200
113113
*/
114-
ATTR_HTTP_STATUS_CODE: 'http.response.status_code',
114+
ATTR_HTTP_RES_STATUS_CODE: 'http.response.status_code',
115+
116+
/**
117+
* The http response status code
118+
*
119+
* {@link ATTR_HTTP_RES_STATUS_CODE} is new and should be used instead.
120+
*/
121+
ATTR_HTTP_STATUS_CODE: 'http.status_code',
115122

116123
/**
117124
* The http response status text

lib/otel/segments/consumer.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ module.exports = createConsumerSegment
1515
const Transaction = require('../../transaction/')
1616
const recorder = require('../../metrics/recorders/message-transaction')
1717
const { TYPES } = Transaction
18+
const { propagateTraceContext } = require('./utils')
1819

1920
const {
2021
ATTR_MESSAGING_DESTINATION,
@@ -25,7 +26,8 @@ const {
2526

2627
function createConsumerSegment(agent, otelSpan) {
2728
const attrs = otelSpan.attributes
28-
const transaction = new Transaction(agent)
29+
const spanContext = otelSpan.spanContext()
30+
const transaction = new Transaction(agent, spanContext?.traceId)
2931
transaction.type = TYPES.MESSAGE
3032

3133
const system = attrs[ATTR_MESSAGING_SYSTEM] ?? 'unknown'
@@ -35,8 +37,10 @@ function createConsumerSegment(agent, otelSpan) {
3537
const segmentName = `${system}/${destKind}/Named/${destination}`
3638

3739
transaction.setPartialName(segmentName)
40+
propagateTraceContext({ transaction, otelSpan, transport: system })
3841

3942
const segment = agent.tracer.createSegment({
43+
id: spanContext?.spanId,
4044
recorder,
4145
name: transaction.getFullName(),
4246
parent: transaction.trace.root,

lib/otel/segments/database.js

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ module.exports = function createDbSegment(agent, otelSpan) {
2929
const parsed = parseStatement(agent.config, otelSpan, system)
3030
const { name, operation } = setName(parsed)
3131
const segment = agent.tracer.createSegment({
32+
id: otelSpan?.spanContext()?.spanId,
3233
name,
3334
recorder: getRecorder({ operation, parsed, system }),
3435
parent: context.segment,

lib/otel/segments/http-external.js

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ module.exports = function createHttpExternalSegment(agent, otelSpan) {
1717
const host = otelSpan.attributes[ATTR_HTTP_HOST] || 'Unknown'
1818
const name = NAMES.EXTERNAL.PREFIX + host
1919
const segment = agent.tracer.createSegment({
20+
id: otelSpan?.spanContext()?.spanId,
2021
name,
2122
recorder: recordExternal(host, 'http'),
2223
parent: context.segment,

lib/otel/segments/internal.js

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module.exports = function createInternalSegment(agent, otelSpan) {
1010
const context = agent.tracer.getContext()
1111
const name = otelSpan.name
1212
const segment = agent.tracer.createSegment({
13+
id: otelSpan?.spanContext()?.spanId,
1314
name,
1415
parent: context.segment,
1516
recorder: customRecorder,

lib/otel/segments/producer.js

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module.exports = function createProducerSegment(agent, otelSpan) {
1818
const name = setName(otelSpan)
1919

2020
const segment = agent.tracer.createSegment({
21+
id: otelSpan?.spanContext()?.spanId,
2122
name,
2223
recorder: genericRecorder,
2324
parent: context.segment,

lib/otel/segments/server.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const httpRecorder = require('../../metrics/recorders/http')
1010
const urltils = require('../../util/urltils')
1111
const url = require('node:url')
1212
const { NODEJS, ACTION_DELIMITER } = require('../../metrics/names')
13+
const { propagateTraceContext } = require('./utils')
1314

1415
const DESTINATION = Transaction.DESTINATIONS.TRANS_COMMON
1516
const {
@@ -22,10 +23,12 @@ const {
2223
} = require('../constants')
2324

2425
module.exports = function createServerSegment(agent, otelSpan) {
25-
const transaction = new Transaction(agent)
26+
const spanContext = otelSpan.spanContext()
27+
const transaction = new Transaction(agent, spanContext?.traceId)
2628
transaction.type = 'web'
2729
transaction.nameState.setPrefix(NODEJS.PREFIX)
2830
transaction.nameState.setPrefix(ACTION_DELIMITER)
31+
propagateTraceContext({ transaction, otelSpan, transport: 'HTTPS' })
2932
const rpcSystem = otelSpan.attributes[ATTR_RPC_SYSTEM]
3033
const httpMethod = otelSpan.attributes[ATTR_HTTP_METHOD] ?? otelSpan.attributes[ATTR_HTTP_REQUEST_METHOD]
3134
let segment
@@ -48,6 +51,7 @@ function rpcSegment({ agent, otelSpan, transaction, rpcSystem }) {
4851
transaction.nameState.setPrefix(rpcSystem)
4952
transaction.nameState.appendPath(transaction.url)
5053
const segment = agent.tracer.createSegment({
54+
id: otelSpan?.spanContext()?.spanId,
5155
name,
5256
recorder: httpRecorder,
5357
parent: transaction.trace.root,
@@ -71,6 +75,7 @@ function httpSegment({ agent, otelSpan, transaction, httpMethod }) {
7175
// accept dt headers?
7276
// synthetics.assignHeadersToTransaction(agent.config, transaction, )
7377
return agent.tracer.createSegment({
78+
id: otelSpan?.spanContext()?.spanId,
7479
recorder: httpRecorder,
7580
name: requestUrl.pathname,
7681
parent: transaction.trace.root,

lib/otel/segments/utils.js

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
function propagateTraceContext({ transaction, otelSpan, transport }) {
9+
const spanContext = otelSpan.spanContext()
10+
11+
if (otelSpan.parentSpanId) {
12+
// 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}`
14+
transaction.acceptTraceContextPayload(traceparent, spanContext?.traceState?.state, transport)
15+
}
16+
}
17+
18+
module.exports = {
19+
propagateTraceContext
20+
}

lib/otel/setup.js

+5-8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
'use strict'
7+
const opentelemetry = require('@opentelemetry/api')
78
const { BasicTracerProvider } = require('@opentelemetry/sdk-trace-base')
89
const { Resource } = require('@opentelemetry/resources')
910
const NrSpanProcessor = require('./span-processor')
@@ -24,24 +25,20 @@ module.exports = function setupOtel(agent, logger = defaultLogger) {
2425

2526
createOtelLogger(logger, agent.config)
2627

27-
const provider = new BasicTracerProvider({
28+
opentelemetry.trace.setGlobalTracerProvider(new BasicTracerProvider({
2829
spanProcessors: [new NrSpanProcessor(agent)],
2930
resource: new Resource({
3031
[ATTR_SERVICE_NAME]: agent.config.applications()[0]
3132
}),
3233
generalLimits: {
3334
attributeValueLengthLimit: 4095
3435
}
36+
}))
3537

36-
})
37-
provider.register({
38-
contextManager: new ContextManager(agent),
39-
propagator: new TracePropagator(agent)
40-
})
38+
opentelemetry.context.setGlobalContextManager(new ContextManager(agent))
39+
opentelemetry.propagation.setGlobalPropagator(new TracePropagator(agent))
4140

4241
agent.metrics
4342
.getOrCreateMetric('Supportability/Nodejs/OpenTelemetryBridge/Setup')
4443
.incrementCallCount()
45-
46-
return provider
4744
}

lib/otel/span-processor.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const {
1515
ATTR_DB_SYSTEM,
1616
ATTR_GRPC_STATUS_CODE,
1717
ATTR_HTTP_ROUTE,
18+
ATTR_HTTP_RES_STATUS_CODE,
1819
ATTR_HTTP_STATUS_CODE,
1920
ATTR_HTTP_STATUS_TEXT,
2021
ATTR_MESSAGING_DESTINATION,
@@ -149,6 +150,9 @@ module.exports = class NrSpanProcessor {
149150
// End the corresponding transaction for the entry point server span.
150151
// We do then when the span ends to ensure all data has been processed
151152
// for the corresponding server span.
153+
if (transaction.statusCode) {
154+
transaction.finalizeNameFromUri(transaction.parsedUrl, transaction.statusCode)
155+
}
152156
transaction.end()
153157
}
154158

@@ -159,14 +163,14 @@ module.exports = class NrSpanProcessor {
159163
if (key === ATTR_HTTP_ROUTE) {
160164
// TODO: can we get the route params?
161165
transaction.nameState.appendPath(sanitized)
162-
} else if (key === ATTR_HTTP_STATUS_CODE) {
163-
transaction.finalizeNameFromUri(transaction.parsedUrl, sanitized)
164-
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, 'http.statusCode', sanitized)
166+
} else if (key === ATTR_HTTP_STATUS_CODE || key === ATTR_HTTP_RES_STATUS_CODE) {
165167
key = 'http.statusCode'
168+
transaction.statusCode = sanitized
169+
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, key, sanitized)
166170
// Not using const as it is not in semantic-conventions
167171
} else if (key === ATTR_HTTP_STATUS_TEXT) {
168-
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, 'http.statusText', sanitized)
169172
key = 'http.statusText'
173+
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, key, sanitized)
170174
} else if (key === ATTR_SERVER_PORT || key === ATTR_NET_HOST_PORT) {
171175
key = 'port'
172176
} else if (key === ATTR_SERVER_ADDRESS || key === ATTR_NET_HOST_NAME) {

lib/otel/trace-propagator.js

+12-15
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44
*/
55

66
'use strict'
7-
const { trace, isSpanContextValid, TraceFlags } = require('@opentelemetry/api')
7+
const { trace, isSpanContextValid } = require('@opentelemetry/api')
88
const { isTracingSuppressed } = require('@opentelemetry/core')
99
const TRACE_PARENT_HEADER = 'traceparent'
1010
const TRACE_STATE_HEADER = 'tracestate'
1111

12-
const VERSION = '00'
1312
const VERSION_PART = '(?!ff)[\\da-f]{2}'
1413
const TRACE_ID_PART = '(?![0]{32})[\\da-f]{32}'
1514
const PARENT_ID_PART = '(?![0]{16})[\\da-f]{16}'
@@ -18,7 +17,6 @@ const TRACE_PARENT_REGEX = new RegExp(
1817
`^\\s?(${VERSION_PART})-(${TRACE_ID_PART})-(${PARENT_ID_PART})-(${FLAGS_PART})(-.*)?\\s?$`
1918
)
2019

21-
// TODO: handle trace state
2220
class TraceState {
2321
constructor(state) {
2422
this.state = state
@@ -58,6 +56,10 @@ module.exports = class NewRelicTracePropagator {
5856
}
5957

6058
inject(context, carrier, setter) {
59+
if (this.agent.config.distributed_tracing.enabled !== true) {
60+
return
61+
}
62+
6163
if (context.constructor.name === 'BaseContext') {
6264
context = this.agent.tracer._contextManager.getContext()
6365
}
@@ -68,30 +70,25 @@ module.exports = class NewRelicTracePropagator {
6870
!isSpanContextValid(spanContext)
6971
) { return }
7072

71-
const traceParent = `${VERSION}-${spanContext.traceId}-${
72-
spanContext.spanId
73-
}-0${Number(spanContext.traceFlags || TraceFlags.NONE).toString(16)}`
74-
75-
setter.set(carrier, TRACE_PARENT_HEADER, traceParent)
76-
if (spanContext.traceState) {
77-
setter.set(
78-
carrier,
79-
TRACE_STATE_HEADER,
80-
spanContext.traceState.serialize()
81-
)
82-
}
73+
context?.transaction?.insertDistributedTraceHeaders(carrier, spanContext)
8374
}
8475

8576
extract(context, carrier, getter) {
8677
if (context.constructor.name === 'BaseContext') {
8778
context = this.agent.tracer._contextManager.getContext()
8879
}
80+
81+
if (this.agent.config.distributed_tracing.enabled !== true) {
82+
return context
83+
}
84+
8985
const traceParentHeader = getter.get(carrier, TRACE_PARENT_HEADER)
9086
if (!traceParentHeader) return context
9187
const traceParent = Array.isArray(traceParentHeader)
9288
? traceParentHeader[0]
9389
: traceParentHeader
9490
if (typeof traceParent !== 'string') return context
91+
// TODO: we parse it as well but the keys we return are different, should we layer this on?
9592
const spanContext = parseTraceParent(traceParent)
9693
if (!spanContext) return context
9794

lib/shim/message-shim/subscribe-consume.js

-1
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,6 @@ function createConsumerWrapper({ shim, spec, consumer }) {
238238
* finalizes transaction name and ends transaction
239239
*/
240240
function endTransaction() {
241-
tx.finalizeName(null) // Use existing partial name.
242241
tx.end()
243242
}
244243
}

lib/transaction/index.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@ const MULTIPLE_INSERT_MESSAGE =
7070
* transaction.
7171
*
7272
* @param {object} agent The agent.
73-
*
73+
* @param {string} traceId if present, it will use this to assign traceId of transaction. only used in otel bridge mode to ensure trace id is same as otel spans
7474
* @fires Agent#transactionStarted
7575
*/
76-
function Transaction(agent) {
76+
function Transaction(agent, traceId) {
7777
if (!agent) {
7878
throw new Error('every transaction must be bound to the agent')
7979
}
@@ -131,7 +131,7 @@ function Transaction(agent) {
131131
this.parentAcct = null
132132
this.parentTransportType = null
133133
this.parentTransportDuration = null
134-
this._traceId = null
134+
this._traceId = traceId || null
135135
Object.defineProperty(this, 'traceId', {
136136
get() {
137137
if (this._traceId === null) {
@@ -949,9 +949,10 @@ function acceptDistributedTraceHeaders(transportType, headers) {
949949
* Inserts distributed trace headers into the provided headers map.
950950
*
951951
* @param {object} headers
952+
* @param {object} spanContext otel span context
952953
*/
953954
Transaction.prototype.insertDistributedTraceHeaders = insertDistributedTraceHeaders
954-
function insertDistributedTraceHeaders(headers) {
955+
function insertDistributedTraceHeaders(headers, spanContext) {
955956
if (!headers) {
956957
logger.trace('insertDistributedTraceHeaders called without headers.')
957958
return
@@ -962,7 +963,7 @@ function insertDistributedTraceHeaders(headers) {
962963
// Ensure we have priority before generating trace headers.
963964
this._calculatePriority()
964965

965-
this.traceContext.addTraceContextHeaders(headers)
966+
this.traceContext.addTraceContextHeaders(headers, spanContext)
966967
this.isDistributedTrace = true
967968

968969
logger.trace('Added outbound request w3c trace context headers in transaction %s', this.id)

lib/transaction/trace/segment.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const ATTRIBUTE_SCOPE = 'segment'
3030
* for now), and has one or more children (that are also part of the same
3131
* transaction trace), as well as an associated timer.
3232
* @param {object} params to function
33+
* @param {number} params.id id if passed in used as segment id. only used in otel bridge mode to ensure span id is same as segment
3334
* @param {object} params.config agent config
3435
* @param {string} params.name Human-readable name for this segment (e.g. 'http', 'net', 'express',
3536
* 'mysql', etc).
@@ -38,15 +39,15 @@ const ATTRIBUTE_SCOPE = 'segment'
3839
* @param {TraceSegment} params.root root segment
3940
* @param {boolean} params.isRoot flag to indicate it is the root segment
4041
*/
41-
function TraceSegment({ config, name, collect, parentId, root, isRoot = false }) {
42+
function TraceSegment({ id, config, name, collect, parentId, root, isRoot = false }) {
4243
this.isRoot = isRoot
4344
this.root = root
4445
this.name = name
4546
this.attributes = new Attributes(ATTRIBUTE_SCOPE)
4647
this.spansEnabled = config?.distributed_tracing?.enabled && config?.span_events?.enabled
4748

4849
// Generate a unique id for use in span events.
49-
this.id = hashes.makeId()
50+
this.id = id || hashes.makeId()
5051
this.parentId = parentId
5152
this.timer = new Timer()
5253

0 commit comments

Comments
 (0)