newrelic 13.2.0 → 13.3.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.
Files changed (59) hide show
  1. package/NEWS.md +44 -0
  2. package/THIRD_PARTY_NOTICES.md +219 -8
  3. package/api.js +1 -1
  4. package/esm-loader.mjs +6 -1
  5. package/lib/instrumentation/@azure/functions.js +8 -46
  6. package/lib/instrumentation/core/http-outbound.js +26 -10
  7. package/lib/instrumentation/core/http.js +20 -118
  8. package/lib/instrumentations.js +0 -14
  9. package/lib/otel/logs/index.js +80 -0
  10. package/lib/otel/metrics/index.js +119 -0
  11. package/lib/otel/setup-signal.js +37 -0
  12. package/lib/otel/setup.js +15 -25
  13. package/lib/otel/{attr-reconciler.js → traces/attr-reconciler.js} +2 -2
  14. package/lib/otel/{exception-mapping.js → traces/exception-mapping.js} +1 -1
  15. package/lib/otel/traces/index.js +40 -0
  16. package/lib/otel/{rules.js → traces/rules.js} +1 -1
  17. package/lib/otel/{segment-synthesis.js → traces/segment-synthesis.js} +5 -4
  18. package/lib/otel/{segments → traces/segments}/consumer.js +4 -4
  19. package/lib/otel/{segments → traces/segments}/database.js +6 -6
  20. package/lib/otel/{segments → traces/segments}/http-external.js +11 -8
  21. package/lib/otel/traces/segments/index.js +22 -0
  22. package/lib/otel/{segments → traces/segments}/internal.js +1 -1
  23. package/lib/otel/{segments → traces/segments}/producer.js +2 -2
  24. package/lib/otel/{segments → traces/segments}/server.js +4 -4
  25. package/lib/otel/{segments → traces/segments}/utils.js +1 -1
  26. package/lib/otel/{span-processor.js → traces/span-processor.js} +13 -19
  27. package/lib/otel/{transformation-rules.json → traces/transformation-rules.json} +3 -2
  28. package/lib/otel/{utils.js → traces/utils.js} +2 -1
  29. package/lib/serverless/aws-lambda.js +7 -68
  30. package/lib/shim/webframework-shim/middleware.js +1 -1
  31. package/lib/shimmer.js +2 -2
  32. package/lib/subscriber-configs.js +2 -1
  33. package/lib/subscribers/base.js +30 -0
  34. package/lib/subscribers/elasticsearch/config.js +1 -0
  35. package/lib/subscribers/mcp-sdk/client-request.js +66 -0
  36. package/lib/subscribers/mcp-sdk/config.js +13 -75
  37. package/lib/subscribers/openai/base.js +21 -0
  38. package/lib/subscribers/openai/chat-responses.js +15 -0
  39. package/lib/subscribers/openai/chat.js +85 -0
  40. package/lib/subscribers/openai/client.js +31 -0
  41. package/lib/subscribers/openai/config.js +85 -0
  42. package/lib/subscribers/openai/embeddings.js +54 -0
  43. package/lib/subscribers/openai/utils.js +349 -0
  44. package/lib/transaction/index.js +157 -56
  45. package/lib/transaction/trace/segment.js +5 -2
  46. package/lib/util/urltils.js +158 -192
  47. package/lib/utilization/common.js +0 -6
  48. package/package.json +3 -4
  49. package/lib/instrumentation/openai.js +0 -482
  50. package/lib/otel/logs/bootstrap-logs.js +0 -84
  51. package/lib/otel/metrics/bootstrap-metrics.js +0 -117
  52. package/lib/otel/segments/index.js +0 -22
  53. package/lib/patch-module.js +0 -70
  54. package/lib/subscribers/mcp-sdk/client-prompt.js +0 -23
  55. package/lib/subscribers/mcp-sdk/client-resource.js +0 -24
  56. package/lib/subscribers/mcp-sdk/client-tool.js +0 -23
  57. package/lib/subscribers/mcp-sdk/client.js +0 -29
  58. /package/lib/otel/{constants.js → traces/constants.js} +0 -0
  59. /package/lib/otel/{sampler.js → traces/sampler.js} +0 -0
