newrelic 13.9.1 → 13.10.0
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 +57 -11
- package/lib/llm-events/openai/chat-completion-message.js +6 -1
- package/lib/metrics/names.js +2 -1
- package/lib/otel/setup.js +14 -10
- package/lib/shimmer.js +1 -1
- package/lib/subscribers/base.js +32 -14
- package/lib/subscribers/dc-base.js +47 -6
- package/lib/subscribers/fastify/index.js +5 -2
- package/lib/subscribers/message-consumer.js +2 -1
- package/lib/subscribers/openai/chat.js +2 -5
- package/lib/subscribers/openai/client.js +10 -4
- package/lib/subscribers/openai/embeddings.js +2 -5
- package/lib/subscribers/openai/utils.js +21 -8
- package/lib/subscribers/record-supportability-metric.js +39 -0
- package/lib/subscribers/resolve-package-version.js +84 -0
- package/lib/tracking-packages.js +1 -9
- package/lib/transaction/trace/partial-trace.js +26 -0
- package/package.json +2 -1
package/NEWS.md
CHANGED
|
@@ -1,14 +1,58 @@
|
|
|
1
|
-
### v13.
|
|
2
|
-
|
|
3
|
-
####
|
|
4
|
-
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
####
|
|
9
|
-
|
|
10
|
-
* Updated
|
|
11
|
-
|
|
1
|
+
### v13.10.0 (2026-01-20)
|
|
2
|
+
|
|
3
|
+
#### Features
|
|
4
|
+
|
|
5
|
+
* Add `timestamp` to OpenAI input `LlmChatCompletionMessage`s ([#3657](https://github.com/newrelic/node-newrelic/pull/3657)) ([099e20e](https://github.com/newrelic/node-newrelic/commit/099e20eaa380615b7e8aae7eaa12e2754a931041))
|
|
6
|
+
* Added reduced tracing mode for span links ([#3649](https://github.com/newrelic/node-newrelic/pull/3649)) ([2414331](https://github.com/newrelic/node-newrelic/commit/24143313e2d894a5e11e42116f33ef4be18f2fb4))
|
|
7
|
+
|
|
8
|
+
#### Bug fixes
|
|
9
|
+
|
|
10
|
+
* Updated OpenAI instrumentation to properly parse headers when a call fails ([#3665](https://github.com/newrelic/node-newrelic/pull/3665)) ([641b7a8](https://github.com/newrelic/node-newrelic/commit/641b7a8c8015855d82574e8173cb6e9fd082b4a0))
|
|
11
|
+
|
|
12
|
+
#### Documentation
|
|
13
|
+
|
|
14
|
+
* Updated compatibility report ([#3669](https://github.com/newrelic/node-newrelic/pull/3669)) ([ec922ae](https://github.com/newrelic/node-newrelic/commit/ec922ae6b0cecfa4ecbce86a789c308d6876ecd7)) ([#3668](https://github.com/newrelic/node-newrelic/pull/3668)) ([85c601f](https://github.com/newrelic/node-newrelic/commit/85c601fe2eaaacd82c0abac81eba96ce6718f2f1)) ([#3663](https://github.com/newrelic/node-newrelic/pull/3663)) ([82b3ac8](https://github.com/newrelic/node-newrelic/commit/82b3ac8cf6f69d0609b7fbaaf8be2a35f6552485))
|
|
15
|
+
|
|
16
|
+
#### Miscellaneous chores
|
|
17
|
+
|
|
18
|
+
* Lazy load OTEL dependencies ([#3667](https://github.com/newrelic/node-newrelic/pull/3667)) ([0b0c764](https://github.com/newrelic/node-newrelic/commit/0b0c7647f3f0bc3093b9ffbf7596c1654ba6113d))
|
|
19
|
+
* Record subscriber usage metric ([#3626](https://github.com/newrelic/node-newrelic/pull/3626)) ([5796574](https://github.com/newrelic/node-newrelic/commit/57965740acd053e6c99a88d861a876bcf2601c07))
|
|
20
|
+
* Update `ai-support.json` ([#3666](https://github.com/newrelic/node-newrelic/pull/3666)) ([562b403](https://github.com/newrelic/node-newrelic/commit/562b403a3414430cdd0afab261d3d119d3d9b4db))
|
|
21
|
+
* Updated OpenAI instrumentation to skip creating a `LlmChatCompletionMessage` for an outgoing tool call response ([#3655](https://github.com/newrelic/node-newrelic/pull/3655)) ([572c3a1](https://github.com/newrelic/node-newrelic/commit/572c3a1741ebd13d77d857c262b050aeb0c801fc))
|
|
22
|
+
|
|
23
|
+
#### Tests
|
|
24
|
+
|
|
25
|
+
* Updated mock response to avoid retries on OpenAI LangChain vectorstore calls ([#3664](https://github.com/newrelic/node-newrelic/pull/3664)) ([bc1faf3](https://github.com/newrelic/node-newrelic/commit/bc1faf30654d920e31ad4ddc205560e56b6c796c))
|
|
26
|
+
|
|
27
|
+
### v13.9.2 (2026-01-14)
|
|
28
|
+
|
|
29
|
+
#### Bug fixes
|
|
30
|
+
|
|
31
|
+
* Updated message consumer subscribers to properly time the consumption actions ([#3660](https://github.com/newrelic/node-newrelic/pull/3660)) ([ef1b611](https://github.com/newrelic/node-newrelic/commit/ef1b611a7c3b45f5fc8d483661859c1595bbe4ab))
|
|
32
|
+
|
|
33
|
+
#### Documentation
|
|
34
|
+
|
|
35
|
+
* Updated compatibility report ([#3654](https://github.com/newrelic/node-newrelic/pull/3654)) ([ca16dae](https://github.com/newrelic/node-newrelic/commit/ca16daefe4da1c17253e165c68d198d607c72d0b))
|
|
36
|
+
|
|
37
|
+
#### Miscellaneous chores
|
|
38
|
+
|
|
39
|
+
* Update OTEL metrics test ([#3661](https://github.com/newrelic/node-newrelic/pull/3661)) ([ae84b62](https://github.com/newrelic/node-newrelic/commit/ae84b62a3c7ef753d4ace8836f38405b4cbbfd88))
|
|
40
|
+
|
|
41
|
+
#### Tests
|
|
42
|
+
|
|
43
|
+
* Added cross agent tests for sampler configuration and sampling rate scenarios ([#3648](https://github.com/newrelic/node-newrelic/pull/3648)) ([fa0e2d1](https://github.com/newrelic/node-newrelic/commit/fa0e2d1bd78051c7f716ebc65bd7f3842698103e))
|
|
44
|
+
|
|
45
|
+
### v13.9.1 (2026-01-12)
|
|
46
|
+
|
|
47
|
+
#### Bug fixes
|
|
48
|
+
|
|
49
|
+
* Updated `getHostnameSafe` to invalidate the cache is trying to assign the host based on the gcp cloud run id ([#3650](https://github.com/newrelic/node-newrelic/pull/3650)) ([d395c76](https://github.com/newrelic/node-newrelic/commit/d395c76d5612a7e562efa1a807c35e805a38ab14))
|
|
50
|
+
* Updated logic to properly assign content and role in LangChain chat completion messages ([#3638](https://github.com/newrelic/node-newrelic/pull/3638)) ([407bcb6](https://github.com/newrelic/node-newrelic/commit/407bcb66b7e3be1b6f1d28bcf6e3b022a7022c96))
|
|
51
|
+
|
|
52
|
+
#### Documentation
|
|
53
|
+
|
|
54
|
+
* Updated compatibility report ([#3643](https://github.com/newrelic/node-newrelic/pull/3643)) ([8929ab3](https://github.com/newrelic/node-newrelic/commit/8929ab3ae6cc28edbef4bee41a2084154361d6e0))
|
|
55
|
+
|
|
12
56
|
### v13.9.0 (2026-01-08)
|
|
13
57
|
|
|
14
58
|
#### Features
|
|
@@ -8214,3 +8258,5 @@ Special thanks to Ryan Copley (@RyanCopley) for the contribution.
|
|
|
8214
8258
|
|
|
8215
8259
|
|
|
8216
8260
|
|
|
8261
|
+
|
|
8262
|
+
|
|
@@ -26,13 +26,18 @@ module.exports = class LlmChatCompletionMessage extends LlmEvent {
|
|
|
26
26
|
// Check if the given message is from the response.
|
|
27
27
|
// The response object differs based on the API called.
|
|
28
28
|
// If it's `responses.create`, we check against `response.output`.
|
|
29
|
-
// If it's `chat.completions.create
|
|
29
|
+
// If it's `chat.completions.create`, we check against `response.choices`.
|
|
30
30
|
if (response?.object === 'response') {
|
|
31
31
|
this.is_response = message.content === response?.output?.[0]?.content?.[0]?.text
|
|
32
32
|
} else {
|
|
33
33
|
this.is_response = message.content === response?.choices?.[0]?.message?.content
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Spec 771: `timestamp` is only required for input/request messages.
|
|
37
|
+
if (this.is_response === false) {
|
|
38
|
+
this.timestamp = segment.timer.start
|
|
39
|
+
}
|
|
40
|
+
|
|
36
41
|
// Assign content to the event.
|
|
37
42
|
const content = message?.content
|
|
38
43
|
if (agent.config.ai_monitoring.record_content.enabled === true) {
|
package/lib/metrics/names.js
CHANGED
|
@@ -353,7 +353,8 @@ const FEATURES = {
|
|
|
353
353
|
SOURCE_MAPS: `${SUPPORTABILITY.FEATURES}/EnableSourceMaps`,
|
|
354
354
|
CERTIFICATES: SUPPORTABILITY.FEATURES + '/Certificates',
|
|
355
355
|
INSTRUMENTATION: {
|
|
356
|
-
ON_REQUIRE: SUPPORTABILITY.FEATURES + '/Instrumentation/OnRequire'
|
|
356
|
+
ON_REQUIRE: SUPPORTABILITY.FEATURES + '/Instrumentation/OnRequire',
|
|
357
|
+
SUBSCRIBER_USED: SUPPORTABILITY.FEATURES + '/Instrumentation/SubscriberUsed'
|
|
357
358
|
}
|
|
358
359
|
}
|
|
359
360
|
|
package/lib/otel/setup.js
CHANGED
|
@@ -5,16 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict'
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const SetupTraces = require('./traces/index.js')
|
|
13
|
-
const ContextManager = require('./context-manager')
|
|
14
|
-
const TracePropagator = require('./trace-propagator')
|
|
8
|
+
// The majority of dependencies in this module are lazy loaded in order to
|
|
9
|
+
// limit the impact on memory usage. In general, the OTEL dependencies are
|
|
10
|
+
// only loaded when they are needed, because they can have a significant
|
|
11
|
+
// impact once loaded.
|
|
15
12
|
const defaultLogger = require('../logger').child({ component: 'opentelemetry-bridge' })
|
|
16
|
-
const createOtelLogger = require('./logger')
|
|
17
|
-
const interceptSpanKey = require('./span-key-interceptor')
|
|
18
13
|
|
|
19
14
|
const signals = []
|
|
20
15
|
|
|
@@ -26,6 +21,12 @@ function setupOtel(agent, logger = defaultLogger) {
|
|
|
26
21
|
return
|
|
27
22
|
}
|
|
28
23
|
|
|
24
|
+
const opentelemetry = require('@opentelemetry/api')
|
|
25
|
+
const ContextManager = require('./context-manager')
|
|
26
|
+
const TracePropagator = require('./trace-propagator')
|
|
27
|
+
const createOtelLogger = require('./logger')
|
|
28
|
+
const interceptSpanKey = require('./span-key-interceptor')
|
|
29
|
+
|
|
29
30
|
// When bridge mode is enabled, our context manager must utilize the OTEL
|
|
30
31
|
// context manager. Otherwise, traces within, e.g. logs, will not be
|
|
31
32
|
// captured correctly. The internal OTEL logger also must be configured
|
|
@@ -37,6 +38,7 @@ function setupOtel(agent, logger = defaultLogger) {
|
|
|
37
38
|
opentelemetry.propagation.setGlobalPropagator(new TracePropagator(agent))
|
|
38
39
|
|
|
39
40
|
if (agent.config.opentelemetry.traces.enabled === true) {
|
|
41
|
+
const SetupTraces = require('./traces/index.js')
|
|
40
42
|
const signal = new SetupTraces({ agent })
|
|
41
43
|
signals.push(signal)
|
|
42
44
|
} else {
|
|
@@ -47,6 +49,7 @@ function setupOtel(agent, logger = defaultLogger) {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
if (agent.config.opentelemetry.metrics.enabled === true) {
|
|
52
|
+
const SetupMetrics = require('./metrics/index.js')
|
|
50
53
|
const signal = new SetupMetrics({ agent })
|
|
51
54
|
signals.push(signal)
|
|
52
55
|
} else {
|
|
@@ -57,6 +60,7 @@ function setupOtel(agent, logger = defaultLogger) {
|
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
if (agent.config.opentelemetry.logs.enabled === true) {
|
|
63
|
+
const SetupLogs = require('./logs/index.js')
|
|
60
64
|
const signal = new SetupLogs({ agent })
|
|
61
65
|
signals.push(signal)
|
|
62
66
|
} else {
|
|
@@ -68,7 +72,7 @@ function setupOtel(agent, logger = defaultLogger) {
|
|
|
68
72
|
.incrementCallCount()
|
|
69
73
|
}
|
|
70
74
|
|
|
71
|
-
function teardownOtel(agent) {
|
|
75
|
+
function teardownOtel(agent, { opentelemetry = require('@opentelemetry/api') } = {}) {
|
|
72
76
|
if (agent?.config?.opentelemetry?.enabled !== true) {
|
|
73
77
|
return
|
|
74
78
|
}
|
package/lib/shimmer.js
CHANGED
|
@@ -306,7 +306,7 @@ const shimmer = (module.exports = {
|
|
|
306
306
|
registerHooks(agent) {
|
|
307
307
|
// add the packages from the subscriber based instrumentation
|
|
308
308
|
// this is only added to add tracking metrics
|
|
309
|
-
|
|
309
|
+
Array.prototype.push.apply(pkgsToHook, trackingPkgs)
|
|
310
310
|
this._ritm = new Hook(pkgsToHook, function onHook(exports, name, basedir) {
|
|
311
311
|
return _postLoad(agent, exports, name, basedir)
|
|
312
312
|
})
|
package/lib/subscribers/base.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
8
8
|
const { tracingChannel } = require('node:diagnostics_channel')
|
|
9
9
|
const cat = require('#agentlib/util/cat.js')
|
|
10
|
+
const recordSupportabilityMetric = require('./record-supportability-metric.js')
|
|
11
|
+
|
|
10
12
|
// Used for the `traceCallback` work.
|
|
11
13
|
// This can be removed when we add true support into orchestrion
|
|
12
14
|
const makeCall = (fn) => (...args) => fn.call(...args)
|
|
@@ -28,38 +30,45 @@ const ArrayPrototypeSplice = makeCall(Array.prototype.splice)
|
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* @property {object} agent A New Relic Node.js agent instance.
|
|
31
|
-
* @property {
|
|
33
|
+
* @property {TracingChannel} channel The tracing channel instance this subscriber will be monitoring.
|
|
34
|
+
* @property {string} channelName A unique name for the diagnostics channel
|
|
35
|
+
* that will be registered.
|
|
32
36
|
* @property {object} config The agent configuration object.
|
|
37
|
+
* @property {string} id A unique identifier for the subscriber, combining the prefix, package
|
|
38
|
+
* name, and channel name.
|
|
39
|
+
* @property {object} logger An agent logger instance.
|
|
33
40
|
* @property {string} packageName The name of the module being instrumented.
|
|
34
41
|
* This is the same string one would pass to the `require` function.
|
|
35
|
-
* @property {
|
|
36
|
-
*
|
|
42
|
+
* @property {AsyncLocalStorage} store The async local storage instance used for context management.
|
|
43
|
+
* @property {number} [callback=null] Position of callback if it needs to be wrapped for instrumentation.
|
|
44
|
+
* -1 means last argument.
|
|
37
45
|
* @property {string[]} [events=[]] Set of tracing channel event names to
|
|
38
46
|
* register handlers for. For any name in the set, a corresponding method
|
|
39
47
|
* must exist on the subscriber instance. The method will be passed the
|
|
40
48
|
* event object. Possible event names are `start`, `end`, `asyncStart`,
|
|
41
49
|
* `asyncEnd`, and `error`.
|
|
42
|
-
*
|
|
43
|
-
* See {@link https://nodejs.org/api/diagnostics_channel.html#class-tracingchannel}
|
|
44
|
-
* @property {boolean} [opaque=false] If true, any children segments will not be created.
|
|
50
|
+
* See {@link https://nodejs.org/api/diagnostics_channel.html#class-tracingchannel}
|
|
45
51
|
* @property {boolean} [internal=false] If true, any children segments from the same library
|
|
46
52
|
* will not be created.
|
|
53
|
+
* @property {boolean} [opaque=false] If true, any children segments will not be created.
|
|
47
54
|
* @property {string} [prefix='orchestrion:'] String to prepend to diagnostics
|
|
48
55
|
* channel event names. This provides a namespace for the events we are
|
|
49
56
|
* injecting into a module.
|
|
50
|
-
* @property {boolean} [requireActiveTx=true] If true, the subscriber will only handle events
|
|
51
|
-
* when there is an active transaction.
|
|
52
57
|
* @property {boolean} [propagateContext=false] If true, it will bind `asyncStart` to the store
|
|
53
58
|
* and re-propagate the active context. It will also attach the `transaction` to the event in
|
|
54
59
|
* `start.bindStore`. This is used for functions that queue async code and context is lost.
|
|
55
|
-
* @property {
|
|
56
|
-
*
|
|
57
|
-
* @property {
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
60
|
+
* @property {boolean} [requireActiveTx=true] If true, the subscriber will only handle events
|
|
61
|
+
* when there is an active transaction.
|
|
62
|
+
* @property {object} [targetModuleMeta] Defines the target module's name and
|
|
63
|
+
* version string, i.e. is an object `{ name, version }`. This is only necessary
|
|
64
|
+
* when target instrumentation can surface an unexpected name for the
|
|
65
|
+
* `packageName` property. For example, `express` uses multiple modules to
|
|
66
|
+
* compose its core functionality. We want to track things under the `express`
|
|
67
|
+
* name, but `packageName` will be set to `router` is most cases.
|
|
61
68
|
*/
|
|
62
69
|
class Subscriber {
|
|
70
|
+
#usageMetricRecorded = false
|
|
71
|
+
|
|
63
72
|
/**
|
|
64
73
|
* @param {SubscriberParams} params the subscriber constructor params
|
|
65
74
|
*/
|
|
@@ -234,6 +243,15 @@ class Subscriber {
|
|
|
234
243
|
* @returns {Context} The context after processing the event
|
|
235
244
|
*/
|
|
236
245
|
const handler = (data) => {
|
|
246
|
+
if (this.#usageMetricRecorded === false) {
|
|
247
|
+
recordSupportabilityMetric({
|
|
248
|
+
agent: this.agent,
|
|
249
|
+
moduleName: this.packageName,
|
|
250
|
+
moduleVersion: data.moduleVersion
|
|
251
|
+
})
|
|
252
|
+
this.#usageMetricRecorded = true
|
|
253
|
+
}
|
|
254
|
+
|
|
237
255
|
// only wrap the callback if a subscriber has a callback property defined
|
|
238
256
|
if (this.callback !== null) {
|
|
239
257
|
this.traceCallback(this.callback, data)
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
'use strict'
|
|
7
|
+
|
|
7
8
|
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
8
9
|
const dc = require('node:diagnostics_channel')
|
|
10
|
+
const recordSupportabilityMetric = require('./record-supportability-metric.js')
|
|
11
|
+
const resolvePackageVersion = require('./resolve-package-version.js')
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* The baseline parameters available to all subscribers.
|
|
@@ -15,6 +18,13 @@ const dc = require('node:diagnostics_channel')
|
|
|
15
18
|
* @property {object} logger An agent logger instance.
|
|
16
19
|
* @property {string} packageName The package name being instrumented.
|
|
17
20
|
* This is what a developer would provide to the `require` function.
|
|
21
|
+
* @property {boolean} [skipUsageMetricRecording=false] When set to `true`, the
|
|
22
|
+
* instrumentation will not attempt to record the usage metric. This is useful
|
|
23
|
+
* when the module being instrumented is also being instrumented via the
|
|
24
|
+
* Orchestrion based subscriber system. It is much cheaper to record the metric
|
|
25
|
+
* via Orchestrion based subscribers than through this direct diagnostics
|
|
26
|
+
* channel method (Orchestrion provides the module version, whereas we have
|
|
27
|
+
* to perform expensive operations here to get the same information).
|
|
18
28
|
*/
|
|
19
29
|
|
|
20
30
|
/**
|
|
@@ -34,19 +44,25 @@ const dc = require('node:diagnostics_channel')
|
|
|
34
44
|
* This is the same string one would pass to the `require` function.
|
|
35
45
|
*/
|
|
36
46
|
class Subscriber {
|
|
47
|
+
#usageMetricRecorded = false
|
|
48
|
+
|
|
37
49
|
/**
|
|
38
50
|
* @param {SubscriberParams} params to function
|
|
39
51
|
*/
|
|
40
|
-
constructor({ agent, logger, packageName }) {
|
|
52
|
+
constructor({ agent, logger, packageName, skipUsageMetricRecording = false }) {
|
|
41
53
|
this.agent = agent
|
|
42
54
|
this.logger = logger.child({ component: `${packageName}-subscriber` })
|
|
43
55
|
this.config = agent.config
|
|
44
56
|
this.id = packageName
|
|
57
|
+
|
|
58
|
+
if (skipUsageMetricRecording === true) {
|
|
59
|
+
this.#usageMetricRecorded = true
|
|
60
|
+
}
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
set channels(channels) {
|
|
48
64
|
if (!Array.isArray(channels)) {
|
|
49
|
-
throw new Error('channels must be a collection of with
|
|
65
|
+
throw new Error('channels must be a collection of objects with properties channel and hook')
|
|
50
66
|
}
|
|
51
67
|
this._channels = channels
|
|
52
68
|
}
|
|
@@ -73,18 +89,43 @@ class Subscriber {
|
|
|
73
89
|
|
|
74
90
|
subscribe() {
|
|
75
91
|
for (let index = 0; index < this.channels.length; index++) {
|
|
92
|
+
const chan = this.channels[index]
|
|
76
93
|
const { hook, channel } = this.channels[index]
|
|
77
94
|
const boundHook = hook.bind(this)
|
|
78
|
-
|
|
79
|
-
|
|
95
|
+
chan.boundHook = boundHook
|
|
96
|
+
chan.eventHandler = (message, name) => {
|
|
97
|
+
this.#supportability()
|
|
98
|
+
boundHook(message, name)
|
|
99
|
+
}
|
|
100
|
+
dc.subscribe(channel, chan.eventHandler)
|
|
80
101
|
}
|
|
81
102
|
}
|
|
82
103
|
|
|
83
104
|
unsubscribe() {
|
|
84
105
|
for (let index = 0; index < this.channels.length; index++) {
|
|
85
|
-
const { channel,
|
|
86
|
-
dc.unsubscribe(channel,
|
|
106
|
+
const { channel, eventHandler } = this.channels[index]
|
|
107
|
+
dc.unsubscribe(channel, eventHandler)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Since this class subscribes to diagnostics channels natively published by
|
|
113
|
+
* target modules, we do not get the package metadata that Orchestrion
|
|
114
|
+
* provides in its channel events. So we have to try and find the package
|
|
115
|
+
* manifest and get the version out of it in order to record our
|
|
116
|
+
* supportability metric.
|
|
117
|
+
*/
|
|
118
|
+
#supportability() {
|
|
119
|
+
if (this.#usageMetricRecorded === true) {
|
|
120
|
+
return
|
|
87
121
|
}
|
|
122
|
+
const version = resolvePackageVersion(this.id)
|
|
123
|
+
recordSupportabilityMetric({
|
|
124
|
+
agent: this.agent,
|
|
125
|
+
moduleName: this.id,
|
|
126
|
+
moduleVersion: version
|
|
127
|
+
})
|
|
128
|
+
this.#usageMetricRecorded = true
|
|
88
129
|
}
|
|
89
130
|
}
|
|
90
131
|
|
|
@@ -10,7 +10,7 @@ const MiddlewareWrapper = require('../middleware-wrapper')
|
|
|
10
10
|
|
|
11
11
|
class FastifyInitialization extends DcBase {
|
|
12
12
|
constructor({ agent, logger }) {
|
|
13
|
-
super({ agent, logger, packageName: 'fastify' })
|
|
13
|
+
super({ agent, logger, packageName: 'fastify', skipUsageMetricRecording: true })
|
|
14
14
|
this.channels = [
|
|
15
15
|
{ channel: initChannel, hook: this.handler }
|
|
16
16
|
]
|
|
@@ -24,7 +24,10 @@ class FastifyInitialization extends DcBase {
|
|
|
24
24
|
return
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
routeOptions.handler = self.wrapper.wrap({
|
|
27
|
+
routeOptions.handler = self.wrapper.wrap({
|
|
28
|
+
handler: routeOptions.handler,
|
|
29
|
+
route: routeOptions.path
|
|
30
|
+
})
|
|
28
31
|
})
|
|
29
32
|
}
|
|
30
33
|
}
|
|
@@ -105,7 +105,7 @@ class MessageConsumerSubscriber extends Subscriber {
|
|
|
105
105
|
*/
|
|
106
106
|
asyncStart() {
|
|
107
107
|
const ctx = this.agent.tracer.getContext()
|
|
108
|
-
const tx = ctx
|
|
108
|
+
const tx = ctx.transaction
|
|
109
109
|
tx.setPartialName(this.name)
|
|
110
110
|
// Note: this is not using `Subscriber.createSegment`
|
|
111
111
|
// this is because it enters the segment and returns a new ctx
|
|
@@ -117,6 +117,7 @@ class MessageConsumerSubscriber extends Subscriber {
|
|
|
117
117
|
parent: tx.trace.root,
|
|
118
118
|
transaction: tx
|
|
119
119
|
})
|
|
120
|
+
tx.baseSegment.start()
|
|
120
121
|
|
|
121
122
|
this.addConsumeParameters(tx)
|
|
122
123
|
|
|
@@ -37,13 +37,10 @@ class OpenAIChatCompletions extends OpenAISubscriber {
|
|
|
37
37
|
return
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
return this.createSegment({
|
|
41
41
|
name: OPENAI.COMPLETION,
|
|
42
|
-
|
|
43
|
-
transaction: ctx.transaction
|
|
42
|
+
ctx
|
|
44
43
|
})
|
|
45
|
-
const newCtx = ctx.enterSegment({ segment })
|
|
46
|
-
return newCtx
|
|
47
44
|
}
|
|
48
45
|
|
|
49
46
|
asyncEnd(data) {
|
|
@@ -17,12 +17,18 @@ class OpenAIClientSubscriber extends OpenAISubscriber {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
asyncEnd(data) {
|
|
20
|
-
const { result } = data
|
|
20
|
+
const { result, error } = data
|
|
21
21
|
const ctx = this.agent.tracer.getContext()
|
|
22
22
|
if (ctx?.segment) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const responseHeaders = result?.response?.headers
|
|
24
|
+
let headers = {}
|
|
25
|
+
if (responseHeaders) {
|
|
26
|
+
headers = Object.fromEntries(responseHeaders)
|
|
27
|
+
} else if (error?.headers) {
|
|
28
|
+
// In openai v5 they made error headers consistent with response headers
|
|
29
|
+
// they are now some custom class that can retrieve headers via `Object.fromEntries`
|
|
30
|
+
headers = error?.headers?.values ? Object.fromEntries(error.headers) : { ...error.headers }
|
|
31
|
+
}
|
|
26
32
|
ctx.extras = { headers }
|
|
27
33
|
}
|
|
28
34
|
}
|
|
@@ -22,13 +22,10 @@ class OpenAIEmbeddings extends OpenAISubscriber {
|
|
|
22
22
|
this.logger.debug('OpenAI instrumentation is disabled, not creating segment.')
|
|
23
23
|
return
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
return this.createSegment({
|
|
26
26
|
name: OPENAI.EMBEDDING,
|
|
27
|
-
|
|
28
|
-
transaction: ctx.transaction
|
|
27
|
+
ctx
|
|
29
28
|
})
|
|
30
|
-
const newCtx = ctx.enterSegment({ segment })
|
|
31
|
-
return newCtx
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
asyncEnd(data) {
|
|
@@ -21,7 +21,8 @@ let TRACKING_METRIC = OPENAI.TRACKING_PREFIX
|
|
|
21
21
|
* Parses the response from OpenAI and extracts the message content and role.
|
|
22
22
|
*
|
|
23
23
|
* @param {object} response The OpenAI SDK response object
|
|
24
|
-
* @returns {
|
|
24
|
+
* @returns {object[]} an array with the message object with fields `content`
|
|
25
|
+
* and `role` or [] if response invalid
|
|
25
26
|
*/
|
|
26
27
|
function getMessageFromResponse(response) {
|
|
27
28
|
let content
|
|
@@ -30,11 +31,17 @@ function getMessageFromResponse(response) {
|
|
|
30
31
|
content = response?.output?.[0]?.content?.[0]?.text
|
|
31
32
|
role = response?.output?.[0]?.role
|
|
32
33
|
} else {
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
const choice = response?.choices?.[0]
|
|
35
|
+
if (choice?.finish_reason === 'tool_calls') {
|
|
36
|
+
// A false response. Don't create a LlmChatCompletionMessage
|
|
37
|
+
// for this -- the full conversation isn't done yet.
|
|
38
|
+
return []
|
|
39
|
+
}
|
|
40
|
+
content = choice?.message?.content
|
|
41
|
+
role = choice?.message?.role
|
|
35
42
|
}
|
|
36
43
|
|
|
37
|
-
return { content, role }
|
|
44
|
+
return [{ content, role }]
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
/**
|
|
@@ -73,7 +80,10 @@ function getMessagesFromRequest(request, logger) {
|
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
/**
|
|
76
|
-
* Enqueues a LLM event to the custom event aggregator
|
|
83
|
+
* Enqueues a LLM event to the custom event aggregator.
|
|
84
|
+
*
|
|
85
|
+
* Will use `msg.timestamp` instead of the current time
|
|
86
|
+
* for `timestamp` if present.
|
|
77
87
|
*
|
|
78
88
|
* @param {object} params input params
|
|
79
89
|
* @param {Agent} params.agent NR agent instance
|
|
@@ -82,9 +92,12 @@ function getMessagesFromRequest(request, logger) {
|
|
|
82
92
|
*/
|
|
83
93
|
function recordEvent({ agent, type, msg }) {
|
|
84
94
|
const llmContext = extractLlmContext(agent)
|
|
85
|
-
|
|
95
|
+
// Spec 771: The `timestamp` attribute MUST be used to override
|
|
96
|
+
// the timestamp value attached to the intrinsics of the custom
|
|
97
|
+
// LlmChatCompletionMessage event before it is recorded using
|
|
98
|
+
// record_custom_event().
|
|
86
99
|
agent.customEventAggregator.add([
|
|
87
|
-
{ type, timestamp: Date.now() },
|
|
100
|
+
{ type, timestamp: msg?.timestamp ?? Date.now() },
|
|
88
101
|
Object.assign({}, msg, llmContext)
|
|
89
102
|
])
|
|
90
103
|
}
|
|
@@ -159,7 +172,7 @@ function recordChatCompletionMessages({
|
|
|
159
172
|
// the response message.
|
|
160
173
|
const messages = [
|
|
161
174
|
...getMessagesFromRequest(request, logger),
|
|
162
|
-
getMessageFromResponse(response)
|
|
175
|
+
...getMessageFromResponse(response)
|
|
163
176
|
]
|
|
164
177
|
|
|
165
178
|
for (let i = 0; i < messages.length; i++) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
module.exports = recordSupportabilityMetric
|
|
9
|
+
|
|
10
|
+
const semver = require('semver')
|
|
11
|
+
const {
|
|
12
|
+
FEATURES: {
|
|
13
|
+
INSTRUMENTATION: { SUBSCRIBER_USED }
|
|
14
|
+
}
|
|
15
|
+
} = require('#agentlib/metrics/names.js')
|
|
16
|
+
|
|
17
|
+
function recordSupportabilityMetric({
|
|
18
|
+
agent,
|
|
19
|
+
moduleName,
|
|
20
|
+
moduleVersion = 'unknown'
|
|
21
|
+
} = {}) {
|
|
22
|
+
const major = moduleVersion === 'unknown'
|
|
23
|
+
? semver.major(process.version)
|
|
24
|
+
: semver.major(moduleVersion)
|
|
25
|
+
|
|
26
|
+
let metric = agent.metrics.getOrCreateMetric(
|
|
27
|
+
`${SUBSCRIBER_USED}/${moduleName}/${major}`
|
|
28
|
+
)
|
|
29
|
+
if (metric.callCount === 0) {
|
|
30
|
+
metric.incrementCallCount()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
metric = agent.metrics.getOrCreateMetric(
|
|
34
|
+
`${SUBSCRIBER_USED}/${moduleName}`
|
|
35
|
+
)
|
|
36
|
+
if (metric.callCount === 0) {
|
|
37
|
+
metric.incrementCallCount()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const path = require('node:path')
|
|
9
|
+
const defaultLogger = require('#agentlib/logger.js').child({
|
|
10
|
+
component: 'resolve-module-version'
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const dcFuncFrame = /^\s*at Channel\.publish/
|
|
14
|
+
const modPathReg = /at .+ \((.+):\d+:\d+\)/
|
|
15
|
+
|
|
16
|
+
module.exports = resolveModuleVersion
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Given a module name, attempt to read the version string from its
|
|
20
|
+
* associated package manifest. If the module is a built-in, or one that has
|
|
21
|
+
* been bundled with Node.js (e.g. `undici`), a package manifest will not be
|
|
22
|
+
* available. In this case, the string "unknown" will be returned.
|
|
23
|
+
*
|
|
24
|
+
* This version resolver assumes that it will be invoked through our
|
|
25
|
+
* diagnostics channel subscriber instrumentations. That is, it expects the
|
|
26
|
+
* call tree to be similar to:
|
|
27
|
+
*
|
|
28
|
+
* 1. some-module.function()
|
|
29
|
+
* 2. diagnostics_channel.publish()
|
|
30
|
+
* 3. subscriber.handler()
|
|
31
|
+
*
|
|
32
|
+
* @param {string} moduleSpecifier What would be passed to `resolve()`.
|
|
33
|
+
* @param {object} [deps] Optional dependencies.
|
|
34
|
+
* @param {object} [deps.logger] Agent logger instance.
|
|
35
|
+
*
|
|
36
|
+
* @returns {string} The version string from the package manifest or "unknown".
|
|
37
|
+
*/
|
|
38
|
+
function resolveModuleVersion(moduleSpecifier, { logger = defaultLogger } = {}) {
|
|
39
|
+
let pkgPath
|
|
40
|
+
// We'd prefer to use `require.resolve(moduleSpecifier)` here, but it gets
|
|
41
|
+
// a bit confused when there are non-standard module directories in play.
|
|
42
|
+
// Once we are able to refactor our "on require" metric recording to
|
|
43
|
+
// utilize `module.registerHooks`, we should be able to eliminate this
|
|
44
|
+
// slow algorithm.
|
|
45
|
+
const err = Error()
|
|
46
|
+
const stack = err.stack.split('\n')
|
|
47
|
+
do {
|
|
48
|
+
stack.shift()
|
|
49
|
+
} while (dcFuncFrame.test(stack[0]) === false && stack.length > 0)
|
|
50
|
+
const matches = modPathReg.exec(stack[1])
|
|
51
|
+
pkgPath = matches?.[1]
|
|
52
|
+
|
|
53
|
+
if (!pkgPath) {
|
|
54
|
+
logger.warn(
|
|
55
|
+
{ moduleSpecifier },
|
|
56
|
+
'Could not resolve module path. Possibly a built-in or Node.js bundled module.'
|
|
57
|
+
)
|
|
58
|
+
return 'unknown'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const cwd = process.cwd()
|
|
62
|
+
let reachedCwd = false
|
|
63
|
+
let pkg
|
|
64
|
+
let base = path.dirname(pkgPath)
|
|
65
|
+
do {
|
|
66
|
+
try {
|
|
67
|
+
pkgPath = path.join(base, 'package.json')
|
|
68
|
+
pkg = require(pkgPath)
|
|
69
|
+
} catch {
|
|
70
|
+
base = path.resolve(path.join(base, '..'))
|
|
71
|
+
if (base === cwd) {
|
|
72
|
+
reachedCwd = true
|
|
73
|
+
} else if (reachedCwd === true) {
|
|
74
|
+
// We reached the supposed app root, attempted to load a manifest
|
|
75
|
+
// file in that location, and still couldn't find one. So we give up.
|
|
76
|
+
pkg = {}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} while (!pkg)
|
|
80
|
+
|
|
81
|
+
const version = pkg.version ?? 'unknown'
|
|
82
|
+
logger.trace({ moduleSpecifier, version }, 'Resolved package version.')
|
|
83
|
+
return version
|
|
84
|
+
}
|
package/lib/tracking-packages.js
CHANGED
|
@@ -19,15 +19,7 @@ const trackingPkgs = [
|
|
|
19
19
|
'fancy-log',
|
|
20
20
|
'knex',
|
|
21
21
|
'loglevel',
|
|
22
|
-
'npmlog'
|
|
23
|
-
// These packages are required for the tracking metrics
|
|
24
|
-
// for @langchain/core to be created. Ideally, these
|
|
25
|
-
// should not be here.
|
|
26
|
-
// TODO: will be addressed in https://github.com/newrelic/node-newrelic/issues/3575
|
|
27
|
-
'@langchain/core/prompts',
|
|
28
|
-
'@langchain/core/tools',
|
|
29
|
-
'@langchain/core/runnables',
|
|
30
|
-
'@langchain/core/vectorstores',
|
|
22
|
+
'npmlog'
|
|
31
23
|
]
|
|
32
24
|
|
|
33
25
|
module.exports = trackingPkgs
|
|
@@ -65,6 +65,7 @@ class PartialTrace {
|
|
|
65
65
|
addSpan({ span, isEntry }) {
|
|
66
66
|
const id = span.id
|
|
67
67
|
const parentId = span.parentId
|
|
68
|
+
const spanLinks = span.spanLinks
|
|
68
69
|
span = span.applyPartialTraceRules({ isEntry, partialTrace: this })
|
|
69
70
|
this.createMetrics(!!span)
|
|
70
71
|
if (span) {
|
|
@@ -76,7 +77,32 @@ class PartialTrace {
|
|
|
76
77
|
// unless compact where all parentIds are assigned to the entry span
|
|
77
78
|
// in finalizeSpanEvents
|
|
78
79
|
this.droppedSpans.set(id, parentId)
|
|
80
|
+
|
|
81
|
+
// span was dropped but we still need to move its span links to the last kept span
|
|
82
|
+
// spanLinks were captured before the span was dropped
|
|
83
|
+
if (spanLinks && spanLinks.length > 0) {
|
|
84
|
+
this.reparentSpanLinks(spanLinks)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Iterates over the span links from a dropped span and reassigns them to the last kept span.
|
|
91
|
+
* The id intrinsic attribute will also be updated to the value of the last kept span id.
|
|
92
|
+
*
|
|
93
|
+
* @param {SpanLink[]} spanLinks an array of span links to reparent to last kept span
|
|
94
|
+
*/
|
|
95
|
+
reparentSpanLinks(spanLinks) {
|
|
96
|
+
const lastSpan = this.spans.at(-1)
|
|
97
|
+
|
|
98
|
+
// The id intrinsics attribute needs to be updated to equal the id of the new span the
|
|
99
|
+
// span links are moving to.
|
|
100
|
+
for (const link of spanLinks) {
|
|
101
|
+
link.intrinsics.id = lastSpan.id
|
|
79
102
|
}
|
|
103
|
+
|
|
104
|
+
// move the span links events to the last kept span
|
|
105
|
+
Array.prototype.push.apply(lastSpan.spanLinks, spanLinks)
|
|
80
106
|
}
|
|
81
107
|
|
|
82
108
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "newrelic",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.10.0",
|
|
4
4
|
"author": "New Relic Node.js agent team <nodejs@newrelic.com>",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"contributors": [
|
|
@@ -198,6 +198,7 @@
|
|
|
198
198
|
"imports": {
|
|
199
199
|
"#agentlib/*.js": "./lib/*.js",
|
|
200
200
|
"#testlib/*.js": "./test/lib/*.js",
|
|
201
|
+
"#testlib/*.json": "./test/lib/*.json",
|
|
201
202
|
"#test/assert": "./test/lib/custom-assertions/index.js"
|
|
202
203
|
},
|
|
203
204
|
"dependencies": {
|