Skip to content

Commit

Permalink
feat: Added ability to propagate traceparent and tracestate on incomi…
Browse files Browse the repository at this point in the history
…ng server/consumer spans and outgoing client http and producer spans.
  • Loading branch information
bizob2828 committed Feb 24, 2025
1 parent 8943672 commit 587e1cc
Show file tree
Hide file tree
Showing 25 changed files with 660 additions and 106 deletions.
3 changes: 2 additions & 1 deletion lib/otel/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ module.exports = {
*
* @example 200
*/
ATTR_HTTP_STATUS_CODE: 'http.response.status_code',
ATTR_HTTP_RES_STATUS_CODE: 'http.response.status_code',
ATTR_HTTP_STATUS_CODE: 'http.status_code',

/**
* The http response status text
Expand Down
6 changes: 5 additions & 1 deletion lib/otel/segments/consumer.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module.exports = createConsumerSegment
const Transaction = require('../../transaction/')
const recorder = require('../../metrics/recorders/message-transaction')
const { TYPES } = Transaction
const { propagateTraceContext } = require('./utils')

const {
ATTR_MESSAGING_DESTINATION,
Expand All @@ -25,7 +26,8 @@ const {

function createConsumerSegment(agent, otelSpan) {
const attrs = otelSpan.attributes
const transaction = new Transaction(agent)
const spanContext = otelSpan.spanContext()
const transaction = new Transaction(agent, spanContext?.traceId)
transaction.type = TYPES.MESSAGE

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

transaction.setPartialName(segmentName)
propagateTraceContext({ transaction, otelSpan, transport: system })

const segment = agent.tracer.createSegment({
id: spanContext?.spanId,
recorder,
name: transaction.getFullName(),
parent: transaction.trace.root,
Expand Down
1 change: 1 addition & 0 deletions lib/otel/segments/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ module.exports = function createDbSegment(agent, otelSpan) {
const parsed = parseStatement(agent.config, otelSpan, system)
const { name, operation } = setName(parsed)
const segment = agent.tracer.createSegment({
id: otelSpan?.spanContext()?.spanId,
name,
recorder: getRecorder({ operation, parsed, system }),
parent: context.segment,
Expand Down
1 change: 1 addition & 0 deletions lib/otel/segments/http-external.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module.exports = function createHttpExternalSegment(agent, otelSpan) {
const host = otelSpan.attributes[ATTR_HTTP_HOST] || 'Unknown'
const name = NAMES.EXTERNAL.PREFIX + host
const segment = agent.tracer.createSegment({
id: otelSpan?.spanContext()?.spanId,
name,
recorder: recordExternal(host, 'http'),
parent: context.segment,
Expand Down
1 change: 1 addition & 0 deletions lib/otel/segments/internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module.exports = function createInternalSegment(agent, otelSpan) {
const context = agent.tracer.getContext()
const name = otelSpan.name
const segment = agent.tracer.createSegment({
id: otelSpan?.spanContext()?.spanId,
name,
parent: context.segment,
recorder: customRecorder,
Expand Down
1 change: 1 addition & 0 deletions lib/otel/segments/producer.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module.exports = function createProducerSegment(agent, otelSpan) {
const name = setName(otelSpan)

const segment = agent.tracer.createSegment({
id: otelSpan?.spanContext()?.spanId,
name,
recorder: genericRecorder,
parent: context.segment,
Expand Down
7 changes: 6 additions & 1 deletion lib/otel/segments/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const httpRecorder = require('../../metrics/recorders/http')
const urltils = require('../../util/urltils')
const url = require('node:url')
const { NODEJS, ACTION_DELIMITER } = require('../../metrics/names')
const { propagateTraceContext } = require('./utils')

const DESTINATION = Transaction.DESTINATIONS.TRANS_COMMON
const {
Expand All @@ -22,10 +23,12 @@ const {
} = require('../constants')

module.exports = function createServerSegment(agent, otelSpan) {
const transaction = new Transaction(agent)
const spanContext = otelSpan.spanContext()
const transaction = new Transaction(agent, spanContext?.traceId)
transaction.type = 'web'
transaction.nameState.setPrefix(NODEJS.PREFIX)
transaction.nameState.setPrefix(ACTION_DELIMITER)
propagateTraceContext({ transaction, otelSpan, transport: 'HTTPS' })
const rpcSystem = otelSpan.attributes[ATTR_RPC_SYSTEM]
const httpMethod = otelSpan.attributes[ATTR_HTTP_METHOD] ?? otelSpan.attributes[ATTR_HTTP_REQUEST_METHOD]
let segment
Expand All @@ -48,6 +51,7 @@ function rpcSegment({ agent, otelSpan, transaction, rpcSystem }) {
transaction.nameState.setPrefix(rpcSystem)
transaction.nameState.appendPath(transaction.url)
const segment = agent.tracer.createSegment({
id: otelSpan?.spanContext()?.spanId,
name,
recorder: httpRecorder,
parent: transaction.trace.root,
Expand All @@ -71,6 +75,7 @@ function httpSegment({ agent, otelSpan, transaction, httpMethod }) {
// accept dt headers?
// synthetics.assignHeadersToTransaction(agent.config, transaction, )
return agent.tracer.createSegment({
id: otelSpan?.spanContext()?.spanId,
recorder: httpRecorder,
name: requestUrl.pathname,
parent: transaction.trace.root,
Expand Down
20 changes: 20 additions & 0 deletions lib/otel/segments/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2025 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'

function propagateTraceContext({ transaction, otelSpan, transport }) {
const spanContext = otelSpan.spanContext()

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

module.exports = {
propagateTraceContext
}
13 changes: 5 additions & 8 deletions lib/otel/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

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

createOtelLogger(logger, agent.config)

const provider = new BasicTracerProvider({
opentelemetry.trace.setGlobalTracerProvider(new BasicTracerProvider({
spanProcessors: [new NrSpanProcessor(agent)],
resource: new Resource({
[ATTR_SERVICE_NAME]: agent.config.applications()[0]
}),
generalLimits: {
attributeValueLengthLimit: 4095
}
}))

})
provider.register({
contextManager: new ContextManager(agent),
propagator: new TracePropagator(agent)
})
opentelemetry.context.setGlobalContextManager(new ContextManager(agent))
opentelemetry.propagation.setGlobalPropagator(new TracePropagator(agent))

agent.metrics
.getOrCreateMetric('Supportability/Nodejs/OpenTelemetryBridge/Setup')
.incrementCallCount()

return provider
}
12 changes: 8 additions & 4 deletions lib/otel/span-processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const {
ATTR_DB_SYSTEM,
ATTR_GRPC_STATUS_CODE,
ATTR_HTTP_ROUTE,
ATTR_HTTP_RES_STATUS_CODE,
ATTR_HTTP_STATUS_CODE,
ATTR_HTTP_STATUS_TEXT,
ATTR_MESSAGING_DESTINATION,
Expand Down Expand Up @@ -149,6 +150,9 @@ module.exports = class NrSpanProcessor {
// End the corresponding transaction for the entry point server span.
// We do then when the span ends to ensure all data has been processed
// for the corresponding server span.
if (transaction.statusCode) {
transaction.finalizeNameFromUri(transaction.parsedUrl, transaction.statusCode)
}
transaction.end()
}

Expand All @@ -159,14 +163,14 @@ module.exports = class NrSpanProcessor {
if (key === ATTR_HTTP_ROUTE) {
// TODO: can we get the route params?
transaction.nameState.appendPath(sanitized)
} else if (key === ATTR_HTTP_STATUS_CODE) {
transaction.finalizeNameFromUri(transaction.parsedUrl, sanitized)
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, 'http.statusCode', sanitized)
} else if (key === ATTR_HTTP_STATUS_CODE || key === ATTR_HTTP_RES_STATUS_CODE) {
key = 'http.statusCode'
transaction.statusCode = sanitized
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, key, sanitized)
// Not using const as it is not in semantic-conventions
} else if (key === ATTR_HTTP_STATUS_TEXT) {
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, 'http.statusText', sanitized)
key = 'http.statusText'
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, key, sanitized)
} else if (key === ATTR_SERVER_PORT || key === ATTR_NET_HOST_PORT) {
key = 'port'
} else if (key === ATTR_SERVER_ADDRESS || key === ATTR_NET_HOST_NAME) {
Expand Down
27 changes: 12 additions & 15 deletions lib/otel/trace-propagator.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
*/

'use strict'
const { trace, isSpanContextValid, TraceFlags } = require('@opentelemetry/api')
const { trace, isSpanContextValid } = require('@opentelemetry/api')
const { isTracingSuppressed } = require('@opentelemetry/core')
const TRACE_PARENT_HEADER = 'traceparent'
const TRACE_STATE_HEADER = 'tracestate'

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

// TODO: handle trace state
class TraceState {
constructor(state) {
this.state = state
Expand Down Expand Up @@ -58,6 +56,10 @@ module.exports = class NewRelicTracePropagator {
}

inject(context, carrier, setter) {
if (this.agent.config.distributed_tracing.enabled !== true) {
return
}

if (context.constructor.name === 'BaseContext') {
context = this.agent.tracer._contextManager.getContext()
}
Expand All @@ -68,30 +70,25 @@ module.exports = class NewRelicTracePropagator {
!isSpanContextValid(spanContext)
) { return }

const traceParent = `${VERSION}-${spanContext.traceId}-${
spanContext.spanId
}-0${Number(spanContext.traceFlags || TraceFlags.NONE).toString(16)}`

setter.set(carrier, TRACE_PARENT_HEADER, traceParent)
if (spanContext.traceState) {
setter.set(
carrier,
TRACE_STATE_HEADER,
spanContext.traceState.serialize()
)
}
context?.transaction?.insertDistributedTraceHeaders(carrier, spanContext)
}

extract(context, carrier, getter) {
if (context.constructor.name === 'BaseContext') {
context = this.agent.tracer._contextManager.getContext()
}

if (this.agent.config.distributed_tracing.enabled !== true) {
return context
}

const traceParentHeader = getter.get(carrier, TRACE_PARENT_HEADER)
if (!traceParentHeader) return context
const traceParent = Array.isArray(traceParentHeader)
? traceParentHeader[0]
: traceParentHeader
if (typeof traceParent !== 'string') return context
// TODO: we parse it as well but the keys we return are different, should we layer this on?
const spanContext = parseTraceParent(traceParent)
if (!spanContext) return context

Expand Down
1 change: 0 additions & 1 deletion lib/shim/message-shim/subscribe-consume.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,6 @@ function createConsumerWrapper({ shim, spec, consumer }) {
* finalizes transaction name and ends transaction
*/
function endTransaction() {
tx.finalizeName(null) // Use existing partial name.
tx.end()
}
}
Expand Down
10 changes: 6 additions & 4 deletions lib/transaction/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,10 @@ const MULTIPLE_INSERT_MESSAGE =
*
* @param {object} agent The agent.
*
* @param traceId
* @fires Agent#transactionStarted
*/
function Transaction(agent) {
function Transaction(agent, traceId) {
if (!agent) {
throw new Error('every transaction must be bound to the agent')
}
Expand Down Expand Up @@ -131,7 +132,7 @@ function Transaction(agent) {
this.parentAcct = null
this.parentTransportType = null
this.parentTransportDuration = null
this._traceId = null
this._traceId = traceId || null
Object.defineProperty(this, 'traceId', {
get() {
if (this._traceId === null) {
Expand Down Expand Up @@ -949,9 +950,10 @@ function acceptDistributedTraceHeaders(transportType, headers) {
* Inserts distributed trace headers into the provided headers map.
*
* @param {object} headers
* @param {object} spanContext otel span context
*/
Transaction.prototype.insertDistributedTraceHeaders = insertDistributedTraceHeaders
function insertDistributedTraceHeaders(headers) {
function insertDistributedTraceHeaders(headers, spanContext) {
if (!headers) {
logger.trace('insertDistributedTraceHeaders called without headers.')
return
Expand All @@ -962,7 +964,7 @@ function insertDistributedTraceHeaders(headers) {
// Ensure we have priority before generating trace headers.
this._calculatePriority()

this.traceContext.addTraceContextHeaders(headers)
this.traceContext.addTraceContextHeaders(headers, spanContext)
this.isDistributedTrace = true

logger.trace('Added outbound request w3c trace context headers in transaction %s', this.id)
Expand Down
5 changes: 3 additions & 2 deletions lib/transaction/trace/segment.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const ATTRIBUTE_SCOPE = 'segment'
* for now), and has one or more children (that are also part of the same
* transaction trace), as well as an associated timer.
* @param {object} params to function
* @param {number} params.id id if passed in used as segment id. This is only used in otel bridge to ensure span id is same as segment
* @param {object} params.config agent config
* @param {string} params.name Human-readable name for this segment (e.g. 'http', 'net', 'express',
* 'mysql', etc).
Expand All @@ -38,15 +39,15 @@ const ATTRIBUTE_SCOPE = 'segment'
* @param {TraceSegment} params.root root segment
* @param {boolean} params.isRoot flag to indicate it is the root segment
*/
function TraceSegment({ config, name, collect, parentId, root, isRoot = false }) {
function TraceSegment({ id, config, name, collect, parentId, root, isRoot = false }) {
this.isRoot = isRoot
this.root = root
this.name = name
this.attributes = new Attributes(ATTRIBUTE_SCOPE)
this.spansEnabled = config?.distributed_tracing?.enabled && config?.span_events?.enabled

// Generate a unique id for use in span events.
this.id = hashes.makeId()
this.id = id || hashes.makeId()
this.parentId = parentId
this.timer = new Timer()

Expand Down
Loading

0 comments on commit 587e1cc

Please sign in to comment.