newrelic 13.3.2 → 13.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NEWS.md +21 -0
- package/lib/otel/metrics/index.js +3 -3
- package/lib/otel/setup.js +6 -0
- package/lib/otel/traces/index.js +1 -1
- package/lib/subscribers/base.js +101 -9
- package/lib/subscribers/openai/chat.js +27 -0
- package/lib/subscribers/openai/config.js +3 -3
- package/package.json +1 -1
package/NEWS.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
### v13.3.3 (2025-09-22)
|
|
2
|
+
|
|
3
|
+
#### Features
|
|
4
|
+
|
|
5
|
+
* Added support for tracing callback on subscribers ([#3371](https://github.com/newrelic/node-newrelic/pull/3371)) ([e6ad52e](https://github.com/newrelic/node-newrelic/commit/e6ad52e1f897ec757e90f9fc12f592e1c6eec2fe))
|
|
6
|
+
|
|
7
|
+
#### Bug fixes
|
|
8
|
+
|
|
9
|
+
* Updated `openai` instrumentation to properly return APIPromise to avoid crashing when using `completions.parse` or `responses.parse` ([#3382](https://github.com/newrelic/node-newrelic/pull/3382)) ([c7ccf26](https://github.com/newrelic/node-newrelic/commit/c7ccf260ffc9c788bde73b43231b716199724d09))
|
|
10
|
+
|
|
11
|
+
#### Documentation
|
|
12
|
+
|
|
13
|
+
* Updated compatibility report ([#3370](https://github.com/newrelic/node-newrelic/pull/3370)) ([754c2f0](https://github.com/newrelic/node-newrelic/commit/754c2f089339268fb73a3ff7765a630ea8982d0d))
|
|
14
|
+
|
|
15
|
+
#### Miscellaneous chores
|
|
16
|
+
|
|
17
|
+
* Removed unused `loader.mjs` ([#3376](https://github.com/newrelic/node-newrelic/pull/3376)) ([66a9fd5](https://github.com/newrelic/node-newrelic/commit/66a9fd543d1a7854b7405c7b5b884d8a71773b85))
|
|
18
|
+
* Updated metrics for OTel Bridge enablement/disablement ([#3381](https://github.com/newrelic/node-newrelic/pull/3381)) ([b976c37](https://github.com/newrelic/node-newrelic/commit/b976c376fe187bda5bde1b8cd4b86a7de86fe4f9))
|
|
19
|
+
* Updated supportability metric names for OTel Bridge enablement… ([#3383](https://github.com/newrelic/node-newrelic/pull/3383)) ([c7c1d6c](https://github.com/newrelic/node-newrelic/commit/c7c1d6c942c6b53fd944e2259b42d1e7e7be483a))
|
|
20
|
+
|
|
1
21
|
### v13.3.2 (2025-09-10)
|
|
2
22
|
|
|
3
23
|
#### Bug fixes
|
|
@@ -7885,3 +7905,4 @@ Special thanks to Ryan Copley (@RyanCopley) for the contribution.
|
|
|
7885
7905
|
|
|
7886
7906
|
|
|
7887
7907
|
|
|
7908
|
+
|
|
@@ -42,7 +42,7 @@ class SetupMetrics extends SetupSignal {
|
|
|
42
42
|
const getMeter = provider.getMeter
|
|
43
43
|
provider.getMeter = function nrGetMeter(...args) {
|
|
44
44
|
agent.metrics
|
|
45
|
-
.getOrCreateMetric('Supportability/Nodejs/OpenTelemetryBridge/
|
|
45
|
+
.getOrCreateMetric('Supportability/Metrics/Nodejs/OpenTelemetryBridge/getMeter')
|
|
46
46
|
.incrementCallCount()
|
|
47
47
|
|
|
48
48
|
const meter = getMeter.apply(provider, args)
|
|
@@ -61,7 +61,7 @@ class SetupMetrics extends SetupSignal {
|
|
|
61
61
|
// + createObservableUpDownCounter
|
|
62
62
|
meter[method] = function nrWrappedMethod(...args) {
|
|
63
63
|
agent.metrics
|
|
64
|
-
.getOrCreateMetric(`Supportability/Nodejs/OpenTelemetryBridge/
|
|
64
|
+
.getOrCreateMetric(`Supportability/Metrics/Nodejs/OpenTelemetryBridge/meter/${method}`)
|
|
65
65
|
.incrementCallCount()
|
|
66
66
|
return originals[method].apply(meter, args)
|
|
67
67
|
}
|
|
@@ -73,7 +73,7 @@ class SetupMetrics extends SetupSignal {
|
|
|
73
73
|
this.coreApi.metrics.setGlobalMeterProvider(provider)
|
|
74
74
|
|
|
75
75
|
agent.metrics
|
|
76
|
-
.getOrCreateMetric('Supportability/Nodejs/OpenTelemetryBridge/
|
|
76
|
+
.getOrCreateMetric('Supportability/Metrics/Nodejs/OpenTelemetryBridge/enabled')
|
|
77
77
|
.incrementCallCount()
|
|
78
78
|
|
|
79
79
|
// We need access to `agent.config.entity_guid` in order to attach metrics
|
package/lib/otel/setup.js
CHANGED
|
@@ -41,6 +41,9 @@ function setupOtel(agent, logger = defaultLogger) {
|
|
|
41
41
|
signals.push(signal)
|
|
42
42
|
} else {
|
|
43
43
|
logger.debug('`opentelemetry_bridge.traces` is not enabled, skipping')
|
|
44
|
+
agent.metrics
|
|
45
|
+
.getOrCreateMetric('Supportability/Tracing/Nodejs/OpenTelemetryBridge/disabled')
|
|
46
|
+
.incrementCallCount()
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
if (agent.config.opentelemetry_bridge.metrics.enabled === true) {
|
|
@@ -48,6 +51,9 @@ function setupOtel(agent, logger = defaultLogger) {
|
|
|
48
51
|
signals.push(signal)
|
|
49
52
|
} else {
|
|
50
53
|
logger.debug('`opentelemetry_bridge.metrics` is not enabled, skipping')
|
|
54
|
+
agent.metrics
|
|
55
|
+
.getOrCreateMetric('Supportability/Metrics/Nodejs/OpenTelemetryBridge/disabled')
|
|
56
|
+
.incrementCallCount()
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
if (agent.config.opentelemetry_bridge.logs.enabled === true) {
|
package/lib/otel/traces/index.js
CHANGED
|
@@ -28,7 +28,7 @@ class SetupTraces extends SetupSignal {
|
|
|
28
28
|
this.coreApi.trace.setGlobalTracerProvider(traceProvider)
|
|
29
29
|
|
|
30
30
|
agent.metrics
|
|
31
|
-
.getOrCreateMetric('Supportability/Nodejs/OpenTelemetryBridge/
|
|
31
|
+
.getOrCreateMetric('Supportability/Tracing/Nodejs/OpenTelemetryBridge/enabled')
|
|
32
32
|
.incrementCallCount()
|
|
33
33
|
}
|
|
34
34
|
|
package/lib/subscribers/base.js
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
const { tracingChannel } = require('node:diagnostics_channel')
|
|
9
9
|
const semver = require('semver')
|
|
10
10
|
const NAMES = require('../metrics/names')
|
|
11
|
+
// Used for the `traceCallback` work.
|
|
12
|
+
// This can be removed when we add true support into orchestrion
|
|
13
|
+
const makeCall = (fn) => (...args) => fn.call(...args)
|
|
14
|
+
const ArrayPrototypeAt = makeCall(Array.prototype.at)
|
|
15
|
+
const ArrayPrototypeSplice = makeCall(Array.prototype.splice)
|
|
16
|
+
// End temp work
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* The baseline parameters available to all subscribers.
|
|
@@ -15,9 +21,8 @@ const NAMES = require('../metrics/names')
|
|
|
15
21
|
* @typedef {object} SubscriberParams
|
|
16
22
|
* @property {object} agent A New Relic Node.js agent instance.
|
|
17
23
|
* @property {object} logger An agent logger instance.
|
|
18
|
-
* @property {string} packageName The
|
|
19
|
-
*
|
|
20
|
-
* function.
|
|
24
|
+
* @property {string} packageName The package name being instrumented.
|
|
25
|
+
* This is what a developer would provide to the `require` function.
|
|
21
26
|
* @property {string} channelName A unique name for the diagnostics channel
|
|
22
27
|
* that will be created and monitored.
|
|
23
28
|
*/
|
|
@@ -44,6 +49,7 @@ const NAMES = require('../metrics/names')
|
|
|
44
49
|
* @property {string} id A unique identifier for the subscriber, combining the prefix, package name, and channel name.
|
|
45
50
|
* @property {TracingChannel} channel The tracing channel instance this subscriber will be monitoring.
|
|
46
51
|
* @property {AsyncLocalStorage} store The async local storage instance used for context management.
|
|
52
|
+
* @property {number} callback position of callback if it needs to be wrapped for instrumentation. -1 means last argument
|
|
47
53
|
*/
|
|
48
54
|
class Subscriber {
|
|
49
55
|
constructor({ agent, logger, packageName, channelName }) {
|
|
@@ -60,6 +66,7 @@ class Subscriber {
|
|
|
60
66
|
this.id = `${this.prefix}${this.packageName}:${this.channelName}`
|
|
61
67
|
this.channel = tracingChannel(this.id)
|
|
62
68
|
this.store = agent.tracer._contextManager._asyncLocalStorage
|
|
69
|
+
this.callback = null
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
shouldCreateSegment(parent) {
|
|
@@ -68,6 +75,49 @@ class Subscriber {
|
|
|
68
75
|
)
|
|
69
76
|
}
|
|
70
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Note: This is a temporary patch until we can get the correct implementation
|
|
80
|
+
* of `tracingChannel.traceCallback` into orchestrion-js.
|
|
81
|
+
*
|
|
82
|
+
* This will wrap a callback at a given position and reassign the callback argument to the wrapped one
|
|
83
|
+
*
|
|
84
|
+
* @param {number} position index of the callback, you can specify -1 to be the last
|
|
85
|
+
* @param {object} context the event passed to the tracing channel hooks
|
|
86
|
+
*/
|
|
87
|
+
traceCallback(position, context) {
|
|
88
|
+
this.logger.trace('Wrapping the callback at position %s', position)
|
|
89
|
+
const { asyncStart, asyncEnd, error } = this.channel
|
|
90
|
+
function wrappedCallback(err, res) {
|
|
91
|
+
// assigning a boolean to the context so we know that the
|
|
92
|
+
// `error`, `asyncStart`, and `asyncEnd` are coming from the wrapped callback
|
|
93
|
+
context.callback = true
|
|
94
|
+
if (err) {
|
|
95
|
+
context.error = err
|
|
96
|
+
error.publish(context)
|
|
97
|
+
} else {
|
|
98
|
+
context.result = res
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Using runStores here enables manual context failure recovery
|
|
102
|
+
asyncStart.runStores(context, () => {
|
|
103
|
+
try {
|
|
104
|
+
if (callback) {
|
|
105
|
+
return Reflect.apply(callback, this, arguments)
|
|
106
|
+
}
|
|
107
|
+
} finally {
|
|
108
|
+
asyncEnd.publish(context)
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const callback = ArrayPrototypeAt(context.arguments, position)
|
|
114
|
+
if (typeof callback !== 'function') {
|
|
115
|
+
this.logger.trace('Callback is not present, not wrapping')
|
|
116
|
+
} else {
|
|
117
|
+
ArrayPrototypeSplice(context.arguments, position, 1, wrappedCallback)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
71
121
|
/**
|
|
72
122
|
* Creates the `Supportability/Features/Instrumentation/OnRequire/<packageName>`
|
|
73
123
|
* and `Supportability/Features/Instrumentation/OnRequire/<packageName>/Version/<majorVersion>`
|
|
@@ -123,12 +173,12 @@ class Subscriber {
|
|
|
123
173
|
segment.opaque = this.opaque
|
|
124
174
|
segment.shimId = this.packageName
|
|
125
175
|
segment.start()
|
|
126
|
-
this.logger.trace('Created segment %s', name)
|
|
176
|
+
this.logger.trace('Created segment %s, returning new context', name)
|
|
127
177
|
this.addAttributes(segment)
|
|
128
178
|
const newCtx = ctx.enterSegment({ segment })
|
|
129
179
|
return newCtx
|
|
130
180
|
} else {
|
|
131
|
-
this.logger.trace('Failed to create segment for %s', name)
|
|
181
|
+
this.logger.trace('Failed to create segment for %s, returning existing context', name)
|
|
132
182
|
return ctx
|
|
133
183
|
}
|
|
134
184
|
}
|
|
@@ -144,6 +194,7 @@ class Subscriber {
|
|
|
144
194
|
|
|
145
195
|
/**
|
|
146
196
|
* Checks if the subscriber is enabled based on the agent's configuration.
|
|
197
|
+
* @returns boolean if subscriber is enabled
|
|
147
198
|
*/
|
|
148
199
|
get enabled() {
|
|
149
200
|
return this.config.instrumentation[this.packageName].enabled === true
|
|
@@ -156,14 +207,27 @@ class Subscriber {
|
|
|
156
207
|
*/
|
|
157
208
|
enable() {
|
|
158
209
|
this.channel.start.bindStore(this.store, (data) => {
|
|
210
|
+
// only wrap the callback if a subscriber has a callback property defined
|
|
211
|
+
if (this.callback !== null) {
|
|
212
|
+
this.traceCallback(this.callback, data)
|
|
213
|
+
}
|
|
159
214
|
this.trackInstrumentationUsage(data?.moduleVersion)
|
|
160
215
|
const ctx = this.agent.tracer.getContext()
|
|
161
216
|
if (this.requireActiveTx && !ctx?.transaction?.isActive()) {
|
|
162
|
-
this.logger.
|
|
217
|
+
this.logger.trace('Not recording event for %s, transaction is not active', this.package)
|
|
163
218
|
return ctx
|
|
164
219
|
}
|
|
165
220
|
|
|
166
|
-
|
|
221
|
+
const result = this.handler(data, ctx)
|
|
222
|
+
// we cannot rely on the context manager to obtain the active segment
|
|
223
|
+
// in the `asyncStart` and `asyncEnd` events. This is because other instrumented
|
|
224
|
+
// functions are being executed at times. so we assign the active segment on the data
|
|
225
|
+
// so it can be used later to properly touch the segment in `asyncStart` and `asyncEnd`
|
|
226
|
+
if (this.callback !== null) {
|
|
227
|
+
data.segment = result?.segment
|
|
228
|
+
this.logger.trace('Adding segment %s to event context', data?.segment?.name)
|
|
229
|
+
}
|
|
230
|
+
return result
|
|
167
231
|
})
|
|
168
232
|
}
|
|
169
233
|
|
|
@@ -174,13 +238,41 @@ class Subscriber {
|
|
|
174
238
|
this.channel.start.unbindStore(this.store)
|
|
175
239
|
}
|
|
176
240
|
|
|
241
|
+
/**
|
|
242
|
+
* This should only be used for callback based functions to touch the segment for the function
|
|
243
|
+
* that implements a callback.
|
|
244
|
+
* @param {object} data event passed to asyncStart hook
|
|
245
|
+
*/
|
|
246
|
+
asyncStart(data) {
|
|
247
|
+
const ctx = this.agent.tracer.getContext()
|
|
248
|
+
if (data.callback !== true || this.internal === true || (this.requireActiveTx && !ctx?.transaction?.isActive())) {
|
|
249
|
+
this.logger.trace('Not touching parent in asyncStart for %s, transaction is not active? %s, segment is internal? %s, or no callback to bind', this.id, ctx?.transaction?.isActive(), this.internal)
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
this.logger.trace('touching segment %s, in asyncStart', data?.segment?.name)
|
|
254
|
+
data?.segment?.touch()
|
|
255
|
+
}
|
|
256
|
+
|
|
177
257
|
/**
|
|
178
258
|
* Common handler for when async events end.
|
|
179
259
|
* It gets the context and touches the segment if it exists.
|
|
260
|
+
* @param {object} data event passed to asyncEnd hook
|
|
180
261
|
*/
|
|
181
|
-
asyncEnd() {
|
|
262
|
+
asyncEnd(data) {
|
|
182
263
|
const ctx = this.agent.tracer.getContext()
|
|
183
|
-
|
|
264
|
+
if (this.internal === true) {
|
|
265
|
+
this.logger.trace('asyncEnd occurring for %s internal event, not touching segment', this.id)
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (data?.callback === true) {
|
|
270
|
+
this.logger.trace('touching callback segment %s, in asyncEnd', data?.segment?.name)
|
|
271
|
+
data?.segment?.touch()
|
|
272
|
+
} else {
|
|
273
|
+
this.logger.trace('touching segment %s, in asyncEnd', ctx?.segment?.name)
|
|
274
|
+
ctx?.segment?.touch()
|
|
275
|
+
}
|
|
184
276
|
}
|
|
185
277
|
|
|
186
278
|
/*
|
|
@@ -23,6 +23,7 @@ const MIN_STREAM_VERSION = '4.12.2'
|
|
|
23
23
|
class OpenAIChatCompletions extends OpenAISubscriber {
|
|
24
24
|
constructor({ agent, logger, channelName = 'nr_completionsCreate' }) {
|
|
25
25
|
super({ agent, logger, channelName })
|
|
26
|
+
this.events = ['asyncEnd', 'end']
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
handler(data, ctx) {
|
|
@@ -42,6 +43,32 @@ class OpenAIChatCompletions extends OpenAISubscriber {
|
|
|
42
43
|
return newCtx
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Temporary fix as `tracePromise` wraps the promise in a native one.
|
|
48
|
+
* We are now wrapping `openai.chat.completions.parse` in a traceSync call
|
|
49
|
+
* and then wrapping the promise here so it returns the custom promise.
|
|
50
|
+
* OpenAI has a [custom promise](https://github.com/openai/openai-node/blob/master/src/core/api-promise.ts) that crashes applications using `openai.chat.completions.parse`
|
|
51
|
+
* see: https://github.com/newrelic/node-newrelic/issues/3379
|
|
52
|
+
* see: https://github.com/nodejs/node/issues/59936
|
|
53
|
+
* @param data
|
|
54
|
+
*/
|
|
55
|
+
end(data) {
|
|
56
|
+
const promise = data?.result
|
|
57
|
+
if (!promise.then) {
|
|
58
|
+
return promise
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return promise.then((result) => {
|
|
62
|
+
data.result = result
|
|
63
|
+
this.channel.asyncEnd.publish(data)
|
|
64
|
+
return result
|
|
65
|
+
}).catch((err) => {
|
|
66
|
+
data.error = err
|
|
67
|
+
this.channel.asyncEnd.publish(data)
|
|
68
|
+
return err
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
45
72
|
asyncEnd(data) {
|
|
46
73
|
const ctx = this.agent.tracer.getContext()
|
|
47
74
|
if (!ctx?.segment || !ctx?.transaction) {
|
|
@@ -16,7 +16,7 @@ module.exports = {
|
|
|
16
16
|
functionQuery: {
|
|
17
17
|
className: 'Completions',
|
|
18
18
|
methodName: 'create',
|
|
19
|
-
kind: '
|
|
19
|
+
kind: 'Sync'
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
{
|
|
@@ -25,7 +25,7 @@ module.exports = {
|
|
|
25
25
|
functionQuery: {
|
|
26
26
|
className: 'Completions',
|
|
27
27
|
methodName: 'create',
|
|
28
|
-
kind: '
|
|
28
|
+
kind: 'Sync'
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
]
|
|
@@ -39,7 +39,7 @@ module.exports = {
|
|
|
39
39
|
functionQuery: {
|
|
40
40
|
className: 'Responses',
|
|
41
41
|
methodName: 'create',
|
|
42
|
-
kind: '
|
|
42
|
+
kind: 'Sync'
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
]
|