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.
- package/NEWS.md +44 -0
- package/THIRD_PARTY_NOTICES.md +219 -8
- package/api.js +1 -1
- package/esm-loader.mjs +6 -1
- package/lib/instrumentation/@azure/functions.js +8 -46
- package/lib/instrumentation/core/http-outbound.js +26 -10
- package/lib/instrumentation/core/http.js +20 -118
- package/lib/instrumentations.js +0 -14
- package/lib/otel/logs/index.js +80 -0
- package/lib/otel/metrics/index.js +119 -0
- package/lib/otel/setup-signal.js +37 -0
- package/lib/otel/setup.js +15 -25
- package/lib/otel/{attr-reconciler.js → traces/attr-reconciler.js} +2 -2
- package/lib/otel/{exception-mapping.js → traces/exception-mapping.js} +1 -1
- package/lib/otel/traces/index.js +40 -0
- package/lib/otel/{rules.js → traces/rules.js} +1 -1
- package/lib/otel/{segment-synthesis.js → traces/segment-synthesis.js} +5 -4
- package/lib/otel/{segments → traces/segments}/consumer.js +4 -4
- package/lib/otel/{segments → traces/segments}/database.js +6 -6
- package/lib/otel/{segments → traces/segments}/http-external.js +11 -8
- package/lib/otel/traces/segments/index.js +22 -0
- package/lib/otel/{segments → traces/segments}/internal.js +1 -1
- package/lib/otel/{segments → traces/segments}/producer.js +2 -2
- package/lib/otel/{segments → traces/segments}/server.js +4 -4
- package/lib/otel/{segments → traces/segments}/utils.js +1 -1
- package/lib/otel/{span-processor.js → traces/span-processor.js} +13 -19
- package/lib/otel/{transformation-rules.json → traces/transformation-rules.json} +3 -2
- package/lib/otel/{utils.js → traces/utils.js} +2 -1
- package/lib/serverless/aws-lambda.js +7 -68
- package/lib/shim/webframework-shim/middleware.js +1 -1
- package/lib/shimmer.js +2 -2
- package/lib/subscriber-configs.js +2 -1
- package/lib/subscribers/base.js +30 -0
- package/lib/subscribers/elasticsearch/config.js +1 -0
- package/lib/subscribers/mcp-sdk/client-request.js +66 -0
- package/lib/subscribers/mcp-sdk/config.js +13 -75
- package/lib/subscribers/openai/base.js +21 -0
- package/lib/subscribers/openai/chat-responses.js +15 -0
- package/lib/subscribers/openai/chat.js +85 -0
- package/lib/subscribers/openai/client.js +31 -0
- package/lib/subscribers/openai/config.js +85 -0
- package/lib/subscribers/openai/embeddings.js +54 -0
- package/lib/subscribers/openai/utils.js +349 -0
- package/lib/transaction/index.js +157 -56
- package/lib/transaction/trace/segment.js +5 -2
- package/lib/util/urltils.js +158 -192
- package/lib/utilization/common.js +0 -6
- package/package.json +3 -4
- package/lib/instrumentation/openai.js +0 -482
- package/lib/otel/logs/bootstrap-logs.js +0 -84
- package/lib/otel/metrics/bootstrap-metrics.js +0 -117
- package/lib/otel/segments/index.js +0 -22
- package/lib/patch-module.js +0 -70
- package/lib/subscribers/mcp-sdk/client-prompt.js +0 -23
- package/lib/subscribers/mcp-sdk/client-resource.js +0 -24
- package/lib/subscribers/mcp-sdk/client-tool.js +0 -23
- package/lib/subscribers/mcp-sdk/client.js +0 -29
- /package/lib/otel/{constants.js → traces/constants.js} +0 -0
- /package/lib/otel/{sampler.js → traces/sampler.js} +0 -0
|
@@ -1,482 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2023 New Relic Corporation. All rights reserved.
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
'use strict'
|
|
7
|
-
const { openAiHeaders, openAiApiKey } = require('../../lib/symbols')
|
|
8
|
-
const {
|
|
9
|
-
LlmChatCompletionMessage,
|
|
10
|
-
LlmChatCompletionSummary,
|
|
11
|
-
LlmEmbedding,
|
|
12
|
-
LlmErrorMessage
|
|
13
|
-
} = require('../../lib/llm-events/openai')
|
|
14
|
-
const { RecorderSpec } = require('../../lib/shim/specs')
|
|
15
|
-
const { extractLlmContext } = require('../util/llm-utils')
|
|
16
|
-
|
|
17
|
-
const MIN_VERSION = '4.0.0'
|
|
18
|
-
const MIN_STREAM_VERSION = '4.12.2'
|
|
19
|
-
const { AI } = require('../../lib/metrics/names')
|
|
20
|
-
const { OPENAI } = AI
|
|
21
|
-
const semver = require('semver')
|
|
22
|
-
const { DESTINATIONS } = require('../config/attribute-filter')
|
|
23
|
-
|
|
24
|
-
let TRACKING_METRIC = OPENAI.TRACKING_PREFIX
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Parses the response from OpenAI and extracts the message content and role.
|
|
28
|
-
*
|
|
29
|
-
* @param {object} response The OpenAI SDK response object
|
|
30
|
-
* @returns {{ content: string, role: string }} the message object with fields `content` and `role`
|
|
31
|
-
*/
|
|
32
|
-
function getMessageFromResponse(response) {
|
|
33
|
-
let content
|
|
34
|
-
let role
|
|
35
|
-
if (response?.object === 'response') {
|
|
36
|
-
content = response?.output?.[0]?.content?.[0]?.text
|
|
37
|
-
role = response?.output?.[0]?.role
|
|
38
|
-
} else {
|
|
39
|
-
content = response?.choices?.[0]?.message?.content
|
|
40
|
-
role = response?.choices?.[0]?.message?.role
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return { content, role }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Parses all messages from the OpenAI request object.
|
|
48
|
-
*
|
|
49
|
-
* @param {object} request The OpenAI SDK request object
|
|
50
|
-
* @param {Shim} shim instance of shim
|
|
51
|
-
* @returns {Array<object>} an array of message objects with fields `content` and `role`
|
|
52
|
-
*/
|
|
53
|
-
function getMessagesFromRequest(request, shim) {
|
|
54
|
-
// There are a few different ways to pass messages to OpenAI SDK.
|
|
55
|
-
//
|
|
56
|
-
// For langchain and `chat.completions.create`, messages are passed
|
|
57
|
-
// as an array of objects with `content` and `role` properties
|
|
58
|
-
// to the `messages` field of the request.
|
|
59
|
-
//
|
|
60
|
-
// For `responses.create`, messages are passed as an array of objects
|
|
61
|
-
// with `content` and `role` properties OR as a single string (implied
|
|
62
|
-
// to be a user message) to the `input` field of the request.
|
|
63
|
-
let messages = []
|
|
64
|
-
|
|
65
|
-
if (Array.isArray(request?.input)) {
|
|
66
|
-
// Handle array of input messages
|
|
67
|
-
messages = request.input.filter((msg) => msg?.content && msg?.role)
|
|
68
|
-
} else if (typeof request?.input === 'string') {
|
|
69
|
-
// Handle single string input as a user message
|
|
70
|
-
messages = [{ content: request.input, role: 'user' }]
|
|
71
|
-
} else if (Array.isArray(request?.messages)) {
|
|
72
|
-
// Handle array of messages
|
|
73
|
-
messages = request.messages.filter((msg) => msg?.content && msg?.role)
|
|
74
|
-
} else {
|
|
75
|
-
shim.logger.warn('No valid messages found in OpenAI request object.')
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return messages
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Checks if we should skip instrumentation.
|
|
83
|
-
* Currently, it checks if `ai_monitoring.enabled` is true
|
|
84
|
-
* and the package version >= 4.0.0
|
|
85
|
-
*
|
|
86
|
-
* @param {object} config agent config
|
|
87
|
-
* @param {Shim} shim instance of shim
|
|
88
|
-
* @returns {boolean} flag if instrumentation should be skipped
|
|
89
|
-
*/
|
|
90
|
-
function shouldSkipInstrumentation(config, shim) {
|
|
91
|
-
if (config?.ai_monitoring?.enabled !== true) {
|
|
92
|
-
shim.logger.debug('config.ai_monitoring.enabled is set to false.')
|
|
93
|
-
return true
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const { pkgVersion } = shim
|
|
97
|
-
return semver.lt(pkgVersion, MIN_VERSION)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Adds apiKey and response headers to the active segment
|
|
102
|
-
* on symbols
|
|
103
|
-
*
|
|
104
|
-
* @param {object} params input params
|
|
105
|
-
* @param {Shim} params.shim instance of shim
|
|
106
|
-
* @param {object} params.result from openai request
|
|
107
|
-
* @param {string} params.apiKey api key from openai client
|
|
108
|
-
*/
|
|
109
|
-
function decorateSegment({ shim, result, apiKey }) {
|
|
110
|
-
const segment = shim.getActiveSegment()
|
|
111
|
-
|
|
112
|
-
if (segment) {
|
|
113
|
-
segment[openAiApiKey] = apiKey
|
|
114
|
-
|
|
115
|
-
// If the result is an error, which is an OpenAI client error, then
|
|
116
|
-
// the headers are provided via a proxy attached to `result.headers`.
|
|
117
|
-
// Otherwise, result is a typical response-like object.
|
|
118
|
-
const headers = result?.response?.headers
|
|
119
|
-
? Object.fromEntries(result.response.headers)
|
|
120
|
-
: { ...result?.headers }
|
|
121
|
-
segment[openAiHeaders] = headers
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Enqueues a LLM event to the custom event aggregator
|
|
127
|
-
*
|
|
128
|
-
* @param {object} params input params
|
|
129
|
-
* @param {Agent} params.agent NR agent instance
|
|
130
|
-
* @param {string} params.type LLM event type
|
|
131
|
-
* @param {object} params.msg LLM event
|
|
132
|
-
*/
|
|
133
|
-
function recordEvent({ agent, type, msg }) {
|
|
134
|
-
const llmContext = extractLlmContext(agent)
|
|
135
|
-
|
|
136
|
-
agent.customEventAggregator.add([
|
|
137
|
-
{ type, timestamp: Date.now() },
|
|
138
|
-
Object.assign({}, msg, llmContext)
|
|
139
|
-
])
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Increments the tracking metric and sets the llm attribute on transactions
|
|
144
|
-
*
|
|
145
|
-
* @param {object} params input params
|
|
146
|
-
* @param {Agent} params.agent NR agent instance
|
|
147
|
-
* @param {Transaction} params.transaction active transaction
|
|
148
|
-
*/
|
|
149
|
-
function addLlmMeta({ agent, transaction }) {
|
|
150
|
-
agent.metrics.getOrCreateMetric(TRACKING_METRIC).incrementCallCount()
|
|
151
|
-
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Generates LlmChatCompletionSummary for a chat completion creation.
|
|
156
|
-
* Also iterates over both input messages and the first response message
|
|
157
|
-
* and creates LlmChatCompletionMessage.
|
|
158
|
-
*
|
|
159
|
-
* Also assigns relevant ids by response id for LlmFeedbackEvent creation
|
|
160
|
-
*
|
|
161
|
-
* @param {object} params input params
|
|
162
|
-
* @param {Agent} params.agent NR agent instance
|
|
163
|
-
* @param {Shim} params.shim the current shim instance
|
|
164
|
-
* @param {TraceSegment} params.segment active segment from chat completion
|
|
165
|
-
* @param {object} params.request chat completion params
|
|
166
|
-
* @param {object} params.response chat completion response
|
|
167
|
-
* @param {boolean} [params.err] err if it exists
|
|
168
|
-
* @param {Transaction} params.transaction active transaction
|
|
169
|
-
*/
|
|
170
|
-
function recordChatCompletionMessages({
|
|
171
|
-
agent,
|
|
172
|
-
shim,
|
|
173
|
-
segment,
|
|
174
|
-
request,
|
|
175
|
-
response,
|
|
176
|
-
err,
|
|
177
|
-
transaction
|
|
178
|
-
}) {
|
|
179
|
-
if (shouldSkipInstrumentation(agent.config, shim) === true) {
|
|
180
|
-
shim.logger.debug('skipping sending of ai data')
|
|
181
|
-
return
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (!response) {
|
|
185
|
-
// If we get an error, it is possible that `response = null`.
|
|
186
|
-
// In that case, we define it to be an empty object.
|
|
187
|
-
response = {}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
response.headers = segment[openAiHeaders]
|
|
191
|
-
// explicitly end segment to consistent duration
|
|
192
|
-
// for both LLM events and the segment
|
|
193
|
-
segment.end()
|
|
194
|
-
const completionSummary = new LlmChatCompletionSummary({
|
|
195
|
-
agent,
|
|
196
|
-
segment,
|
|
197
|
-
transaction,
|
|
198
|
-
request,
|
|
199
|
-
response,
|
|
200
|
-
withError: err != null
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
// Note: langchain still expects a message event even
|
|
204
|
-
// when the response is empty, so we will not filter
|
|
205
|
-
// the response message.
|
|
206
|
-
const messages = [
|
|
207
|
-
...getMessagesFromRequest(request, shim),
|
|
208
|
-
getMessageFromResponse(response)
|
|
209
|
-
]
|
|
210
|
-
|
|
211
|
-
messages.forEach((message, index) => {
|
|
212
|
-
const completionMsg = new LlmChatCompletionMessage({
|
|
213
|
-
agent,
|
|
214
|
-
segment,
|
|
215
|
-
transaction,
|
|
216
|
-
request,
|
|
217
|
-
response,
|
|
218
|
-
index,
|
|
219
|
-
completionId: completionSummary.id,
|
|
220
|
-
message
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
recordEvent({ agent, type: 'LlmChatCompletionMessage', msg: completionMsg })
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
recordEvent({ agent, type: 'LlmChatCompletionSummary', msg: completionSummary })
|
|
227
|
-
|
|
228
|
-
if (err) {
|
|
229
|
-
const llmError = new LlmErrorMessage({ cause: err, summary: completionSummary, response })
|
|
230
|
-
agent.errors.add(transaction, err, llmError)
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
delete response.headers
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* `chat.completions.create` can return a stream once promise resolves.
|
|
238
|
-
* This wraps the iterator which is a generator function.
|
|
239
|
-
* We will call the original iterator, intercept chunks and yield
|
|
240
|
-
* to the original. On complete, we will construct the new message object
|
|
241
|
-
* with what we have seen in the stream and create the chat completion
|
|
242
|
-
* messages.
|
|
243
|
-
* @param {object} params input params
|
|
244
|
-
* @param {Agent} params.agent NR agent instance
|
|
245
|
-
* @param {Shim} params.shim the current shim instance
|
|
246
|
-
* @param {object} params.request chat completion params
|
|
247
|
-
* @param {object} params.response chat completion response
|
|
248
|
-
* @param {TraceSegment} params.segment active segment from chat completion
|
|
249
|
-
* @param {Transaction} params.transaction active transaction
|
|
250
|
-
* @param {Error} params.err error if it exists
|
|
251
|
-
*/
|
|
252
|
-
function instrumentStream({ agent, shim, request, response, segment, transaction, err = null }) {
|
|
253
|
-
if (!agent.config.ai_monitoring.streaming.enabled) {
|
|
254
|
-
shim.logger.warn(
|
|
255
|
-
'`ai_monitoring.streaming.enabled` is set to `false`, stream will not be instrumented.'
|
|
256
|
-
)
|
|
257
|
-
agent.metrics.getOrCreateMetric(AI.STREAMING_DISABLED).incrementCallCount()
|
|
258
|
-
return
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (err) {
|
|
262
|
-
// If there is an error already e.g. APIConnectionError,
|
|
263
|
-
// the iterator will not be called, so we have to
|
|
264
|
-
// record the chat completion messages with the error now.
|
|
265
|
-
segment.touch()
|
|
266
|
-
recordChatCompletionMessages({
|
|
267
|
-
agent,
|
|
268
|
-
shim,
|
|
269
|
-
segment,
|
|
270
|
-
transaction,
|
|
271
|
-
request,
|
|
272
|
-
response,
|
|
273
|
-
err
|
|
274
|
-
})
|
|
275
|
-
return
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
shim.wrap(response, 'iterator', function wrapIterator(shim, orig) {
|
|
279
|
-
return async function * wrappedIterator() {
|
|
280
|
-
let content = ''
|
|
281
|
-
let role = ''
|
|
282
|
-
let chunk
|
|
283
|
-
try {
|
|
284
|
-
const iterator = orig.apply(this, arguments)
|
|
285
|
-
|
|
286
|
-
for await (chunk of iterator) {
|
|
287
|
-
if (chunk.choices?.[0]?.delta?.role) {
|
|
288
|
-
role = chunk.choices[0].delta.role
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
content += chunk.choices?.[0]?.delta?.content ?? ''
|
|
292
|
-
yield chunk
|
|
293
|
-
}
|
|
294
|
-
} catch (streamErr) {
|
|
295
|
-
err = streamErr
|
|
296
|
-
throw err
|
|
297
|
-
} finally {
|
|
298
|
-
if (chunk?.choices) {
|
|
299
|
-
chunk.choices[0].message = { role, content }
|
|
300
|
-
} else if (chunk?.response) {
|
|
301
|
-
chunk = chunk.response
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// update segment duration since we want to extend the time it took to
|
|
305
|
-
// handle the stream
|
|
306
|
-
segment.touch()
|
|
307
|
-
|
|
308
|
-
recordChatCompletionMessages({
|
|
309
|
-
agent: shim.agent,
|
|
310
|
-
shim,
|
|
311
|
-
segment,
|
|
312
|
-
transaction,
|
|
313
|
-
request,
|
|
314
|
-
response: chunk,
|
|
315
|
-
err
|
|
316
|
-
})
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
})
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
module.exports = function initialize(agent, openai, moduleName, shim) {
|
|
323
|
-
if (shouldSkipInstrumentation(agent.config, shim)) {
|
|
324
|
-
shim.logger.debug(
|
|
325
|
-
`${moduleName} instrumentation support is for versions >=${MIN_VERSION}. Skipping instrumentation.`
|
|
326
|
-
)
|
|
327
|
-
return
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Update the tracking metric name with the version of the library
|
|
331
|
-
// being instrumented. We do not have access to the version when
|
|
332
|
-
// initially declaring the variable.
|
|
333
|
-
TRACKING_METRIC = `${TRACKING_METRIC}/${shim.pkgVersion}`
|
|
334
|
-
|
|
335
|
-
// openai@^5.0.0 uses a different prototype structure
|
|
336
|
-
const openaiClient = semver.gte(shim.pkgVersion, '5.0.0') ? openai.OpenAI : openai
|
|
337
|
-
|
|
338
|
-
// Instrumentation is only done to get the response headers and attach
|
|
339
|
-
// to the active segment as openai hides the headers from the functions we are
|
|
340
|
-
// trying to instrument
|
|
341
|
-
shim.wrap(openaiClient.prototype, 'makeRequest', function wrapRequest(shim, makeRequest) {
|
|
342
|
-
return function wrappedRequest() {
|
|
343
|
-
const apiKey = this.apiKey
|
|
344
|
-
const request = makeRequest.apply(this, arguments)
|
|
345
|
-
request.then(
|
|
346
|
-
(result) => {
|
|
347
|
-
// add headers on resolve
|
|
348
|
-
decorateSegment({ shim, result, apiKey })
|
|
349
|
-
},
|
|
350
|
-
(result) => {
|
|
351
|
-
// add headers on reject
|
|
352
|
-
decorateSegment({ shim, result, apiKey })
|
|
353
|
-
}
|
|
354
|
-
)
|
|
355
|
-
return request
|
|
356
|
-
}
|
|
357
|
-
})
|
|
358
|
-
|
|
359
|
-
// Instruments chat completion creation
|
|
360
|
-
// and creates the LLM events
|
|
361
|
-
shim.record(
|
|
362
|
-
openaiClient.Chat.Completions.prototype,
|
|
363
|
-
'create',
|
|
364
|
-
function wrapCreate(shim, create, name, args) {
|
|
365
|
-
const [request] = args
|
|
366
|
-
if (request.stream && semver.lt(shim.pkgVersion, MIN_STREAM_VERSION)) {
|
|
367
|
-
shim.logger.warn(
|
|
368
|
-
`Instrumenting chat completion streams is only supported with openai version ${MIN_STREAM_VERSION}+.`
|
|
369
|
-
)
|
|
370
|
-
return
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return new RecorderSpec({
|
|
374
|
-
name: OPENAI.COMPLETION,
|
|
375
|
-
promise: true,
|
|
376
|
-
after({ error: err, result: response, segment, transaction }) {
|
|
377
|
-
if (request.stream) {
|
|
378
|
-
instrumentStream({ agent, shim, request, response, segment, transaction, err })
|
|
379
|
-
} else {
|
|
380
|
-
recordChatCompletionMessages({
|
|
381
|
-
agent,
|
|
382
|
-
shim,
|
|
383
|
-
segment,
|
|
384
|
-
transaction,
|
|
385
|
-
request,
|
|
386
|
-
response,
|
|
387
|
-
err
|
|
388
|
-
})
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
addLlmMeta({ agent, transaction })
|
|
392
|
-
}
|
|
393
|
-
})
|
|
394
|
-
}
|
|
395
|
-
)
|
|
396
|
-
|
|
397
|
-
// New API introduced in openai@4.87.0
|
|
398
|
-
// Also instruments chat completion creation
|
|
399
|
-
// and creates the LLM events
|
|
400
|
-
if (semver.gte(shim.pkgVersion, '4.87.0')) {
|
|
401
|
-
shim.record(openaiClient.Responses.prototype, 'create', function wrapCreate(shim, create, name, args) {
|
|
402
|
-
const [request] = args
|
|
403
|
-
|
|
404
|
-
return new RecorderSpec({
|
|
405
|
-
name: OPENAI.COMPLETION,
|
|
406
|
-
promise: true,
|
|
407
|
-
after({ error: err, result: response, segment, transaction }) {
|
|
408
|
-
if (request.stream) {
|
|
409
|
-
instrumentStream({ agent, shim, request, response, segment, transaction, err })
|
|
410
|
-
} else {
|
|
411
|
-
recordChatCompletionMessages({
|
|
412
|
-
agent,
|
|
413
|
-
shim,
|
|
414
|
-
segment,
|
|
415
|
-
transaction,
|
|
416
|
-
request,
|
|
417
|
-
response,
|
|
418
|
-
err
|
|
419
|
-
})
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
addLlmMeta({ agent, transaction })
|
|
423
|
-
}
|
|
424
|
-
})
|
|
425
|
-
})
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Instruments embedding creation
|
|
429
|
-
// and creates LlmEmbedding event
|
|
430
|
-
shim.record(
|
|
431
|
-
openaiClient.Embeddings.prototype,
|
|
432
|
-
'create',
|
|
433
|
-
function wrapEmbeddingCreate(shim, embeddingCreate, name, args) {
|
|
434
|
-
const [request] = args
|
|
435
|
-
return new RecorderSpec({
|
|
436
|
-
name: OPENAI.EMBEDDING,
|
|
437
|
-
promise: true,
|
|
438
|
-
after({ error: err, result: response, segment, transaction }) {
|
|
439
|
-
addLlmMeta({ agent, transaction })
|
|
440
|
-
|
|
441
|
-
if (!response) {
|
|
442
|
-
// If we get an error, it is possible that `response = null`.
|
|
443
|
-
// In that case, we define it to be an empty object.
|
|
444
|
-
response = {}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
segment.end()
|
|
448
|
-
if (shouldSkipInstrumentation(shim.agent.config, shim) === true) {
|
|
449
|
-
// We need this check inside the wrapper because it is possible for monitoring
|
|
450
|
-
// to be disabled at the account level. In such a case, the value is set
|
|
451
|
-
// after the instrumentation has been initialized.
|
|
452
|
-
shim.logger.debug('skipping sending of ai data')
|
|
453
|
-
return
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
response.headers = segment[openAiHeaders]
|
|
457
|
-
// explicitly end segment to get consistent duration
|
|
458
|
-
// for both LLM events and the segment
|
|
459
|
-
|
|
460
|
-
const embedding = new LlmEmbedding({
|
|
461
|
-
agent,
|
|
462
|
-
segment,
|
|
463
|
-
transaction,
|
|
464
|
-
request,
|
|
465
|
-
response,
|
|
466
|
-
withError: err != null
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
recordEvent({ agent, type: 'LlmEmbedding', msg: embedding })
|
|
470
|
-
|
|
471
|
-
if (err) {
|
|
472
|
-
const llmError = new LlmErrorMessage({ cause: err, embedding, response })
|
|
473
|
-
shim.agent.errors.add(transaction, err, llmError)
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// cleanup keys on response before returning to user code
|
|
477
|
-
delete response.headers
|
|
478
|
-
}
|
|
479
|
-
})
|
|
480
|
-
}
|
|
481
|
-
)
|
|
482
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
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 = bootstrapOtelLogs
|
|
9
|
-
|
|
10
|
-
const logsApi = require('@opentelemetry/api-logs')
|
|
11
|
-
const logsSdk = require('@opentelemetry/sdk-logs')
|
|
12
|
-
const defaultLogger = require('#agentlib/logger.js').child({ component: 'otel-logs' })
|
|
13
|
-
const {
|
|
14
|
-
isApplicationLoggingEnabled,
|
|
15
|
-
isLogForwardingEnabled,
|
|
16
|
-
isMetricsEnabled,
|
|
17
|
-
incrementLoggingLinesMetrics
|
|
18
|
-
} = require('#agentlib/util/application-logging.js')
|
|
19
|
-
|
|
20
|
-
const NewRelicLoggerProvider = require('./proxying-provider.js')
|
|
21
|
-
const NoOpExporter = require('./no-op-exporter.js')
|
|
22
|
-
const normalizeTimestamp = require('./normalize-timestamp.js')
|
|
23
|
-
const severityToString = require('./severity-to-string.js')
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Sets up the OTEL logging system and instruments it in such a fashion
|
|
27
|
-
* that logs are shipped to New Relic when logs forwarding is enabled.
|
|
28
|
-
*
|
|
29
|
-
* The API, as of 2025-07, is in development. We should expect changes that
|
|
30
|
-
* will require significant refactoring here. For example, we might need to
|
|
31
|
-
* accept instances of various components, e.g. record processors, in order
|
|
32
|
-
* for customer apps to work as they expect.
|
|
33
|
-
*
|
|
34
|
-
* @param {object} params Factory function parameters.
|
|
35
|
-
* @param {Agent} params.agent The Node.js agent instance.
|
|
36
|
-
* @param {object} [params.logger] An agent logger instance.
|
|
37
|
-
*/
|
|
38
|
-
function bootstrapOtelLogs({ agent, logger = defaultLogger } = {}) {
|
|
39
|
-
if (isApplicationLoggingEnabled(agent.config) === false) {
|
|
40
|
-
logger.info('application logging disabled, skipping otel logs setup')
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
agent.metrics
|
|
45
|
-
.getOrCreateMetric('Supportability/Nodejs/OpenTelemetryBridge/Logs')
|
|
46
|
-
.incrementCallCount()
|
|
47
|
-
|
|
48
|
-
const exporter = new NoOpExporter()
|
|
49
|
-
const processor = new logsSdk.BatchLogRecordProcessor(exporter)
|
|
50
|
-
const otelProvider = new logsSdk.LoggerProvider({
|
|
51
|
-
processors: [processor]
|
|
52
|
-
})
|
|
53
|
-
const provider = new NewRelicLoggerProvider({
|
|
54
|
-
agent,
|
|
55
|
-
provider: otelProvider,
|
|
56
|
-
emitHandler: nrEmitHandler
|
|
57
|
-
})
|
|
58
|
-
logsApi.logs.setGlobalLoggerProvider(provider)
|
|
59
|
-
|
|
60
|
-
function nrEmitHandler(record) {
|
|
61
|
-
const level = severityToString(record.severityNumber ?? 0)
|
|
62
|
-
if (isMetricsEnabled(agent.config) === true) {
|
|
63
|
-
incrementLoggingLinesMetrics(level, agent.metrics)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// TODO: if we decide to support local decorating, implement it here
|
|
67
|
-
|
|
68
|
-
if (isLogForwardingEnabled(agent.config, agent) === true) {
|
|
69
|
-
const meta = agent.getLinkingMetadata(true)
|
|
70
|
-
const timestamp = normalizeTimestamp(record.timestamp)
|
|
71
|
-
const logData = {
|
|
72
|
-
message: record.body,
|
|
73
|
-
level,
|
|
74
|
-
timestamp,
|
|
75
|
-
...record.attributes,
|
|
76
|
-
...meta
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
agent.logs.add(logData)
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
agent.emit('otelLogsBootstrapped')
|
|
84
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
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 = bootstrapOtelMetrics
|
|
9
|
-
|
|
10
|
-
const { metrics } = require('@opentelemetry/api')
|
|
11
|
-
const { resourceFromAttributes } = require('@opentelemetry/resources')
|
|
12
|
-
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-proto')
|
|
13
|
-
const {
|
|
14
|
-
AggregationTemporality,
|
|
15
|
-
InMemoryMetricExporter,
|
|
16
|
-
MeterProvider,
|
|
17
|
-
PeriodicExportingMetricReader
|
|
18
|
-
} = require('@opentelemetry/sdk-metrics')
|
|
19
|
-
|
|
20
|
-
const ProxyingExporter = require('./proxying-exporter.js')
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Configures the OpenTelemetry metrics API client to send metrics data
|
|
24
|
-
* to New Relic.
|
|
25
|
-
*
|
|
26
|
-
* @param {Agent} agent The Node.js agent instance.
|
|
27
|
-
* @fires Agent#otelMetricsBootstrapped
|
|
28
|
-
*/
|
|
29
|
-
function bootstrapOtelMetrics(agent) {
|
|
30
|
-
const { config } = agent
|
|
31
|
-
const exportInterval = config.opentelemetry_bridge.metrics.exportInterval
|
|
32
|
-
const exportTimeout = config.opentelemetry_bridge.metrics.exportTimeout
|
|
33
|
-
|
|
34
|
-
const resource = resourceFromAttributes({ })
|
|
35
|
-
const memExporter = new InMemoryMetricExporter(AggregationTemporality.DELTA)
|
|
36
|
-
const proxyExporter = new ProxyingExporter({ exporter: memExporter })
|
|
37
|
-
const reader = new PeriodicExportingMetricReader({
|
|
38
|
-
exporter: proxyExporter,
|
|
39
|
-
exportIntervalMillis: exportInterval,
|
|
40
|
-
exportTimeoutMillis: exportTimeout
|
|
41
|
-
})
|
|
42
|
-
const provider = new MeterProvider({
|
|
43
|
-
readers: [reader],
|
|
44
|
-
resource
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
const getMeter = provider.getMeter
|
|
48
|
-
provider.getMeter = function nrGetMeter(...args) {
|
|
49
|
-
agent.metrics
|
|
50
|
-
.getOrCreateMetric('Supportability/Nodejs/OpenTelemetryBridge/Metrics/getMeter')
|
|
51
|
-
.incrementCallCount()
|
|
52
|
-
|
|
53
|
-
const meter = getMeter.apply(provider, args)
|
|
54
|
-
const proto = Object.getPrototypeOf(meter)
|
|
55
|
-
const methods = Object.getOwnPropertyNames(proto).filter((name) => name.startsWith('create'))
|
|
56
|
-
const originals = {}
|
|
57
|
-
for (const method of methods) {
|
|
58
|
-
originals[method] = meter[method]
|
|
59
|
-
// As of 2025-06-17:
|
|
60
|
-
// + createGauge
|
|
61
|
-
// + createHistogram
|
|
62
|
-
// + createCounter
|
|
63
|
-
// + createUpDownCounter
|
|
64
|
-
// + createObservableGauge
|
|
65
|
-
// + createObservableCounter
|
|
66
|
-
// + createObservableUpDownCounter
|
|
67
|
-
meter[method] = function nrWrappedMethod(...args) {
|
|
68
|
-
agent.metrics
|
|
69
|
-
.getOrCreateMetric(`Supportability/Nodejs/OpenTelemetryBridge/Metrics/meter/${method}`)
|
|
70
|
-
.incrementCallCount()
|
|
71
|
-
return originals[method].apply(meter, args)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return meter
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
metrics.setGlobalMeterProvider(provider)
|
|
79
|
-
|
|
80
|
-
agent.metrics
|
|
81
|
-
.getOrCreateMetric('Supportability/Nodejs/OpenTelemetryBridge/Metrics')
|
|
82
|
-
.incrementCallCount()
|
|
83
|
-
|
|
84
|
-
// We need access to `agent.config.entity_guid` in order to attach metrics
|
|
85
|
-
// to the correct instrumentation entity. But that value is not available
|
|
86
|
-
// until either at least the first firing of `agent.config.on('change')`, or
|
|
87
|
-
// the `agent.on('started')` event. Which means that we can't finalize the
|
|
88
|
-
// metrics client configuration until after the `started` event.
|
|
89
|
-
agent.on('started', postReady)
|
|
90
|
-
function postReady() {
|
|
91
|
-
agent.removeListener('started', postReady)
|
|
92
|
-
|
|
93
|
-
reader.collect().then(({ resourceMetrics: collectedMetrics }) => {
|
|
94
|
-
proxyExporter.exporter = new OTLPMetricExporter({
|
|
95
|
-
url: `https://${config.host}:${config.port}/v1/metrics`,
|
|
96
|
-
headers: {
|
|
97
|
-
'api-key': config.license_key
|
|
98
|
-
},
|
|
99
|
-
temporalityPreference: AggregationTemporality.DELTA
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
const resource = resourceFromAttributes({ 'entity.guid': config.entity_guid })
|
|
103
|
-
// Assigning the resource after having received the `entity.guid` from
|
|
104
|
-
// the server is a key detail of this implementation. Unfortunately,
|
|
105
|
-
// we don't have real public access to the object that retains the
|
|
106
|
-
// resource reference. If upstream ever hides this from us, we'll be
|
|
107
|
-
// in a bit of a bind.
|
|
108
|
-
provider._sharedState.resource = resource
|
|
109
|
-
|
|
110
|
-
// Attempt to ship any metrics recorded prior to the `started` event.
|
|
111
|
-
collectedMetrics.resource = resource
|
|
112
|
-
proxyExporter.exporter.export(collectedMetrics, () => {})
|
|
113
|
-
|
|
114
|
-
agent.emit('otelMetricsBootstrapped')
|
|
115
|
-
})
|
|
116
|
-
}
|
|
117
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2024 New Relic Corporation. All rights reserved.
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
'use strict'
|
|
7
|
-
|
|
8
|
-
const createConsumerSegment = require('./consumer')
|
|
9
|
-
const createDbSegment = require('./database')
|
|
10
|
-
const createHttpExternalSegment = require('./http-external')
|
|
11
|
-
const createProducerSegment = require('./producer')
|
|
12
|
-
const createServerSegment = require('./server')
|
|
13
|
-
const createInternalSegment = require('./internal')
|
|
14
|
-
|
|
15
|
-
module.exports = {
|
|
16
|
-
createConsumerSegment,
|
|
17
|
-
createDbSegment,
|
|
18
|
-
createHttpExternalSegment,
|
|
19
|
-
createInternalSegment,
|
|
20
|
-
createProducerSegment,
|
|
21
|
-
createServerSegment
|
|
22
|
-
}
|