Skip to content

Commit

Permalink
feat: Added opentelemetry bridge instrumentation that adds a context …
Browse files Browse the repository at this point in the history
…manager, and processor to handle synthesizing segments and time slice metrics.
  • Loading branch information
bizob2828 committed Jan 27, 2025
1 parent cb16516 commit f6591ad
Show file tree
Hide file tree
Showing 21 changed files with 1,191 additions and 308 deletions.
896 changes: 643 additions & 253 deletions THIRD_PARTY_NOTICES.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions documentation/feature-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,10 @@ Any prerelease flags can be enabled or disabled in your agent config by adding a
* Configuration: `{ feature_flag: { kafkajs_instrumentation: true|false }}`
* Environment Variable: `NEW_RELIC_FEATURE_FLAG_KAFKAJS_INSTRUMENTATION`
* Description: Enables instrumentation of `kafkajs`.

#### otel_instrumentation
* Enabled by default: `false`
* Configuration: `{ feature_flag: { otel_instrumentation: true|false }}`
* Environment Variable: `NEW_RELIC_FEATURE_FLAG_OTEL_INSTRUMENTATION`
* 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.
* **WARNING**: This is not feature complete and is not intended to be enabled yet.
34 changes: 32 additions & 2 deletions lib/context-manager/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
*/

'use strict'
const { otelSynthesis } = require('../symbols')

