newrelic 11.4.0 → 11.6.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 +80 -20
- package/README.md +3 -3
- package/THIRD_PARTY_NOTICES.md +115 -115
- package/api.js +108 -3
- package/lib/adaptive-sampler.js +1 -1
- package/lib/agent.js +2 -0
- package/lib/collector/remote-method.js +1 -1
- package/lib/config/default.js +14 -0
- package/lib/config/hsm.js +3 -0
- package/lib/instrumentation/@elastic/elasticsearch.js +2 -2
- package/lib/instrumentation/@nestjs/core.js +1 -1
- package/lib/instrumentation/@prisma/client.js +1 -1
- package/lib/instrumentation/cassandra-driver.js +1 -1
- package/lib/instrumentation/fastify.js +1 -1
- package/lib/instrumentation/grpc-js/grpc.js +1 -1
- package/lib/instrumentation/mongodb.js +1 -1
- package/lib/instrumentation/openai.js +329 -0
- package/lib/instrumentation/pino/pino.js +1 -1
- package/lib/instrumentations.js +1 -0
- package/lib/llm-events/openai/chat-completion-message.js +20 -0
- package/lib/llm-events/openai/chat-completion-summary.js +20 -0
- package/lib/llm-events/openai/embedding.js +15 -0
- package/lib/llm-events/openai/error-message.js +30 -0
- package/lib/llm-events/openai/event.js +56 -0
- package/lib/llm-events/openai/feedback-message.js +20 -0
- package/lib/llm-events/openai/index.js +20 -0
- package/lib/llm-events/tracked-ids.js +27 -0
- package/lib/logger.js +1 -1
- package/lib/metrics/names.js +6 -1
- package/lib/metrics/normalizer/rule.js +1 -1
- package/lib/metrics/normalizer.js +2 -2
- package/lib/shim/conglomerate-shim.js +3 -2
- package/lib/shim/datastore-shim.js +3 -2
- package/lib/shim/index.js +11 -8
- package/lib/shim/message-shim.js +3 -2
- package/lib/shim/promise-shim.js +4 -3
- package/lib/shim/shim.js +24 -22
- package/lib/shim/transaction-shim.js +7 -5
- package/lib/shim/webframework-shim.js +9 -5
- package/lib/shimmer.js +19 -13
- package/lib/spans/streaming-span-event-aggregator.js +1 -1
- package/lib/symbols.js +2 -0
- package/lib/timer.js +3 -3
- package/lib/transaction/index.js +5 -0
- package/lib/util/flatten.js +1 -1
- package/lib/util/objects.js +1 -1
- package/lib/util/process-version.js +2 -2
- package/lib/util/properties.js +2 -2
- package/lib/util/urltils.js +4 -4
- package/package.json +3 -3
package/lib/agent.js
CHANGED
|
@@ -197,6 +197,8 @@ function Agent(config) {
|
|
|
197
197
|
// Used by shutdown code as well as entity tracking stats
|
|
198
198
|
this.activeTransactions = 0
|
|
199
199
|
|
|
200
|
+
this.llm = {}
|
|
201
|
+
|
|
200
202
|
// Finally, add listeners for the agent's own events.
|
|
201
203
|
this.on('transactionFinished', this._transactionFinished.bind(this))
|
|
202
204
|
}
|
|
@@ -360,7 +360,7 @@ RemoteMethod.prototype._path = function _path() {
|
|
|
360
360
|
* @param {object} options
|
|
361
361
|
* @param {number} options.body - Data to be sent.
|
|
362
362
|
* @param {object} options.nrHeaders - NR request headers from the connect response.
|
|
363
|
-
* @param {
|
|
363
|
+
* @param {boolean} options.compressed - The compression method used, if any.
|
|
364
364
|
*/
|
|
365
365
|
RemoteMethod.prototype._headers = function _headers(options) {
|
|
366
366
|
const agent = this._userAgent()
|
package/lib/config/default.js
CHANGED
|
@@ -1345,6 +1345,20 @@ defaultConfig.definition = () => ({
|
|
|
1345
1345
|
formatter: boolean,
|
|
1346
1346
|
default: false
|
|
1347
1347
|
}
|
|
1348
|
+
},
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* When enabled, instrumentation of supported AI libraries will be in
|
|
1352
|
+
* effect.
|
|
1353
|
+
*/
|
|
1354
|
+
ai_monitoring: {
|
|
1355
|
+
/**
|
|
1356
|
+
* Toggles the generation of AI monitoring events by the agent.
|
|
1357
|
+
*/
|
|
1358
|
+
enabled: {
|
|
1359
|
+
formatter: boolean,
|
|
1360
|
+
default: false
|
|
1361
|
+
}
|
|
1348
1362
|
}
|
|
1349
1363
|
})
|
|
1350
1364
|
|
package/lib/config/hsm.js
CHANGED
|
@@ -19,7 +19,7 @@ const { isNotEmpty } = require('../../util/objects')
|
|
|
19
19
|
* @returns {void}
|
|
20
20
|
*/
|
|
21
21
|
module.exports = function initialize(_agent, elastic, _moduleName, shim) {
|
|
22
|
-
const pkgVersion = shim.
|
|
22
|
+
const pkgVersion = shim.pkgVersion
|
|
23
23
|
if (semver.lt(pkgVersion, '7.13.0')) {
|
|
24
24
|
shim &&
|
|
25
25
|
shim.logger.debug(
|
|
@@ -67,7 +67,7 @@ function queryParser(params) {
|
|
|
67
67
|
// let body or bulkBody override querystring, as some requests have both
|
|
68
68
|
if (isNotEmpty(params.body)) {
|
|
69
69
|
queryParam = params.body
|
|
70
|
-
} else if (
|
|
70
|
+
} else if (Array.isArray(params.bulkBody) && params.bulkBody.length) {
|
|
71
71
|
queryParam = params.bulkBody
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -8,7 +8,7 @@ const logger = require('../../logger').child({ component: 'nestjs' })
|
|
|
8
8
|
const semver = require('semver')
|
|
9
9
|
|
|
10
10
|
module.exports = function initialize(agent, core, moduleName, shim) {
|
|
11
|
-
const nestJsVersion = shim.
|
|
11
|
+
const nestJsVersion = shim.pkgVersion
|
|
12
12
|
shim.setFramework(shim.NEST)
|
|
13
13
|
// Earliest version that runs in the tests
|
|
14
14
|
if (semver.lt(nestJsVersion, '8.0.0')) {
|
|
@@ -154,7 +154,7 @@ function extractPrismaDatasource(client) {
|
|
|
154
154
|
* @param {object} shim New Relic shim
|
|
155
155
|
*/
|
|
156
156
|
module.exports = async function initialize(_agent, prisma, _moduleName, shim) {
|
|
157
|
-
const pkgVersion = shim.
|
|
157
|
+
const pkgVersion = shim.pkgVersion
|
|
158
158
|
if (semver.lt(pkgVersion, '4.0.0')) {
|
|
159
159
|
logger.warn(
|
|
160
160
|
'Skipping instrumentation of @prisma/client. Minimum supported version of library is 4.0.0, actual version %s',
|
|
@@ -17,7 +17,7 @@ const semver = require('semver')
|
|
|
17
17
|
* @param {object} shim - shim for instrumentation
|
|
18
18
|
*/
|
|
19
19
|
module.exports = function initialize(_agent, cassandra, _moduleName, shim) {
|
|
20
|
-
const cassandraVersion = shim.
|
|
20
|
+
const cassandraVersion = shim.pkgVersion
|
|
21
21
|
|
|
22
22
|
shim.setDatastore(shim.CASSANDRA)
|
|
23
23
|
|
|
@@ -86,7 +86,7 @@ const setupRouteHandler = (shim, fastify) => {
|
|
|
86
86
|
module.exports = function initialize(agent, fastify, moduleName, shim) {
|
|
87
87
|
shim.setFramework(shim.FASTIFY)
|
|
88
88
|
|
|
89
|
-
const fastifyVersion = shim.
|
|
89
|
+
const fastifyVersion = shim.pkgVersion
|
|
90
90
|
const isv3Plus = semver.satisfies(fastifyVersion, '>=3.0.0')
|
|
91
91
|
|
|
92
92
|
/**
|
|
@@ -20,7 +20,7 @@ module.exports.wrapStartCall = function wrappedClient(shim, callStream) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
module.exports.wrapServer = function wrapServer(shim, server) {
|
|
23
|
-
const grpcVersion = shim.
|
|
23
|
+
const grpcVersion = shim.pkgVersion
|
|
24
24
|
if (semver.lt(grpcVersion, '1.4.0')) {
|
|
25
25
|
shim.logger.debug('gRPC server-side instrumentation only supported on grpc-js >=1.4.0')
|
|
26
26
|
return
|
|
@@ -36,7 +36,7 @@ function initialize(agent, mongodb, moduleName, shim) {
|
|
|
36
36
|
|
|
37
37
|
shim.setDatastore(shim.MONGODB)
|
|
38
38
|
|
|
39
|
-
const mongoVersion = shim.
|
|
39
|
+
const mongoVersion = shim.pkgVersion
|
|
40
40
|
if (semver.satisfies(mongoVersion, '>=4.0.0')) {
|
|
41
41
|
instrumentV4(shim, mongodb)
|
|
42
42
|
} else if (semver.satisfies(mongoVersion, '>=3.0.6')) {
|
|
@@ -0,0 +1,329 @@
|
|
|
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 LlmTrackedIds = require('../../lib/llm-events/tracked-ids')
|
|
15
|
+
|
|
16
|
+
const MIN_VERSION = '4.0.0'
|
|
17
|
+
const MIN_STREAM_VERSION = '4.12.2'
|
|
18
|
+
const { AI } = require('../../lib/metrics/names')
|
|
19
|
+
const semver = require('semver')
|
|
20
|
+
|
|
21
|
+
let TRACKING_METRIC = AI.TRACKING_PREFIX
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Checks if we should skip instrumentation.
|
|
25
|
+
* Currently it checks if `ai_monitoring.enabled` is true
|
|
26
|
+
* and the package version >= 4.0.0
|
|
27
|
+
*
|
|
28
|
+
* @param {object} config agent config
|
|
29
|
+
* @param {Shim} shim instance of shim
|
|
30
|
+
* @returns {boolean} flag if instrumentation should be skipped
|
|
31
|
+
*/
|
|
32
|
+
function shouldSkipInstrumentation(config, shim) {
|
|
33
|
+
if (config?.ai_monitoring?.enabled !== true) {
|
|
34
|
+
shim.logger.debug('config.ai_monitoring.enabled is set to false.')
|
|
35
|
+
return true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { pkgVersion } = shim
|
|
39
|
+
return semver.lt(pkgVersion, MIN_VERSION)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
43
|
+
module.exports = function initialize(agent, openai, moduleName, shim) {
|
|
44
|
+
if (shouldSkipInstrumentation(agent.config, shim)) {
|
|
45
|
+
shim.logger.debug(
|
|
46
|
+
`${moduleName} instrumentation support is for versions >=${MIN_VERSION}. Skipping instrumentation.`
|
|
47
|
+
)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Update the tracking metric name with the version of the library
|
|
52
|
+
// being instrumented. We do not have access to the version when
|
|
53
|
+
// initially declaring the variable.
|
|
54
|
+
TRACKING_METRIC = `${TRACKING_METRIC}OpenAI/${shim.pkgVersion}`
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Adds apiKey and response headers to the active segment
|
|
58
|
+
* on symbols
|
|
59
|
+
*
|
|
60
|
+
* @param {object} result from openai request
|
|
61
|
+
* @param {string} apiKey api key from openai client
|
|
62
|
+
*/
|
|
63
|
+
function decorateSegment(result, apiKey) {
|
|
64
|
+
const segment = shim.getActiveSegment()
|
|
65
|
+
|
|
66
|
+
if (segment) {
|
|
67
|
+
segment[openAiApiKey] = apiKey
|
|
68
|
+
|
|
69
|
+
// If the result is an error, which is an OpenAI client error, then
|
|
70
|
+
// the headers are provided via a proxy attached to `result.headers`.
|
|
71
|
+
// Otherwise, result is a typical response-like object.
|
|
72
|
+
const headers = result?.response?.headers
|
|
73
|
+
? Object.fromEntries(result.response.headers)
|
|
74
|
+
: { ...result?.headers }
|
|
75
|
+
segment[openAiHeaders] = headers
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Enqueues a LLM event to the custom event aggregator
|
|
81
|
+
*
|
|
82
|
+
* @param {string} type of LLM event
|
|
83
|
+
* @param {object} msg LLM event
|
|
84
|
+
*/
|
|
85
|
+
function recordEvent(type, msg) {
|
|
86
|
+
agent.metrics.getOrCreateMetric(TRACKING_METRIC).incrementCallCount()
|
|
87
|
+
msg = agent?.llm?.metadata ? { ...agent.llm.metadata, ...msg } : msg
|
|
88
|
+
agent.customEventAggregator.add([{ type, timestamp: Date.now() }, msg])
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Instrumentation is only done to get the response headers and attach
|
|
93
|
+
* to the active segment as openai hides the headers from the functions we are
|
|
94
|
+
* trying to instrument
|
|
95
|
+
*/
|
|
96
|
+
shim.wrap(openai.prototype, 'makeRequest', function wrapRequest(shim, makeRequest) {
|
|
97
|
+
return function wrappedRequest() {
|
|
98
|
+
const apiKey = this.apiKey
|
|
99
|
+
const result = makeRequest.apply(this, arguments)
|
|
100
|
+
result.then(
|
|
101
|
+
(data) => {
|
|
102
|
+
// add headers on resolve
|
|
103
|
+
decorateSegment(data, apiKey)
|
|
104
|
+
},
|
|
105
|
+
(data) => {
|
|
106
|
+
// add headers on reject
|
|
107
|
+
decorateSegment(data, apiKey)
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
return result
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Assigns requestId, conversationId and messageIds for a given
|
|
116
|
+
* chat completion response on the active transaction.
|
|
117
|
+
* This is used for generating LlmFeedbackEvent via `api.recordLlmFeedbackEvent`
|
|
118
|
+
*
|
|
119
|
+
* @param {object} params input params
|
|
120
|
+
* @param {Transaction} params.tx active transaction
|
|
121
|
+
* @param {LlmChatCompletionMessage} params.completionMsg chat completion message
|
|
122
|
+
* @param {string} params.responseId id of response
|
|
123
|
+
*/
|
|
124
|
+
function assignIdsToTx({ tx, completionMsg, responseId }) {
|
|
125
|
+
const tracker = tx.llm.responses
|
|
126
|
+
const trackedIds =
|
|
127
|
+
tracker.get(responseId) ??
|
|
128
|
+
new LlmTrackedIds({
|
|
129
|
+
requestId: completionMsg.request_id,
|
|
130
|
+
conversationId: completionMsg.conversation_id
|
|
131
|
+
})
|
|
132
|
+
trackedIds.message_ids.push(completionMsg.id)
|
|
133
|
+
tracker.set(responseId, trackedIds)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Generates LlmChatCompletionSummary for a chat completion creation.
|
|
138
|
+
* Also iterates over both input messages and the first response message
|
|
139
|
+
* and creates LlmChatCompletionMessage.
|
|
140
|
+
*
|
|
141
|
+
* Also assigns relevant ids by response id for LlmFeedbackEvent creation
|
|
142
|
+
*
|
|
143
|
+
* @param {object} params input params
|
|
144
|
+
* @param {TraceSegment} params.segment active segment from chat completion
|
|
145
|
+
* @param {object} params.request chat completion params
|
|
146
|
+
* @param {object} params.response chat completion response
|
|
147
|
+
* @param {boolean} [params.err] err if it exists
|
|
148
|
+
*/
|
|
149
|
+
function recordChatCompletionMessages({ segment, request, response, err }) {
|
|
150
|
+
if (!response) {
|
|
151
|
+
// If we get an error, it is possible that `response = null`.
|
|
152
|
+
// In that case, we define it to be an empty object.
|
|
153
|
+
response = {}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
response.headers = segment[openAiHeaders]
|
|
157
|
+
response.api_key = segment[openAiApiKey]
|
|
158
|
+
const tx = segment.transaction
|
|
159
|
+
// explicitly end segment to consistent duration
|
|
160
|
+
// for both LLM events and the segment
|
|
161
|
+
segment.end()
|
|
162
|
+
const completionSummary = new LlmChatCompletionSummary({
|
|
163
|
+
agent,
|
|
164
|
+
segment,
|
|
165
|
+
request,
|
|
166
|
+
response,
|
|
167
|
+
withError: err != null
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// Only take the first response message and append to input messages
|
|
171
|
+
const messages = [...request.messages, response?.choices?.[0]?.message]
|
|
172
|
+
messages.forEach((message, index) => {
|
|
173
|
+
const completionMsg = new LlmChatCompletionMessage({
|
|
174
|
+
agent,
|
|
175
|
+
segment,
|
|
176
|
+
request,
|
|
177
|
+
response,
|
|
178
|
+
index,
|
|
179
|
+
completionId: completionSummary.id,
|
|
180
|
+
message
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
assignIdsToTx({ tx, completionMsg, responseId: response.id })
|
|
184
|
+
recordEvent('LlmChatCompletionMessage', completionMsg)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
recordEvent('LlmChatCompletionSummary', completionSummary)
|
|
188
|
+
|
|
189
|
+
if (err) {
|
|
190
|
+
const llmError = new LlmErrorMessage({ cause: err, summary: completionSummary, response })
|
|
191
|
+
shim.agent.errors.add(segment.transaction, err, llmError)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
delete response.headers
|
|
195
|
+
delete response.api_key
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/*
|
|
199
|
+
* Chat completions create can return a stream once promise resolves
|
|
200
|
+
* This wraps the iterator which is a generator function
|
|
201
|
+
* We will call the original iterator, intercept chunks and yield
|
|
202
|
+
* to the original. On complete we will construct the new message object
|
|
203
|
+
* with what we have seen in the stream and create the chat completion
|
|
204
|
+
* messages
|
|
205
|
+
*
|
|
206
|
+
*/
|
|
207
|
+
function instrumentStream({ request, response, segment }) {
|
|
208
|
+
shim.wrap(response, 'iterator', function wrapIterator(shim, orig) {
|
|
209
|
+
return async function* wrappedIterator() {
|
|
210
|
+
let content = ''
|
|
211
|
+
let role = ''
|
|
212
|
+
let chunk
|
|
213
|
+
let err
|
|
214
|
+
try {
|
|
215
|
+
const iterator = orig.apply(this, arguments)
|
|
216
|
+
|
|
217
|
+
for await (chunk of iterator) {
|
|
218
|
+
if (chunk.choices?.[0]?.delta?.role) {
|
|
219
|
+
role = chunk.choices[0].delta.role
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
content += chunk.choices?.[0]?.delta?.content ?? ''
|
|
223
|
+
yield chunk
|
|
224
|
+
}
|
|
225
|
+
} catch (streamErr) {
|
|
226
|
+
err = streamErr
|
|
227
|
+
throw err
|
|
228
|
+
} finally {
|
|
229
|
+
chunk.choices[0].message = { role, content }
|
|
230
|
+
// update segment duration since we want to extend the time it took to
|
|
231
|
+
// handle the stream
|
|
232
|
+
segment.touch()
|
|
233
|
+
|
|
234
|
+
recordChatCompletionMessages({
|
|
235
|
+
segment,
|
|
236
|
+
request,
|
|
237
|
+
response: chunk,
|
|
238
|
+
err
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Instruments chat completion creation
|
|
247
|
+
* and creates the LLM events
|
|
248
|
+
*
|
|
249
|
+
* **Note**: Currently only for promises. streams will come later
|
|
250
|
+
*/
|
|
251
|
+
shim.record(
|
|
252
|
+
openai.Chat.Completions.prototype,
|
|
253
|
+
'create',
|
|
254
|
+
function wrapCreate(shim, create, name, args) {
|
|
255
|
+
const [request] = args
|
|
256
|
+
if (request.stream && semver.lt(shim.pkgVersion, MIN_STREAM_VERSION)) {
|
|
257
|
+
shim.logger.warn(
|
|
258
|
+
`Instrumenting chat completion streams is only supported with openai version ${MIN_STREAM_VERSION}+.`
|
|
259
|
+
)
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
name: `${AI.OPEN_AI}/Chat/Completions/Create`,
|
|
265
|
+
promise: true,
|
|
266
|
+
// eslint-disable-next-line max-params
|
|
267
|
+
after(_shim, _fn, _name, err, response, segment) {
|
|
268
|
+
if (request.stream) {
|
|
269
|
+
instrumentStream({ request, response, segment })
|
|
270
|
+
} else {
|
|
271
|
+
recordChatCompletionMessages({
|
|
272
|
+
segment,
|
|
273
|
+
request,
|
|
274
|
+
response,
|
|
275
|
+
err
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Instruments embedding creation
|
|
285
|
+
* and creates LlmEmbedding event
|
|
286
|
+
*/
|
|
287
|
+
shim.record(
|
|
288
|
+
openai.Embeddings.prototype,
|
|
289
|
+
'create',
|
|
290
|
+
function wrapEmbeddingCreate(shim, embeddingCreate, name, args) {
|
|
291
|
+
const [request] = args
|
|
292
|
+
return {
|
|
293
|
+
name: `${AI.OPEN_AI}/Embeddings/Create`,
|
|
294
|
+
promise: true,
|
|
295
|
+
// eslint-disable-next-line max-params
|
|
296
|
+
after(_shim, _fn, _name, err, response, segment) {
|
|
297
|
+
if (!response) {
|
|
298
|
+
// If we get an error, it is possible that `response = null`.
|
|
299
|
+
// In that case, we define it to be an empty object.
|
|
300
|
+
response = {}
|
|
301
|
+
}
|
|
302
|
+
response.headers = segment[openAiHeaders]
|
|
303
|
+
response.api_key = segment[openAiApiKey]
|
|
304
|
+
// explicitly end segment to get consistent duration
|
|
305
|
+
// for both LLM events and the segment
|
|
306
|
+
segment.end()
|
|
307
|
+
const embedding = new LlmEmbedding({
|
|
308
|
+
agent,
|
|
309
|
+
segment,
|
|
310
|
+
request,
|
|
311
|
+
response,
|
|
312
|
+
withError: err != null
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
recordEvent('LlmEmbedding', embedding)
|
|
316
|
+
|
|
317
|
+
if (err) {
|
|
318
|
+
const llmError = new LlmErrorMessage({ cause: err, embedding, response })
|
|
319
|
+
shim.agent.errors.add(segment.transaction, err, llmError)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// cleanup keys on response before returning to user code
|
|
323
|
+
delete response.api_key
|
|
324
|
+
delete response.headers
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
}
|
|
@@ -16,7 +16,7 @@ const {
|
|
|
16
16
|
const semver = require('semver')
|
|
17
17
|
|
|
18
18
|
module.exports = function instrument(shim, tools) {
|
|
19
|
-
const pinoVersion = shim.
|
|
19
|
+
const pinoVersion = shim.pkgVersion
|
|
20
20
|
|
|
21
21
|
if (semver.lt(pinoVersion, '7.0.0')) {
|
|
22
22
|
shim.logger.debug('Instrumentation only supported on pino >=7.0.0.')
|
package/lib/instrumentations.js
CHANGED
|
@@ -28,6 +28,7 @@ module.exports = function instrumentations() {
|
|
|
28
28
|
'memcached': { type: MODULE_TYPE.DATASTORE },
|
|
29
29
|
'mongodb': { type: MODULE_TYPE.DATASTORE },
|
|
30
30
|
'mysql': { module: './instrumentation/mysql' },
|
|
31
|
+
'openai': { type: MODULE_TYPE.GENERIC },
|
|
31
32
|
'@nestjs/core': { type: MODULE_TYPE.WEB_FRAMEWORK },
|
|
32
33
|
'pino': { module: './instrumentation/pino' },
|
|
33
34
|
'pg': { type: MODULE_TYPE.DATASTORE },
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
const LlmEvent = require('./event')
|
|
8
|
+
|
|
9
|
+
module.exports = class LlmChatCompletionMessage extends LlmEvent {
|
|
10
|
+
constructor({ agent, segment, request = {}, response = {}, index = 0, message, completionId }) {
|
|
11
|
+
super({ agent, segment, request, response })
|
|
12
|
+
this.id = `${response.id}-${index}`
|
|
13
|
+
this.conversation_id = this.conversationId(agent)
|
|
14
|
+
this.content = message?.content
|
|
15
|
+
this.role = message?.role
|
|
16
|
+
this.sequence = index
|
|
17
|
+
this.completion_id = completionId
|
|
18
|
+
this.is_response = response?.choices?.[0]?.message?.content === this.content
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
const LlmEvent = require('./event')
|
|
8
|
+
|
|
9
|
+
module.exports = class LlmChatCompletionSummary extends LlmEvent {
|
|
10
|
+
constructor({ agent, segment, request = {}, response = {}, withError = false }) {
|
|
11
|
+
super({ agent, segment, request, response, responseAttrs: true })
|
|
12
|
+
this.error = withError
|
|
13
|
+
this.conversation_id = this.conversationId(agent)
|
|
14
|
+
this['request.max_tokens'] = request.max_tokens
|
|
15
|
+
this['request.temperature'] = request.temperature
|
|
16
|
+
this['response.number_of_messages'] = request?.messages?.length + response?.choices?.length
|
|
17
|
+
this['response.usage.completion_tokens'] = response?.usage?.completion_tokens
|
|
18
|
+
this['response.choices.finish_reason'] = response?.choices?.[0]?.finish_reason
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
const LlmEvent = require('./event')
|
|
8
|
+
|
|
9
|
+
module.exports = class LlmEmbedding extends LlmEvent {
|
|
10
|
+
constructor({ agent, segment, request = {}, response = {}, withError = false }) {
|
|
11
|
+
super({ agent, segment, request, response, responseAttrs: true })
|
|
12
|
+
this.error = withError
|
|
13
|
+
this.input = request.input?.toString()
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Represents an error object, to be tracked via `agent.errors`, that is the
|
|
10
|
+
* result of some error returned from the OpenAI API.
|
|
11
|
+
*/
|
|
12
|
+
module.exports = class LlmErrorMessage {
|
|
13
|
+
/**
|
|
14
|
+
* @param {object} params Constructor parameters
|
|
15
|
+
* @param {object} [params.response] Instance of an incoming message.
|
|
16
|
+
* @param {object} [params.cause] An instance of the OpenAI error object.
|
|
17
|
+
* @param {LlmChatCompletionSummary} [params.summary] Details about the
|
|
18
|
+
* conversation if it was a chat completion conversation.
|
|
19
|
+
* @param {LlmEmbedding} [params.embedding] Details about the conversation
|
|
20
|
+
* if it was an embedding conversation.
|
|
21
|
+
*/
|
|
22
|
+
constructor({ response, cause, summary, embedding } = {}) {
|
|
23
|
+
this['http.statusCode'] = response?.status ?? cause?.status
|
|
24
|
+
this['error.message'] = cause?.message
|
|
25
|
+
this['error.code'] = response?.code ?? cause?.error?.code
|
|
26
|
+
this['error.param'] = response?.param ?? cause?.error?.param
|
|
27
|
+
this.completion_id = summary?.id
|
|
28
|
+
this.embedding_id = embedding?.id
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
const { makeId } = require('../../util/hashes')
|
|
8
|
+
const { DESTINATIONS } = require('../../../lib/config/attribute-filter')
|
|
9
|
+
|
|
10
|
+
module.exports = class LlmEvent {
|
|
11
|
+
constructor({ agent, segment, request, response, responseAttrs = false }) {
|
|
12
|
+
this.id = makeId(36)
|
|
13
|
+
this.appName = agent.config.applications()[0]
|
|
14
|
+
this.request_id = response?.headers?.['x-request-id']
|
|
15
|
+
this.trace_id = segment?.transaction?.traceId
|
|
16
|
+
this.span_id = segment?.id
|
|
17
|
+
this.transaction_id = segment?.transaction?.id
|
|
18
|
+
this['response.model'] = response.model
|
|
19
|
+
this.vendor = 'openAI'
|
|
20
|
+
this.ingest_source = 'Node'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Used in embedding, and chat completion summary.
|
|
24
|
+
* The flag will include response attributes but also
|
|
25
|
+
* other attributes from request like model, and api key.
|
|
26
|
+
* Lastly, it includes the active span's duration.
|
|
27
|
+
*/
|
|
28
|
+
if (responseAttrs) {
|
|
29
|
+
this['request.model'] = request.model || request.engine
|
|
30
|
+
this.duration = segment?.getDurationInMillis()
|
|
31
|
+
this.api_key_last_four_digits = response?.api_key && `sk-${response.api_key.slice(-4)}`
|
|
32
|
+
this.responseAttrs(response)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
responseAttrs(response) {
|
|
37
|
+
this['response.organization'] = response?.headers?.['openai-organization']
|
|
38
|
+
this['response.usage.total_tokens'] = response?.usage?.total_tokens
|
|
39
|
+
this['response.usage.prompt_tokens'] = response?.usage?.prompt_tokens
|
|
40
|
+
this['response.headers.llmVersion'] = response?.headers?.['openai-version']
|
|
41
|
+
this['response.headers.ratelimitLimitRequests'] =
|
|
42
|
+
response?.headers?.['x-ratelimit-limit-requests']
|
|
43
|
+
this['response.headers.ratelimitLimitTokens'] = response?.headers?.['x-ratelimit-limit-tokens']
|
|
44
|
+
this['response.headers.ratelimitResetTokens'] = response?.headers?.['x-ratelimit-reset-tokens']
|
|
45
|
+
this['response.headers.ratelimitRemainingTokens'] =
|
|
46
|
+
response?.headers?.['x-ratelimit-remaining-tokens']
|
|
47
|
+
this['response.headers.ratelimitRemainingRequests'] =
|
|
48
|
+
response?.headers?.['x-ratelimit-remaining-requests']
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
conversationId(agent) {
|
|
52
|
+
const transaction = agent.tracer.getTransaction()
|
|
53
|
+
const attrs = transaction?.trace?.custom.get(DESTINATIONS.TRANS_SCOPE)
|
|
54
|
+
return attrs?.conversation_id
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
const { makeId } = require('../../util/hashes')
|
|
8
|
+
|
|
9
|
+
module.exports = class LlmFeedbackMessage {
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
this.id = makeId(32)
|
|
12
|
+
this.conversation_id = opts.conversationId
|
|
13
|
+
this.request_id = opts.requestId
|
|
14
|
+
this.message_id = opts.messageId
|
|
15
|
+
this.category = opts.category
|
|
16
|
+
this.rating = opts.rating
|
|
17
|
+
this.message = opts.message
|
|
18
|
+
this.ingest_source = 'Node'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const LlmChatCompletionSummary = require('./chat-completion-summary')
|
|
9
|
+
const LlmChatCompletionMessage = require('./chat-completion-message')
|
|
10
|
+
const LlmEmbedding = require('./embedding')
|
|
11
|
+
const LlmFeedbackMessage = require('./feedback-message')
|
|
12
|
+
const LlmErrorMessage = require('./error-message')
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
LlmChatCompletionMessage,
|
|
16
|
+
LlmChatCompletionSummary,
|
|
17
|
+
LlmEmbedding,
|
|
18
|
+
LlmErrorMessage,
|
|
19
|
+
LlmFeedbackMessage
|
|
20
|
+
}
|