@@ -0,0 +1,85 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ /*
8
+ * Copyright 2025 New Relic Corporation. All rights reserved.
9
+ * SPDX-License-Identifier: Apache-2.0
10
+ */
11
+
12
+ const OpenAISubscriber = require('./base')
13
+ const { AI } = require('../../metrics/names')
14
+ const { OPENAI } = AI
15
+ const {
16
+ addLlmMeta,
17
+ recordChatCompletionMessages,
18
+ instrumentStream,
19
+ } = require('./utils')
20
+ const semver = require('semver')
21
+ const MIN_STREAM_VERSION = '4.12.2'
22
+
23
+ class OpenAIChatCompletions extends OpenAISubscriber {
24
+ constructor({ agent, logger, channelName = 'nr_completionsCreate' }) {
25
+ super({ agent, logger, channelName })
26
+ }
27
+
28
+ handler(data, ctx) {
29
+ const { arguments: args, moduleVersion } = data
30
+ const [request] = args
31
+ if (request.stream && semver.lt(moduleVersion, MIN_STREAM_VERSION)) {
32
+ this.logger.warn(`Instrumenting chat completion streams is only supported with openai version ${MIN_STREAM_VERSION}+.`)
33
+ return ctx
34
+ }
35
+
36
+ const segment = this.agent.tracer.createSegment({
37
+ name: OPENAI.COMPLETION,
38
+ parent: ctx.segment,
39
+ transaction: ctx.transaction
40
+ })
41
+ const newCtx = ctx.enterSegment({ segment })
42
+ return newCtx
43
+ }
44
+
45
+ asyncEnd(data) {
46
+ const ctx = this.agent.tracer.getContext()
47
+ if (!ctx?.segment || !ctx?.transaction) {
48
+ return
49
+ }
50
+ const { result: response, arguments: args, error: err } = data
51
+ const [request] = args
52
+
53
+ if (request.stream) {
54
+ instrumentStream({
55
+ agent: this.agent,
56
+ logger: this.logger,
57
+ request,
58
+ headers: ctx.extras?.headers,
59
+ response,
60
+ segment: ctx.segment,
61
+ transaction: ctx.transaction,
62
+ err
63
+ })
64
+ } else {
65
+ recordChatCompletionMessages({
66
+ agent: this.agent,
67
+ logger: this.logger,
68
+ segment: ctx.segment,
69
+ transaction: ctx.transaction,
70
+ request,
71
+ response,
72
+ headers: ctx.extras?.headers,
73
+ err
74
+ })
75
+ }
76
+
77
+ addLlmMeta({
78
+ agent: this.agent,
79
+ transaction: ctx.transaction,
80
+ version: data.moduleVersion,
81
+ })
82
+ }
83
+ }
84
+
85
+ module.exports = OpenAIChatCompletions
@@ -0,0 +1,31 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const OpenAISubscriber = require('./base')
8
+ class OpenAIClientSubscriber extends OpenAISubscriber {
9
+ constructor({ agent, logger }) {
10
+ super({ agent, logger, channelName: 'nr_makeRequest' })
11
+ }
12
+
13
+ handler(data, ctx) {
14
+ const { self } = data
15
+ ctx.extras = { apiKey: self.apiKey }
16
+ return ctx
17
+ }
18
+
19
+ asyncEnd(data) {
20
+ const { result } = data
21
+ const ctx = this.agent.tracer.getContext()
22
+ if (ctx?.segment) {
23
+ const headers = result?.response?.headers
24
+ ? Object.fromEntries(result.response.headers)
25
+ : { ...result?.headers }
26
+ ctx.extras = { headers }
27
+ }
28
+ }
29
+ }
30
+
31
+ module.exports = OpenAIClientSubscriber
@@ -0,0 +1,85 @@
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 = {
9
+ openai: [
10
+ {
11
+ path: './openai/chat.js',
12
+ instrumentations: [
13
+ {
14
+ channelName: 'nr_completionsCreate',
15
+ module: { name: 'openai', versionRange: '>=4.0.0', filePath: 'resources/chat/completions.js' },
16
+ functionQuery: {
17
+ className: 'Completions',
18
+ methodName: 'create',
19
+ kind: 'Async'
20
+ }
21
+ },
22
+ {
23
+ channelName: 'nr_completionsCreate',
24
+ module: { name: 'openai', versionRange: '>=4.0.0', filePath: 'resources/chat/completions/completions.js' },
25
+ functionQuery: {
26
+ className: 'Completions',
27
+ methodName: 'create',
28
+ kind: 'Async'
29
+ }
30
+ }
31
+ ]
32
+ },
33
+ {
34
+ path: './openai/chat-responses.js',
35
+ instrumentations: [
36
+ {
37
+ channelName: 'nr_responses',
38
+ module: { name: 'openai', versionRange: '>=4.87.0', filePath: 'resources/responses/responses.js' },
39
+ functionQuery: {
40
+ className: 'Responses',
41
+ methodName: 'create',
42
+ kind: 'Async'
43
+ }
44
+ }
45
+ ]
46
+ },
47
+ {
48
+ path: './openai/client.js',
49
+ instrumentations: [
50
+ {
51
+ channelName: 'nr_makeRequest',
52
+ module: { name: 'openai', versionRange: '>=4.0.0', filePath: 'core.js' },
53
+ functionQuery: {
54
+ className: 'APIClient',
55
+ methodName: 'makeRequest',
56
+ kind: 'Async'
57
+ }
58
+ },
59
+ {
60
+ channelName: 'nr_makeRequest',
61
+ module: { name: 'openai', versionRange: '>=4.0.0', filePath: 'client.js' },
62
+ functionQuery: {
63
+ className: 'OpenAI',
64
+ methodName: 'makeRequest',
65
+ kind: 'Async'
66
+ }
67
+ }
68
+ ]
69
+ },
70
+ {
71
+ path: './openai/embeddings.js',
72
+ instrumentations: [
73
+ {
74
+ channelName: 'nr_embeddingsCreate',
75
+ module: { name: 'openai', versionRange: '>=4.0.0', filePath: 'resources/embeddings.js' },
76
+ functionQuery: {
77
+ className: 'Embeddings',
78
+ methodName: 'create',
79
+ kind: 'Async'
80
+ }
81
+ }
82
+ ]
83
+ }
84
+ ]
85
+ }
@@ -0,0 +1,54 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const OpenAISubscriber = require('./base')
8
+ const { AI } = require('../../metrics/names')
9
+ const { OPENAI } = AI
10
+ const {
11
+ addLlmMeta,
12
+ recordEmbeddingMessage
13
+ } = require('./utils')
14
+
15
+ class OpenAIEmbeddings extends OpenAISubscriber {
16
+ constructor({ agent, logger }) {
17
+ super({ agent, logger, channelName: 'nr_embeddingsCreate' })
18
+ }
19
+
20
+ handler(data, ctx) {
21
+ const segment = this.agent.tracer.createSegment({
22
+ name: OPENAI.EMBEDDING,
23
+ parent: ctx.segment,
24
+ transaction: ctx.transaction
25
+ })
26
+ const newCtx = ctx.enterSegment({ segment })
27
+ return newCtx
28
+ }
29
+
30
+ asyncEnd(data) {
31
+ const ctx = this.agent.tracer.getContext()
32
+ if (!ctx?.segment || !ctx?.transaction) {
33
+ return
34
+ }
35
+ const { arguments: args, error: err } = data
36
+ const { result: response } = data
37
+ const [request] = args
38
+ const agent = this.agent
39
+ const { segment, transaction } = ctx
40
+ recordEmbeddingMessage({
41
+ agent,
42
+ logger: this.logger,
43
+ segment,
44
+ transaction,
45
+ request,
46
+ response,
47
+ headers: ctx.extras?.headers,
48
+ err
49
+ })
50
+ addLlmMeta({ agent, transaction, version: data.moduleVersion })
51
+ }
52
+ }
53
+
54
+ module.exports = OpenAIEmbeddings
@@ -0,0 +1,349 @@
1
+ /*
2
+ * Copyright 2023 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const {
8
+ LlmChatCompletionMessage,
9
+ LlmChatCompletionSummary,
10
+ LlmEmbedding,
11
+ LlmErrorMessage
12
+ } = require('../../llm-events/openai')
13
+ const { extractLlmContext } = require('../../util/llm-utils')
14
+
15
+ const { AI } = require('../../metrics/names')
16
+ const { OPENAI } = AI
17
+ const { DESTINATIONS } = require('../../config/attribute-filter')
18
+ let TRACKING_METRIC = OPENAI.TRACKING_PREFIX
19
+
20
+ /**
21
+ * Parses the response from OpenAI and extracts the message content and role.
22
+ *
23
+ * @param {object} response The OpenAI SDK response object
24
+ * @returns {{ content: string, role: string }} the message object with fields `content` and `role`
25
+ */
26
+ function getMessageFromResponse(response) {
27
+ let content
28
+ let role
29
+ if (response?.object === 'response') {
30
+ content = response?.output?.[0]?.content?.[0]?.text
31
+ role = response?.output?.[0]?.role
32
+ } else {
33
+ content = response?.choices?.[0]?.message?.content
34
+ role = response?.choices?.[0]?.message?.role
35
+ }
36
+
37
+ return { content, role }
38
+ }
39
+
40
+ /**
41
+ * Parses all messages from the OpenAI request object.
42
+ *
43
+ * @param {object} request The OpenAI SDK request object
44
+ * @param logger
45
+ * @returns {Array<object>} an array of message objects with fields `content` and `role`
46
+ */
47
+ function getMessagesFromRequest(request, logger) {
48
+ // There are a few different ways to pass messages to OpenAI SDK.
49
+ //
50
+ // For langchain and `chat.completions.create`, messages are passed
51
+ // as an array of objects with `content` and `role` properties
52
+ // to the `messages` field of the request.
53
+ //
54
+ // For `responses.create`, messages are passed as an array of objects
55
+ // with `content` and `role` properties OR as a single string (implied
56
+ // to be a user message) to the `input` field of the request.
57
+ let messages = []
58
+
59
+ if (Array.isArray(request?.input)) {
60
+ // Handle array of input messages
61
+ messages = request.input.filter((msg) => msg?.content && msg?.role)
62
+ } else if (typeof request?.input === 'string') {
63
+ // Handle single string input as a user message
64
+ messages = [{ content: request.input, role: 'user' }]
65
+ } else if (Array.isArray(request?.messages)) {
66
+ // Handle array of messages
67
+ messages = request.messages.filter((msg) => msg?.content && msg?.role)
68
+ } else {
69
+ logger.warn('No valid messages found in OpenAI request object.')
70
+ }
71
+
72
+ return messages
73
+ }
74
+
75
+ /**
76
+ * Enqueues a LLM event to the custom event aggregator
77
+ *
78
+ * @param {object} params input params
79
+ * @param {Agent} params.agent NR agent instance
80
+ * @param {string} params.type LLM event type
81
+ * @param {object} params.msg LLM event
82
+ */
83
+ function recordEvent({ agent, type, msg }) {
84
+ const llmContext = extractLlmContext(agent)
85
+
86
+ agent.customEventAggregator.add([
87
+ { type, timestamp: Date.now() },
88
+ Object.assign({}, msg, llmContext)
89
+ ])
90
+ }
91
+
92
+ /**
93
+ * Increments the tracking metric and sets the llm attribute on transactions
94
+ *
95
+ * @param {object} params input params
96
+ * @param {Agent} params.agent NR agent instance
97
+ * @param {Transaction} params.transaction active transaction
98
+ * @param params.version
99
+ */
100
+ function addLlmMeta({ agent, transaction, version }) {
101
+ TRACKING_METRIC = `${TRACKING_METRIC}/${version}`
102
+ agent.metrics.getOrCreateMetric(TRACKING_METRIC).incrementCallCount()
103
+ transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true)
104
+ }
105
+
106
+ /**
107
+ * Generates LlmChatCompletionSummary for a chat completion creation.
108
+ * Also iterates over both input messages and the first response message
109
+ * and creates LlmChatCompletionMessage.
110
+ *
111
+ * Also assigns relevant ids by response id for LlmFeedbackEvent creation
112
+ *
113
+ * @param {object} params input params
114
+ * @param {Agent} params.agent NR agent instance
115
+ * @param {TraceSegment} params.segment active segment from chat completion
116
+ * @param {object} params.request chat completion params
117
+ * @param {object} params.response chat completion response
118
+ * @param {boolean} [params.err] err if it exists
119
+ * @param {Transaction} params.transaction active transaction
120
+ * @param params.headers
121
+ * @param params.logger
122
+ */
123
+ function recordChatCompletionMessages({
124
+ agent,
125
+ headers,
126
+ logger,
127
+ segment,
128
+ request,
129
+ response,
130
+ err,
131
+ transaction
132
+ }) {
133
+ if (shouldSkipInstrumentation(agent.config, logger) === true) {
134
+ logger.debug('skipping sending of ai data')
135
+ return
136
+ }
137
+
138
+ if (!response) {
139
+ // If we get an error, it is possible that `response = null`.
140
+ // In that case, we define it to be an empty object.
141
+ response = {}
142
+ }
143
+
144
+ response.headers = headers
145
+ // explicitly end segment to consistent duration
146
+ // for both LLM events and the segment
147
+ segment.end()
148
+ const completionSummary = new LlmChatCompletionSummary({
149
+ agent,
150
+ segment,
151
+ transaction,
152
+ request,
153
+ response,
154
+ withError: err != null
155
+ })
156
+
157
+ // Note: langchain still expects a message event even
158
+ // when the response is empty, so we will not filter
159
+ // the response message.
160
+ const messages = [
161
+ ...getMessagesFromRequest(request, logger),
162
+ getMessageFromResponse(response)
163
+ ]
164
+
165
+ messages.forEach((message, index) => {
166
+ const completionMsg = new LlmChatCompletionMessage({
167
+ agent,
168
+ segment,
169
+ transaction,
170
+ request,
171
+ response,
172
+ index,
173
+ completionId: completionSummary.id,
174
+ message
175
+ })
176
+
177
+ recordEvent({ agent, type: 'LlmChatCompletionMessage', msg: completionMsg })
178
+ })
179
+
180
+ recordEvent({ agent, type: 'LlmChatCompletionSummary', msg: completionSummary })
181
+
182
+ if (err) {
183
+ const llmError = new LlmErrorMessage({ cause: err, summary: completionSummary, response })
184
+ agent.errors.add(transaction, err, llmError)
185
+ }
186
+
187
+ delete response.headers
188
+ }
189
+
190
+ /**
191
+ * Checks if we should skip instrumentation.
192
+ * Currently, it checks if `ai_monitoring.enabled` is true
193
+ * and the package version >= 4.0.0
194
+ *
195
+ * @param {object} config agent config
196
+ * @param logger
197
+ * @returns {boolean} flag if instrumentation should be skipped
198
+ */
199
+ function shouldSkipInstrumentation(config, logger) {
200
+ if (config?.ai_monitoring?.enabled !== true) {
201
+ logger.debug('config.ai_monitoring.enabled is set to false.')
202
+ return true
203
+ }
204
+ }
205
+
206
+ /**
207
+ * `chat.completions.create` can return a stream once promise resolves.
208
+ * This wraps the iterator which is a generator function.
209
+ * We will call the original iterator, intercept chunks and yield
210
+ * to the original. On complete, we will construct the new message object
211
+ * with what we have seen in the stream and create the chat completion
212
+ * messages.
213
+ * @param {object} params input params
214
+ * @param {Agent} params.agent NR agent instance
215
+ * @param {Shim} params.shim the current shim instance
216
+ * @param {object} params.request chat completion params
217
+ * @param {object} params.response chat completion response
218
+ * @param {TraceSegment} params.segment active segment from chat completion
219
+ * @param {Transaction} params.transaction active transaction
220
+ * @param {Error} params.err error if it exists
221
+ * @param params.headers
222
+ * @param params.logger
223
+ */
224
+ function instrumentStream({ agent, headers, logger, request, response, segment, transaction, err = null }) {
225
+ if (!agent.config.ai_monitoring.streaming.enabled) {
226
+ logger.warn(
227
+ '`ai_monitoring.streaming.enabled` is set to `false`, stream will not be instrumented.'
228
+ )
229
+ agent.metrics.getOrCreateMetric(AI.STREAMING_DISABLED).incrementCallCount()
230
+ return
231
+ }
232
+
233
+ if (err) {
234
+ // If there is an error already e.g. APIConnectionError,
235
+ // the iterator will not be called, so we have to
236
+ // record the chat completion messages with the error now.
237
+ segment.touch()
238
+ recordChatCompletionMessages({
239
+ agent,
240
+ headers,
241
+ logger,
242
+ segment,
243
+ transaction,
244
+ request,
245
+ response,
246
+ err
247
+ })
248
+ return
249
+ }
250
+
251
+ const orig = response.iterator
252
+ response.iterator = async function * wrappedIterator() {
253
+ let content = ''
254
+ let role = ''
255
+ let chunk
256
+ try {
257
+ const iterator = orig.apply(this, arguments)
258
+
259
+ for await (chunk of iterator) {
260
+ if (chunk.choices?.[0]?.delta?.role) {
261
+ role = chunk.choices[0].delta.role
262
+ }
263
+
264
+ content += chunk.choices?.[0]?.delta?.content ?? ''
265
+ yield chunk
266
+ }
267
+ } catch (streamErr) {
268
+ err = streamErr
269
+ throw err
270
+ } finally {
271
+ if (chunk?.choices) {
272
+ chunk.choices[0].message = { role, content }
273
+ } else if (chunk?.response) {
274
+ chunk = chunk.response
275
+ }
276
+
277
+ // update segment duration since we want to extend the time it took to
278
+ // handle the stream
279
+ segment.touch()
280
+
281
+ recordChatCompletionMessages({
282
+ agent,
283
+ headers,
284
+ logger,
285
+ segment,
286
+ transaction,
287
+ request,
288
+ response: chunk,
289
+ err
290
+ })
291
+ }
292
+ }
293
+ }
294
+
295
+ function recordEmbeddingMessage({
296
+ agent,
297
+ logger,
298
+ request,
299
+ headers,
300
+ response,
301
+ segment,
302
+ transaction,
303
+ err
304
+ }) {
305
+ if (!response) {
306
+ // If we get an error, it is possible that `response = null`.
307
+ // In that case, we define it to be an empty object.
308
+ response = {}
309
+ }
310
+
311
+ segment.end()
312
+ if (shouldSkipInstrumentation(agent.config, logger) === true) {
313
+ // We need this check inside the wrapper because it is possible for monitoring
314
+ // to be disabled at the account level. In such a case, the value is set
315
+ // after the instrumentation has been initialized.
316
+ logger.debug('skipping sending of ai data')
317
+ return
318
+ }
319
+
320
+ response.headers = headers
321
+ // explicitly end segment to get consistent duration
322
+ // for both LLM events and the segment
323
+
324
+ const embedding = new LlmEmbedding({
325
+ agent,
326
+ segment,
327
+ transaction,
328
+ request,
329
+ response,
330
+ withError: err != null
331
+ })
332
+
333
+ recordEvent({ agent, type: 'LlmEmbedding', msg: embedding })
334
+
335
+ if (err) {
336
+ const llmError = new LlmErrorMessage({ cause: err, embedding, response })
337
+ agent.errors.add(transaction, err, llmError)
338
+ }
339
+
340
+ // cleanup keys on response before returning to user code
341
+ delete response.headers
342
+ }
343
+
344
+ module.exports = {
345
+ addLlmMeta,
346
+ instrumentStream,
347
+ recordChatCompletionMessages,
348
+ recordEmbeddingMessage
349
+ }