module.exports = class Context {
constructor(transaction, segment) {
constructor(transaction, segment, parentContext) {
this._transaction = transaction
this._segment = segment
this._otelCtx = parentContext ? new Map(parentContext) : new Map()
}

get segment() {
Expand All @@ -19,11 +21,39 @@ module.exports = class Context {
return this._transaction
}

enterSegment({ segment, transaction = this.transaction }) {
enterSegment({ segment, transaction = this._transaction }) {
return new this.constructor(transaction, segment)
}

enterTransaction(transaction) {
return new this.constructor(transaction, transaction.trace.root)
}

// the following methods are used in opentelemetry bridge
// see: https://opentelemetry.io/docs/languages/js/context/
getValue(key) {
return this._otelCtx.get(key)
}

setValue(key, value) {
let ctx

if (value[otelSynthesis] && value[otelSynthesis].segment && value[otelSynthesis].transaction) {
const { segment, transaction } = value[otelSynthesis]
segment.start()
ctx = new this.constructor(transaction, segment, this._otelCtx)
delete value[otelSynthesis]
} else {
ctx = new this.constructor(this._transaction, this._segment, this._otelCtx)
}

ctx._otelCtx.set(key, value)
return ctx
}

deleteValue(key) {
const ctx = new this.constructor(this._transaction, this._segment, this._otelCtx)
ctx._otelCtx.delete(key)
return ctx
}
}
2 changes: 1 addition & 1 deletion lib/feature_flags.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ exports.prerelease = {
// internal_test_only is used for testing our feature flag implementation.
// It is not used to gate any features.
internal_test_only: false,

opentelemetry_bridge: false,
promise_segments: false,
reverse_naming_rules: false,
unresolved_promise_cleanup: true,
Expand Down
52 changes: 52 additions & 0 deletions lib/otel/context-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'

/**
* @see https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api.ContextManager.html
*/
class ContextManager {
#ctxMgr

constructor(agent) {
this.#ctxMgr = agent.tracer._contextManager
}

active() {
return this.#ctxMgr.getContext()
}

bind(context, target) {
return boundContext.bind(this)

function boundContext(...args) {
return this.with(context, target, this, ...args)
}
}

Check warning on line 28 in lib/otel/context-manager.js

View check run for this annotation

Codecov / codecov/patch

lib/otel/context-manager.js#L23-L28

Added lines #L23 - L28 were not covered by tests

/**
* Runs the callback within the provided context, optionally
* bound with a provided `this`.
*
* @param context
* @param callback
* @param thisRef
* @param args
*/
with(context, callback, thisRef, ...args) {
return this.#ctxMgr.runInContext(context, callback, thisRef, args)
}

enable() {
return this
}

Check warning on line 45 in lib/otel/context-manager.js

View check run for this annotation

Codecov / codecov/patch

lib/otel/context-manager.js#L44-L45

Added lines #L44 - L45 were not covered by tests

disable() {
return this
}
}

module.exports = ContextManager
25 changes: 25 additions & 0 deletions lib/otel/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2025 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'
const { diag, DiagLogLevel } = require('@opentelemetry/api')

// Map New Relic log levels to OTel log levels
const logLevels = {
trace: 'VERBOSE',
debug: 'DEBUG',
info: 'INFO',
warn: 'WARN',
error: 'ERROR',
fatal: 'ERROR'
}

module.exports = function createOtelLogger(logger, config) {
// enable exporter logging
// OTel API calls "verbose" what we call "trace".
logger.verbose = logger.trace
const logLevel = DiagLogLevel[logLevels[config.logging.level]]
diag.setLogger(logger, logLevel)
}
2 changes: 2 additions & 0 deletions lib/otel/segments/http-external.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
'use strict'
const NAMES = require('../../metrics/names')
const { SEMATTRS_HTTP_HOST } = require('@opentelemetry/semantic-conventions')
const recordExternal = require('../../metrics/recorders/http_external')

module.exports = function createHttpExternalSegment(agent, otelSpan) {
const context = agent.tracer.getContext()
const host = otelSpan.attributes[SEMATTRS_HTTP_HOST] || 'Unknown'
const name = NAMES.EXTERNAL.PREFIX + host
const segment = agent.tracer.createSegment({
name,
recorder: recordExternal(host, 'http'),
parent: context.segment,
transaction: context.transaction
})
Expand Down
4 changes: 3 additions & 1 deletion lib/otel/segments/internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
*/

'use strict'
const customRecorder = require('../../metrics/recorders/custom')

module.exports = function createInternalSegment(agent, otelSpan) {
const context = agent.tracer.getContext()
const name = `Custom/${otelSpan.name}`
const name = otelSpan.name
const segment = agent.tracer.createSegment({
name,
parent: context.segment,
recorder: customRecorder,
transaction: context.transaction
})
return { segment, transaction: context.transaction }
Expand Down
45 changes: 45 additions & 0 deletions lib/otel/setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2025 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'
const { BasicTracerProvider } = require('@opentelemetry/sdk-trace-base')
const { Resource } = require('@opentelemetry/resources')
const { SEMRESATTRS_SERVICE_NAME } = require('@opentelemetry/semantic-conventions')
const NrSpanProcessor = require('./span-processor')
const ContextManager = require('./context-manager')
const defaultLogger = require('../logger').child({ component: 'opentelemetry-bridge' })
const createOtelLogger = require('./logger')

module.exports = function setupOtel(agent, logger = defaultLogger) {
if (agent.config.feature_flag.opentelemetry_bridge !== true) {
logger.warn(
'`feature_flag.opentelemetry_bridge` is not enabled, skipping setup of opentelemetry-bridge'
)
return
}

createOtelLogger(logger, agent.config)

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

})
provider.register({
contextManager: new ContextManager(agent)
// propagator: // todo: https://github.com/newrelic/node-newrelic/issues/2662
})

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

return provider
}
43 changes: 43 additions & 0 deletions lib/otel/span-processor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2025 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'
const SegmentSynthesizer = require('./segment-synthesis')
const { otelSynthesis } = require('../symbols')
const { hrTimeToMilliseconds } = require('@opentelemetry/core')

module.exports = class NrSpanProcessor {
constructor(agent) {
this.agent = agent
this.synthesizer = new SegmentSynthesizer(agent)
this.tracer = agent.tracer
}

/**
* Synthesize segment at start of span and assign to a symbol
* that will be removed in Context after it is assigned to the context.
* @param span
*/
onStart(span) {
span[otelSynthesis] = this.synthesizer.synthesize(span)
}

/**
* Update the segment duration from span and reconcile
* any attributes that were added after the start
* @param span
*/
onEnd(span) {
const ctx = this.tracer.getContext()
this.updateDuration(ctx._segment, span)
// TODO: add attributes from span that did not exist at start
}

updateDuration(segment, span) {
segment.end()
const duration = hrTimeToMilliseconds(span.duration)
segment.overwriteDurationInMillis(duration)
}
}
2 changes: 2 additions & 0 deletions lib/shimmer.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ let pkgsToHook = []
const NAMES = require('./metrics/names')
const symbols = require('./symbols')
const { unsubscribe } = require('./instrumentation/undici')
const setupOtel = require('./otel/setup')

const CORE_INSTRUMENTATION = {
child_process: {
Expand Down Expand Up @@ -393,6 +394,7 @@ const shimmer = (module.exports = {
bootstrapInstrumentation: function bootstrapInstrumentation(agent) {
shimmer.registerCoreInstrumentation(agent)
shimmer.registerThirdPartyInstrumentation(agent)
setupOtel(agent)
},

registerInstrumentation: function registerInstrumentation(opts) {
Expand Down
1 change: 1 addition & 0 deletions lib/symbols.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = {
openAiApiKey: Symbol('openAiApiKey'),
parentSegment: Symbol('parentSegment'),
langchainRunId: Symbol('runId'),
otelSynthesis: Symbol('otelSynthesis'),
prismaConnection: Symbol('prismaConnection'),
prismaModelCall: Symbol('modelCall'),
redisClientOpts: Symbol('clientOptions'),
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@
"@grpc/proto-loader": "^0.7.5",
"@newrelic/security-agent": "^2.2.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/core": "^1.30.0",
"@opentelemetry/resources": "^1.30.1",
"@opentelemetry/sdk-trace-base": "^1.30.0",
"@opentelemetry/semantic-conventions": "^1.27.0",
"@tyriar/fibonacci-heap": "^2.0.7",
"concat-stream": "^2.0.0",
Expand Down Expand Up @@ -226,7 +229,6 @@
"@newrelic/newrelic-oss-cli": "^0.1.2",
"@newrelic/test-utilities": "^9.1.0",
"@octokit/rest": "^18.0.15",
"@opentelemetry/sdk-trace-base": "^1.27.0",
"@slack/bolt": "^3.7.0",
"@smithy/eventstream-codec": "^2.2.0",
"@smithy/util-utf8": "^2.3.0",
Expand Down
1 change: 1 addition & 0 deletions test/unit/feature_flag.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const used = [
'legacy_context_manager',
'native_metrics',
'new_promise_tracking',
'opentelemetry_bridge',
'promise_segments',
'protocol_17',
'serverless_mode',
Expand Down
Loading

0 comments on commit f6591ad

Please sign in to comment.