Skip to content

Commit d2e8a9e

Browse files
authored
feat: Added opentelemetry bridge instrumentation that adds a context manager, and processor to handle synthesizing segments and time slice metrics. (#2906)
1 parent acd0c90 commit d2e8a9e

21 files changed

+1214
-308
lines changed

THIRD_PARTY_NOTICES.md

+643-253
Large diffs are not rendered by default.

documentation/feature-flags.md

+7
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,10 @@ Any prerelease flags can be enabled or disabled in your agent config by adding a
3232
* Configuration: `{ feature_flag: { kafkajs_instrumentation: true|false }}`
3333
* Environment Variable: `NEW_RELIC_FEATURE_FLAG_KAFKAJS_INSTRUMENTATION`
3434
* Description: Enables instrumentation of `kafkajs`.
35+
36+
#### otel_instrumentation
37+
* Enabled by default: `false`
38+
* Configuration: `{ feature_flag: { otel_instrumentation: true|false }}`
39+
* Environment Variable: `NEW_RELIC_FEATURE_FLAG_OTEL_INSTRUMENTATION`
40+
* Description: Enables the creation of Transaction Trace segments and time slices metrics from opentelemetry spans. This will help drive New Relic UI experience for opentelemetry spans.
41+
* **WARNING**: This is not feature complete and is not intended to be enabled yet.

lib/context-manager/context.js

+51-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
*/
55

66
'use strict'
7+
const { otelSynthesis } = require('../symbols')
78

89
module.exports = class Context {
9-
constructor(transaction, segment) {
10+
constructor(transaction, segment, parentContext) {
1011
this._transaction = transaction
1112
this._segment = segment
13+
this._otelCtx = parentContext ? new Map(parentContext) : new Map()
1214
}
1315

1416
get segment() {
@@ -19,11 +21,58 @@ module.exports = class Context {
1921
return this._transaction
2022
}
2123

22-
enterSegment({ segment, transaction = this.transaction }) {
24+
enterSegment({ segment, transaction = this._transaction }) {
2325
return new this.constructor(transaction, segment)
2426
}
2527

2628
enterTransaction(transaction) {
2729
return new this.constructor(transaction, transaction.trace.root)
2830
}
31+
32+
/**
33+
* Required for bridging OTEL data into the agent.
34+
*
35+
* @param {string} key Stored entity name to retrieve.
36+
*
37+
* @returns {*} The stored value.
38+
*/
39+
getValue(key) {
40+
return this._otelCtx.get(key)
41+
}
42+
43+
/**
44+
* Required for bridging OTEL data into the agent.
45+
*
46+
* @param {string} key Name for stored value.
47+
* @param {*} value Value to store.
48+
*
49+
* @returns {object} The context manager object.
50+
*/
51+
setValue(key, value) {
52+
let ctx
53+
54+
if (value[otelSynthesis] && value[otelSynthesis].segment && value[otelSynthesis].transaction) {
55+
const { segment, transaction } = value[otelSynthesis]
56+
segment.start()
57+
ctx = new this.constructor(transaction, segment, this._otelCtx)
58+
} else {
59+
ctx = new this.constructor(this._transaction, this._segment, this._otelCtx)
60+
}
61+
62+
ctx._otelCtx.set(key, value)
63+
return ctx
64+
}
65+
66+
/**
67+
* Required for bridging OTEL data into the agent.
68+
*
69+
* @param {string} key Named value to remove from the store.
70+
*
71+
* @returns {object} The context manager object.
72+
*/
73+
deleteValue(key) {
74+
const ctx = new this.constructor(this._transaction, this._segment, this._otelCtx)
75+
ctx._otelCtx.delete(key)
76+
return ctx
77+
}
2978
}

lib/feature_flags.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ exports.prerelease = {
1010
// internal_test_only is used for testing our feature flag implementation.
1111
// It is not used to gate any features.
1212
internal_test_only: false,
13-
13+
opentelemetry_bridge: false,
1414
promise_segments: false,
1515
reverse_naming_rules: false,
1616
unresolved_promise_cleanup: true,

lib/otel/context-manager.js

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2024 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
/**
9+
* @see https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api.ContextManager.html
10+
*/
11+
class ContextManager {
12+
#ctxMgr
13+
14+
constructor(agent) {
15+
this.#ctxMgr = agent.tracer._contextManager
16+
}
17+
18+
active() {
19+
return this.#ctxMgr.getContext()
20+
}
21+
22+
bind(context, target) {
23+
return boundContext.bind(this)
24+
25+
function boundContext(...args) {
26+
return this.with(context, target, this, ...args)
27+
}
28+
}
29+
30+
/**
31+
* Runs the callback within the provided context, optionally
32+
* bound with a provided `this`.
33+
*
34+
* @param context
35+
* @param callback
36+
* @param thisRef
37+
* @param args
38+
*/
39+
with(context, callback, thisRef, ...args) {
40+
return this.#ctxMgr.runInContext(context, callback, thisRef, args)
41+
}
42+
43+
enable() {
44+
return this
45+
}
46+
47+
disable() {
48+
return this
49+
}
50+
}
51+
52+
module.exports = ContextManager

lib/otel/logger.js

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
const { diag, DiagLogLevel } = require('@opentelemetry/api')
8+
9+
// Map New Relic log levels to OTel log levels
10+
const logLevels = {
11+
trace: 'VERBOSE',
12+
debug: 'DEBUG',
13+
info: 'INFO',
14+
warn: 'WARN',
15+
error: 'ERROR',
16+
fatal: 'ERROR'
17+
}
18+
19+
module.exports = function createOtelLogger(logger, config) {
20+
// enable exporter logging
21+
// OTel API calls "verbose" what we call "trace".
22+
logger.verbose = logger.trace
23+
const logLevel = DiagLogLevel[logLevels[config.logging.level]]
24+
diag.setLogger(logger, logLevel)
25+
}

lib/otel/segments/http-external.js

+2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
'use strict'
77
const NAMES = require('../../metrics/names')
88
const { SEMATTRS_HTTP_HOST } = require('@opentelemetry/semantic-conventions')
9+
const recordExternal = require('../../metrics/recorders/http_external')
910

1011
module.exports = function createHttpExternalSegment(agent, otelSpan) {
1112
const context = agent.tracer.getContext()
1213
const host = otelSpan.attributes[SEMATTRS_HTTP_HOST] || 'Unknown'
1314
const name = NAMES.EXTERNAL.PREFIX + host
1415
const segment = agent.tracer.createSegment({
1516
name,
17+
recorder: recordExternal(host, 'http'),
1618
parent: context.segment,
1719
transaction: context.transaction
1820
})

lib/otel/segments/internal.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
*/
55

66
'use strict'
7+
const customRecorder = require('../../metrics/recorders/custom')
78

89
module.exports = function createInternalSegment(agent, otelSpan) {
910
const context = agent.tracer.getContext()
10-
const name = `Custom/${otelSpan.name}`
11+
const name = otelSpan.name
1112
const segment = agent.tracer.createSegment({
1213
name,
1314
parent: context.segment,
15+
recorder: customRecorder,
1416
transaction: context.transaction
1517
})
1618
return { segment, transaction: context.transaction }

lib/otel/setup.js

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
const { BasicTracerProvider } = require('@opentelemetry/sdk-trace-base')
8+
const { Resource } = require('@opentelemetry/resources')
9+
const { SEMRESATTRS_SERVICE_NAME } = require('@opentelemetry/semantic-conventions')
10+
const NrSpanProcessor = require('./span-processor')
11+
const ContextManager = require('./context-manager')
12+
const defaultLogger = require('../logger').child({ component: 'opentelemetry-bridge' })
13+
const createOtelLogger = require('./logger')
14+
15+
module.exports = function setupOtel(agent, logger = defaultLogger) {
16+
if (agent.config.feature_flag.opentelemetry_bridge !== true) {
17+
logger.warn(
18+
'`feature_flag.opentelemetry_bridge` is not enabled, skipping setup of opentelemetry-bridge'
19+
)
20+
return
21+
}
22+
23+
createOtelLogger(logger, agent.config)
24+
25+
const provider = new BasicTracerProvider({
26+
spanProcessors: [new NrSpanProcessor(agent)],
27+
resource: new Resource({
28+
[SEMRESATTRS_SERVICE_NAME]: agent.config.applications()[0]
29+
}),
30+
generalLimits: {
31+
attributeValueLengthLimit: 4095
32+
}
33+
34+
})
35+
provider.register({
36+
contextManager: new ContextManager(agent)
37+
// propagator: // todo: https://github.com/newrelic/node-newrelic/issues/2662
38+
})
39+
40+
agent.metrics
41+
.getOrCreateMetric('Supportability/Nodejs/OpenTelemetryBridge/Setup')
42+
.incrementCallCount()
43+
44+
return provider
45+
}

lib/otel/span-processor.js

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
const SegmentSynthesizer = require('./segment-synthesis')
8+
const { otelSynthesis } = require('../symbols')
9+
const { hrTimeToMilliseconds } = require('@opentelemetry/core')
10+
11+
module.exports = class NrSpanProcessor {
12+
constructor(agent) {
13+
this.agent = agent
14+
this.synthesizer = new SegmentSynthesizer(agent)
15+
this.tracer = agent.tracer
16+
}
17+
18+
/**
19+
* Synthesize segment at start of span and assign to a symbol
20+
* that will be removed in `onEnd` once the correspondig
21+
* segment is read.
22+
* @param span
23+
*/
24+
onStart(span) {
25+
span[otelSynthesis] = this.synthesizer.synthesize(span)
26+
}
27+
28+
/**
29+
* Update the segment duration from span and reconcile
30+
* any attributes that were added after the start
31+
* @param span
32+
*/
33+
onEnd(span) {
34+
this.updateDuration(span)
35+
// TODO: add attributes from span that did not exist at start
36+
}
37+
38+
updateDuration(span) {
39+
if (span[otelSynthesis] && span[otelSynthesis].segment) {
40+
const { segment } = span[otelSynthesis]
41+
segment.touch()
42+
const duration = hrTimeToMilliseconds(span.duration)
43+
segment.overwriteDurationInMillis(duration)
44+
delete span[otelSynthesis]
45+
}
46+
}
47+
}

lib/shimmer.js

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ let pkgsToHook = []
2222
const NAMES = require('./metrics/names')
2323
const symbols = require('./symbols')
2424
const { unsubscribe } = require('./instrumentation/undici')
25+
const setupOtel = require('./otel/setup')
2526

2627
const CORE_INSTRUMENTATION = {
2728
child_process: {
@@ -393,6 +394,7 @@ const shimmer = (module.exports = {
393394
bootstrapInstrumentation: function bootstrapInstrumentation(agent) {
394395
shimmer.registerCoreInstrumentation(agent)
395396
shimmer.registerThirdPartyInstrumentation(agent)
397+
setupOtel(agent)
396398
},
397399

398400
registerInstrumentation: function registerInstrumentation(opts) {

lib/symbols.js

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module.exports = {
2626
openAiApiKey: Symbol('openAiApiKey'),
2727
parentSegment: Symbol('parentSegment'),
2828
langchainRunId: Symbol('runId'),
29+
otelSynthesis: Symbol('otelSynthesis'),
2930
prismaConnection: Symbol('prismaConnection'),
3031
prismaModelCall: Symbol('modelCall'),
3132
redisClientOpts: Symbol('clientOptions'),

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,9 @@
199199
"@grpc/proto-loader": "^0.7.5",
200200
"@newrelic/security-agent": "^2.2.0",
201201
"@opentelemetry/api": "^1.9.0",
202+
"@opentelemetry/core": "^1.30.0",
203+
"@opentelemetry/resources": "^1.30.1",
204+
"@opentelemetry/sdk-trace-base": "^1.30.0",
202205
"@opentelemetry/semantic-conventions": "^1.27.0",
203206
"@tyriar/fibonacci-heap": "^2.0.7",
204207
"concat-stream": "^2.0.0",
@@ -226,7 +229,6 @@
226229
"@newrelic/newrelic-oss-cli": "^0.1.2",
227230
"@newrelic/test-utilities": "^9.1.0",
228231
"@octokit/rest": "^18.0.15",
229-
"@opentelemetry/sdk-trace-base": "^1.27.0",
230232
"@slack/bolt": "^3.7.0",
231233
"@smithy/eventstream-codec": "^2.2.0",
232234
"@smithy/util-utf8": "^2.3.0",

test/unit/feature_flag.test.js

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const used = [
2424
'legacy_context_manager',
2525
'native_metrics',
2626
'new_promise_tracking',
27+
'opentelemetry_bridge',
2728
'promise_segments',
2829
'protocol_17',
2930
'serverless_mode',

0 commit comments

Comments
 (0)