newrelic 11.6.0 → 11.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NEWS.md +21 -0
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/lib/instrumentation/openai.js +189 -181
- package/lib/instrumentation/pg.js +43 -16
- package/lib/instrumentation/winston.js +6 -1
- package/lib/metrics/names.js +8 -1
- package/lib/shimmer.js +56 -4
- package/lib/symbols.js +6 -1
- package/package.json +2 -2
package/NEWS.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
### v11.6.1 (2023-12-07)
|
|
2
|
+
|
|
3
|
+
#### Bug fixes
|
|
4
|
+
|
|
5
|
+
* **deps:** Updated @newrelic/aws-sdk to 7.0.3 ([#1902](https://github.com/newrelic/node-newrelic/pull/1902)) ([bf22ae5](https://github.com/newrelic/node-newrelic/commit/bf22ae502ee74d5071dc5578cc77c38039e68778))
|
|
6
|
+
* Updated shimmer to handle instrumenting named and default exports of CommonJS modules in ESM ([#1894](https://github.com/newrelic/node-newrelic/pull/1894)) ([9f7979c](https://github.com/newrelic/node-newrelic/commit/9f7979cd41dcb3735e553cbd4837ad455836e3ff))
|
|
7
|
+
|
|
8
|
+
#### Code refactoring
|
|
9
|
+
|
|
10
|
+
* Fixed cognitive complexity violations of openai instrumentation by moving functions outside of the parent function scope ([#1895](https://github.com/newrelic/node-newrelic/pull/1895)) ([31dc717](https://github.com/newrelic/node-newrelic/commit/31dc71797c4798793b1bba8ca15733482ba3e7d6))
|
|
11
|
+
* Updated span names of openai actions to allow AI O11y team to locate relevant spans ([#1896](https://github.com/newrelic/node-newrelic/pull/1896)) ([b7a644d](https://github.com/newrelic/node-newrelic/commit/b7a644d1ef56bf88171fb74868ae78d24745dd19))
|
|
12
|
+
|
|
13
|
+
#### Miscellaneous chores
|
|
14
|
+
|
|
15
|
+
* Improve OpenAI mock server streams ([#1890](https://github.com/newrelic/node-newrelic/pull/1890)) ([d12bfe4](https://github.com/newrelic/node-newrelic/commit/d12bfe45f2c8d01277a1f8186844f0dd3598cbd7))
|
|
16
|
+
|
|
17
|
+
#### Tests
|
|
18
|
+
|
|
19
|
+
* Fix winston-esm versioned tests ([#1898](https://github.com/newrelic/node-newrelic/pull/1898)) ([6e02e57](https://github.com/newrelic/node-newrelic/commit/6e02e57a2b4ed8351e39e5fb8d47e2702bcafde0))
|
|
20
|
+
* pin undici versioned to <6.0.0 on Node 16 ([#1900](https://github.com/newrelic/node-newrelic/pull/1900)) ([9973b24](https://github.com/newrelic/node-newrelic/commit/9973b24067211f5fcc06adae79e02df8f599d735))
|
|
21
|
+
|
|
1
22
|
### v11.6.0 (2023-11-29)
|
|
2
23
|
|
|
3
24
|
#### Features
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -510,7 +510,7 @@ This product includes source derived from [@grpc/proto-loader](https://github.co
|
|
|
510
510
|
|
|
511
511
|
### @newrelic/aws-sdk
|
|
512
512
|
|
|
513
|
-
This product includes source derived from [@newrelic/aws-sdk](https://github.com/newrelic/node-newrelic-aws-sdk) ([v7.0.
|
|
513
|
+
This product includes source derived from [@newrelic/aws-sdk](https://github.com/newrelic/node-newrelic-aws-sdk) ([v7.0.3](https://github.com/newrelic/node-newrelic-aws-sdk/tree/v7.0.3)), distributed under the [Apache-2.0 License](https://github.com/newrelic/node-newrelic-aws-sdk/blob/v7.0.3/LICENSE):
|
|
514
514
|
|
|
515
515
|
```
|
|
516
516
|
Apache License
|
|
@@ -15,10 +15,12 @@ const LlmTrackedIds = require('../../lib/llm-events/tracked-ids')
|
|
|
15
15
|
|
|
16
16
|
const MIN_VERSION = '4.0.0'
|
|
17
17
|
const MIN_STREAM_VERSION = '4.12.2'
|
|
18
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
AI: { OPENAI }
|
|
20
|
+
} = require('../../lib/metrics/names')
|
|
19
21
|
const semver = require('semver')
|
|
20
22
|
|
|
21
|
-
let TRACKING_METRIC =
|
|
23
|
+
let TRACKING_METRIC = OPENAI.TRACKING_PREFIX
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* Checks if we should skip instrumentation.
|
|
@@ -39,7 +41,178 @@ function shouldSkipInstrumentation(config, shim) {
|
|
|
39
41
|
return semver.lt(pkgVersion, MIN_VERSION)
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Adds apiKey and response headers to the active segment
|
|
46
|
+
* on symbols
|
|
47
|
+
*
|
|
48
|
+
* @param {object} params input params
|
|
49
|
+
* @param {Shim} params.shim instance of shim
|
|
50
|
+
* @param {object} params.result from openai request
|
|
51
|
+
* @param {string} params.apiKey api key from openai client
|
|
52
|
+
*/
|
|
53
|
+
function decorateSegment({ shim, result, apiKey }) {
|
|
54
|
+
const segment = shim.getActiveSegment()
|
|
55
|
+
|
|
56
|
+
if (segment) {
|
|
57
|
+
segment[openAiApiKey] = apiKey
|
|
58
|
+
|
|
59
|
+
// If the result is an error, which is an OpenAI client error, then
|
|
60
|
+
// the headers are provided via a proxy attached to `result.headers`.
|
|
61
|
+
// Otherwise, result is a typical response-like object.
|
|
62
|
+
const headers = result?.response?.headers
|
|
63
|
+
? Object.fromEntries(result.response.headers)
|
|
64
|
+
: { ...result?.headers }
|
|
65
|
+
segment[openAiHeaders] = headers
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Enqueues a LLM event to the custom event aggregator
|
|
71
|
+
*
|
|
72
|
+
* @param {object} params input params
|
|
73
|
+
* @param {Agent} params.agent NR agent instance
|
|
74
|
+
* @param {string} params.type LLM event type
|
|
75
|
+
* @param {object} params.msg LLM event
|
|
76
|
+
*/
|
|
77
|
+
function recordEvent({ agent, type, msg }) {
|
|
78
|
+
agent.metrics.getOrCreateMetric(TRACKING_METRIC).incrementCallCount()
|
|
79
|
+
msg = agent?.llm?.metadata ? { ...agent.llm.metadata, ...msg } : msg
|
|
80
|
+
agent.customEventAggregator.add([{ type, timestamp: Date.now() }, msg])
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Assigns requestId, conversationId and messageIds for a given
|
|
85
|
+
* chat completion response on the active transaction.
|
|
86
|
+
* This is used for generating LlmFeedbackEvent via `api.recordLlmFeedbackEvent`
|
|
87
|
+
*
|
|
88
|
+
* @param {object} params input params
|
|
89
|
+
* @param {Transaction} params.tx active transaction
|
|
90
|
+
* @param {LlmChatCompletionMessage} params.completionMsg chat completion message
|
|
91
|
+
* @param {string} params.responseId id of response
|
|
92
|
+
*/
|
|
93
|
+
function assignIdsToTx({ tx, completionMsg, responseId }) {
|
|
94
|
+
const tracker = tx.llm.responses
|
|
95
|
+
const trackedIds =
|
|
96
|
+
tracker.get(responseId) ??
|
|
97
|
+
new LlmTrackedIds({
|
|
98
|
+
requestId: completionMsg.request_id,
|
|
99
|
+
conversationId: completionMsg.conversation_id
|
|
100
|
+
})
|
|
101
|
+
trackedIds.message_ids.push(completionMsg.id)
|
|
102
|
+
tracker.set(responseId, trackedIds)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generates LlmChatCompletionSummary for a chat completion creation.
|
|
107
|
+
* Also iterates over both input messages and the first response message
|
|
108
|
+
* and creates LlmChatCompletionMessage.
|
|
109
|
+
*
|
|
110
|
+
* Also assigns relevant ids by response id for LlmFeedbackEvent creation
|
|
111
|
+
*
|
|
112
|
+
* @param {object} params input params
|
|
113
|
+
* @param {Agent} params.agent NR agent instance
|
|
114
|
+
* @param {TraceSegment} params.segment active segment from chat completion
|
|
115
|
+
* @param {object} params.request chat completion params
|
|
116
|
+
* @param {object} params.response chat completion response
|
|
117
|
+
* @param {boolean} [params.err] err if it exists
|
|
118
|
+
*/
|
|
119
|
+
function recordChatCompletionMessages({ agent, segment, request, response, err }) {
|
|
120
|
+
if (!response) {
|
|
121
|
+
// If we get an error, it is possible that `response = null`.
|
|
122
|
+
// In that case, we define it to be an empty object.
|
|
123
|
+
response = {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
response.headers = segment[openAiHeaders]
|
|
127
|
+
response.api_key = segment[openAiApiKey]
|
|
128
|
+
const tx = segment.transaction
|
|
129
|
+
// explicitly end segment to consistent duration
|
|
130
|
+
// for both LLM events and the segment
|
|
131
|
+
segment.end()
|
|
132
|
+
const completionSummary = new LlmChatCompletionSummary({
|
|
133
|
+
agent,
|
|
134
|
+
segment,
|
|
135
|
+
request,
|
|
136
|
+
response,
|
|
137
|
+
withError: err != null
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// Only take the first response message and append to input messages
|
|
141
|
+
const messages = [...request.messages, response?.choices?.[0]?.message]
|
|
142
|
+
messages.forEach((message, index) => {
|
|
143
|
+
const completionMsg = new LlmChatCompletionMessage({
|
|
144
|
+
agent,
|
|
145
|
+
segment,
|
|
146
|
+
request,
|
|
147
|
+
response,
|
|
148
|
+
index,
|
|
149
|
+
completionId: completionSummary.id,
|
|
150
|
+
message
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
assignIdsToTx({ tx, completionMsg, responseId: response.id })
|
|
154
|
+
recordEvent({ agent, type: 'LlmChatCompletionMessage', msg: completionMsg })
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
recordEvent({ agent, type: 'LlmChatCompletionSummary', msg: completionSummary })
|
|
158
|
+
|
|
159
|
+
if (err) {
|
|
160
|
+
const llmError = new LlmErrorMessage({ cause: err, summary: completionSummary, response })
|
|
161
|
+
agent.errors.add(segment.transaction, err, llmError)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
delete response.headers
|
|
165
|
+
delete response.api_key
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/*
|
|
169
|
+
* Chat completions create can return a stream once promise resolves
|
|
170
|
+
* This wraps the iterator which is a generator function
|
|
171
|
+
* We will call the original iterator, intercept chunks and yield
|
|
172
|
+
* to the original. On complete we will construct the new message object
|
|
173
|
+
* with what we have seen in the stream and create the chat completion
|
|
174
|
+
* messages
|
|
175
|
+
*
|
|
176
|
+
*/
|
|
177
|
+
function instrumentStream({ shim, request, response, segment }) {
|
|
178
|
+
shim.wrap(response, 'iterator', function wrapIterator(shim, orig) {
|
|
179
|
+
return async function* wrappedIterator() {
|
|
180
|
+
let content = ''
|
|
181
|
+
let role = ''
|
|
182
|
+
let chunk
|
|
183
|
+
let err
|
|
184
|
+
try {
|
|
185
|
+
const iterator = orig.apply(this, arguments)
|
|
186
|
+
|
|
187
|
+
for await (chunk of iterator) {
|
|
188
|
+
if (chunk.choices?.[0]?.delta?.role) {
|
|
189
|
+
role = chunk.choices[0].delta.role
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
content += chunk.choices?.[0]?.delta?.content ?? ''
|
|
193
|
+
yield chunk
|
|
194
|
+
}
|
|
195
|
+
} catch (streamErr) {
|
|
196
|
+
err = streamErr
|
|
197
|
+
throw err
|
|
198
|
+
} finally {
|
|
199
|
+
chunk.choices[0].message = { role, content }
|
|
200
|
+
// update segment duration since we want to extend the time it took to
|
|
201
|
+
// handle the stream
|
|
202
|
+
segment.touch()
|
|
203
|
+
|
|
204
|
+
recordChatCompletionMessages({
|
|
205
|
+
agent: shim.agent,
|
|
206
|
+
segment,
|
|
207
|
+
request,
|
|
208
|
+
response: chunk,
|
|
209
|
+
err
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
43
216
|
module.exports = function initialize(agent, openai, moduleName, shim) {
|
|
44
217
|
if (shouldSkipInstrumentation(agent.config, shim)) {
|
|
45
218
|
shim.logger.debug(
|
|
@@ -51,42 +224,7 @@ module.exports = function initialize(agent, openai, moduleName, shim) {
|
|
|
51
224
|
// Update the tracking metric name with the version of the library
|
|
52
225
|
// being instrumented. We do not have access to the version when
|
|
53
226
|
// initially declaring the variable.
|
|
54
|
-
TRACKING_METRIC = `${TRACKING_METRIC}
|
|
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
|
-
}
|
|
227
|
+
TRACKING_METRIC = `${TRACKING_METRIC}/${shim.pkgVersion}`
|
|
90
228
|
|
|
91
229
|
/**
|
|
92
230
|
* Instrumentation is only done to get the response headers and attach
|
|
@@ -96,152 +234,21 @@ module.exports = function initialize(agent, openai, moduleName, shim) {
|
|
|
96
234
|
shim.wrap(openai.prototype, 'makeRequest', function wrapRequest(shim, makeRequest) {
|
|
97
235
|
return function wrappedRequest() {
|
|
98
236
|
const apiKey = this.apiKey
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
(
|
|
237
|
+
const request = makeRequest.apply(this, arguments)
|
|
238
|
+
request.then(
|
|
239
|
+
(result) => {
|
|
102
240
|
// add headers on resolve
|
|
103
|
-
decorateSegment(
|
|
241
|
+
decorateSegment({ shim, result, apiKey })
|
|
104
242
|
},
|
|
105
|
-
(
|
|
243
|
+
(result) => {
|
|
106
244
|
// add headers on reject
|
|
107
|
-
decorateSegment(
|
|
245
|
+
decorateSegment({ shim, result, apiKey })
|
|
108
246
|
}
|
|
109
247
|
)
|
|
110
|
-
return
|
|
248
|
+
return request
|
|
111
249
|
}
|
|
112
250
|
})
|
|
113
251
|
|
|
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
252
|
/**
|
|
246
253
|
* Instruments chat completion creation
|
|
247
254
|
* and creates the LLM events
|
|
@@ -261,14 +268,15 @@ module.exports = function initialize(agent, openai, moduleName, shim) {
|
|
|
261
268
|
}
|
|
262
269
|
|
|
263
270
|
return {
|
|
264
|
-
name:
|
|
271
|
+
name: OPENAI.COMPLETION,
|
|
265
272
|
promise: true,
|
|
266
273
|
// eslint-disable-next-line max-params
|
|
267
274
|
after(_shim, _fn, _name, err, response, segment) {
|
|
268
275
|
if (request.stream) {
|
|
269
|
-
instrumentStream({ request, response, segment })
|
|
276
|
+
instrumentStream({ shim, request, response, segment })
|
|
270
277
|
} else {
|
|
271
278
|
recordChatCompletionMessages({
|
|
279
|
+
agent,
|
|
272
280
|
segment,
|
|
273
281
|
request,
|
|
274
282
|
response,
|
|
@@ -290,7 +298,7 @@ module.exports = function initialize(agent, openai, moduleName, shim) {
|
|
|
290
298
|
function wrapEmbeddingCreate(shim, embeddingCreate, name, args) {
|
|
291
299
|
const [request] = args
|
|
292
300
|
return {
|
|
293
|
-
name:
|
|
301
|
+
name: OPENAI.EMBEDDING,
|
|
294
302
|
promise: true,
|
|
295
303
|
// eslint-disable-next-line max-params
|
|
296
304
|
after(_shim, _fn, _name, err, response, segment) {
|
|
@@ -312,7 +320,7 @@ module.exports = function initialize(agent, openai, moduleName, shim) {
|
|
|
312
320
|
withError: err != null
|
|
313
321
|
})
|
|
314
322
|
|
|
315
|
-
recordEvent('LlmEmbedding', embedding)
|
|
323
|
+
recordEvent({ agent, type: 'LlmEmbedding', msg: embedding })
|
|
316
324
|
|
|
317
325
|
if (err) {
|
|
318
326
|
const llmError = new LlmErrorMessage({ cause: err, embedding, response })
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict'
|
|
7
7
|
|
|
8
|
+
const { nrEsmProxy } = require('../symbols')
|
|
9
|
+
|
|
8
10
|
function getQuery(shim, original, name, args) {
|
|
9
11
|
const config = args[0]
|
|
10
12
|
let statement
|
|
@@ -78,27 +80,52 @@ module.exports = function initialize(agent, pgsql, moduleName, shim) {
|
|
|
78
80
|
})
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
|
|
82
|
-
// (only when called). We replace the getter, so that we can instrument the native
|
|
83
|
-
// client. The original getter replaces itself with the instance of the native
|
|
84
|
-
// client, so only instrument if the getter exists (otherwise assume already
|
|
85
|
-
// instrumented).
|
|
86
|
-
const origGetter = pgsql.__lookupGetter__('native')
|
|
87
|
-
if (origGetter) {
|
|
88
|
-
delete pgsql.native
|
|
89
|
-
pgsql.__defineGetter__('native', function getNative() {
|
|
90
|
-
const temp = origGetter()
|
|
91
|
-
if (temp != null) {
|
|
92
|
-
instrumentPGNative(temp)
|
|
93
|
-
}
|
|
94
|
-
return temp
|
|
95
|
-
})
|
|
96
|
-
}
|
|
83
|
+
updateNative(pgsql, instrumentPGNative)
|
|
97
84
|
|
|
98
85
|
// wrapping for JS
|
|
99
86
|
shim.recordQuery(pgsql && pgsql.Client && pgsql.Client.prototype, 'query', wrapJSClientQuery)
|
|
100
87
|
}
|
|
101
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Determines if the `pg` module is a plain CJS module or a CJS module that
|
|
91
|
+
* has been imported via ESM's import. After making the determination, it will
|
|
92
|
+
* update the `native` export of the module to be instrumented.
|
|
93
|
+
*
|
|
94
|
+
* @param {object} pg The module to inspect.
|
|
95
|
+
* @param {Function} instrumentPGNative A function that will apply instrumentation
|
|
96
|
+
* to the `native` export.
|
|
97
|
+
*/
|
|
98
|
+
function updateNative(pg, instrumentPGNative) {
|
|
99
|
+
if (pg[nrEsmProxy] === true) {
|
|
100
|
+
// When pg is imported via an ESM import statement, then our proxy will
|
|
101
|
+
// make our non-ESM native getter wrapper not work correctly. Basically,
|
|
102
|
+
// the getter will get evaluated by the proxy, and we never gain access to
|
|
103
|
+
// replace the getter with our own implementation. Luckily, we get to
|
|
104
|
+
// simplify in this scenario.
|
|
105
|
+
const native = pg.default.native
|
|
106
|
+
if (native !== null) {
|
|
107
|
+
instrumentPGNative(native)
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
// The pg module defines a "native" getter which sets up the native client lazily
|
|
111
|
+
// (only when called). We replace the getter, so that we can instrument the native
|
|
112
|
+
// client. The original getter replaces itself with the instance of the native
|
|
113
|
+
// client, so only instrument if the getter exists (otherwise assume already
|
|
114
|
+
// instrumented).
|
|
115
|
+
const origGetter = pg.__lookupGetter__('native')
|
|
116
|
+
if (origGetter) {
|
|
117
|
+
delete pg.native
|
|
118
|
+
pg.__defineGetter__('native', function getNative() {
|
|
119
|
+
const temp = origGetter()
|
|
120
|
+
if (temp != null) {
|
|
121
|
+
instrumentPGNative(temp)
|
|
122
|
+
}
|
|
123
|
+
return temp
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
102
129
|
function getInstanceParameters(shim, client) {
|
|
103
130
|
return {
|
|
104
131
|
host: client.host || null,
|
|
@@ -101,7 +101,12 @@ function registerFormatter({ opts, agent, winston }) {
|
|
|
101
101
|
if (opts.format) {
|
|
102
102
|
opts.format = winston.format.combine(instrumentedFormatter(), opts.format)
|
|
103
103
|
} else {
|
|
104
|
-
|
|
104
|
+
// The default formatter for Winston is the JSON formatter. If the user
|
|
105
|
+
// has not provided a formatter through opts.format, we must emulate the
|
|
106
|
+
// default. Otherwise, the message symbol will not get attached to log
|
|
107
|
+
// messages and transports, e.g. the "Console" transport, will not be able
|
|
108
|
+
// to output logs correctly.
|
|
109
|
+
opts.format = winston.format.combine(instrumentedFormatter(), winston.format.json())
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
112
|
|
package/lib/metrics/names.js
CHANGED
|
@@ -166,7 +166,14 @@ const EXPRESS = {
|
|
|
166
166
|
|
|
167
167
|
const AI = {
|
|
168
168
|
TRACKING_PREFIX: 'Nodejs/ML/',
|
|
169
|
-
|
|
169
|
+
EMBEDDING: 'Llm/embedding',
|
|
170
|
+
COMPLETION: 'Llm/completion'
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
AI.OPENAI = {
|
|
174
|
+
TRACKING_PREFIX: `${AI.TRACKING_PREFIX}/OpenAI`,
|
|
175
|
+
EMBEDDING: `${AI.EMBEDDING}/OpenAI/create`,
|
|
176
|
+
COMPLETION: `${AI.COMPLETION}/OpenAI/create`
|
|
170
177
|
}
|
|
171
178
|
|
|
172
179
|
const RESTIFY = {
|
package/lib/shimmer.js
CHANGED
|
@@ -13,6 +13,7 @@ const INSTRUMENTATIONS = require('./instrumentations')()
|
|
|
13
13
|
const shims = require('./shim')
|
|
14
14
|
const { Hook } = require('require-in-the-middle')
|
|
15
15
|
const IitmHook = require('import-in-the-middle')
|
|
16
|
+
const { nrEsmProxy } = require('./symbols')
|
|
16
17
|
let pkgsToHook = []
|
|
17
18
|
|
|
18
19
|
const NAMES = require('./metrics/names')
|
|
@@ -537,10 +538,7 @@ function instrumentPostLoad(agent, nodule, moduleName, resolvedName, esmResolver
|
|
|
537
538
|
return
|
|
538
539
|
}
|
|
539
540
|
|
|
540
|
-
|
|
541
|
-
// callback hook
|
|
542
|
-
const resolvedNodule = instrumentation.isEsm || !esmResolver ? nodule : nodule.default
|
|
543
|
-
|
|
541
|
+
const resolvedNodule = resolveNodule({ nodule, instrumentation, esmResolver })
|
|
544
542
|
const shim = shims.createShimFromType({
|
|
545
543
|
type: instrumentation.type,
|
|
546
544
|
agent,
|
|
@@ -579,6 +577,60 @@ function instrumentPostLoad(agent, nodule, moduleName, resolvedName, esmResolver
|
|
|
579
577
|
return nodule
|
|
580
578
|
}
|
|
581
579
|
|
|
580
|
+
/**
|
|
581
|
+
* If a module that is being shimmed was loaded via a typical `require` then
|
|
582
|
+
* we can use it as normal. If was loaded via an ESM import, then we need to
|
|
583
|
+
* handle it with some extra care. This function inspects the module,
|
|
584
|
+
* the conditions around its loading, and returns an appropriate object for
|
|
585
|
+
* subsequent shimming methods.
|
|
586
|
+
*
|
|
587
|
+
* @param {object} params Input parameters.
|
|
588
|
+
* @param {object} params.nodule The nodule being instrumented.
|
|
589
|
+
* @param {object} params.instrumentation The configuration for the nodule
|
|
590
|
+
* to be instrumented.
|
|
591
|
+
* @param {boolean|null} params.esmResolver Indicates if the nodule was loaded
|
|
592
|
+
* via an ESM import.
|
|
593
|
+
* @returns {object} The nodule or a Proxy.
|
|
594
|
+
*/
|
|
595
|
+
function resolveNodule({ nodule, instrumentation, esmResolver }) {
|
|
596
|
+
if (instrumentation.isEsm === true || !esmResolver) {
|
|
597
|
+
return nodule
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// We have a CJS module wrapped by import-in-the-middle having been
|
|
601
|
+
// imported through ESM syntax. Due to the way CJS modules are parsed by
|
|
602
|
+
// ESM's import, we can have the same "export" attached to the `default`
|
|
603
|
+
// export and as a top-level named export. In order to shim things such
|
|
604
|
+
// that our users don't need to know to access `something.default.foo`
|
|
605
|
+
// when they have done `import * as something from 'something'`, we need
|
|
606
|
+
// to proxy the proxy in order to set our wrappers on both instances.
|
|
607
|
+
const noduleDefault = nodule.default
|
|
608
|
+
const origNodule = nodule
|
|
609
|
+
return new Proxy(
|
|
610
|
+
{ origNodule, noduleDefault },
|
|
611
|
+
{
|
|
612
|
+
get(target, name) {
|
|
613
|
+
if (name === nrEsmProxy) {
|
|
614
|
+
return true
|
|
615
|
+
}
|
|
616
|
+
if (target.noduleDefault[name]) {
|
|
617
|
+
return target.noduleDefault[name]
|
|
618
|
+
}
|
|
619
|
+
return target.origNodule[name]
|
|
620
|
+
},
|
|
621
|
+
set(target, name, value) {
|
|
622
|
+
if (target.origNodule[name]) {
|
|
623
|
+
target.origNodule[name] = value
|
|
624
|
+
}
|
|
625
|
+
if (target.noduleDefault[name]) {
|
|
626
|
+
target.noduleDefault[name] = value
|
|
627
|
+
}
|
|
628
|
+
return true
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
)
|
|
632
|
+
}
|
|
633
|
+
|
|
582
634
|
/**
|
|
583
635
|
* Attempts to execute an onRequire hook for a given module.
|
|
584
636
|
* If it fails it will call an onError hook and log warnings accordingly
|
package/lib/symbols.js
CHANGED
|
@@ -34,5 +34,10 @@ module.exports = {
|
|
|
34
34
|
unwrapPool: Symbol('unwrapPool'),
|
|
35
35
|
clusterOf: Symbol('clusterOf'),
|
|
36
36
|
createPoolCluster: Symbol('createPoolCluster'),
|
|
37
|
-
wrappedPoolConnection: Symbol('wrappedPoolConnection')
|
|
37
|
+
wrappedPoolConnection: Symbol('wrappedPoolConnection'),
|
|
38
|
+
|
|
39
|
+
// Used to "decorate" the proxy returned during ESM importing so that
|
|
40
|
+
// instrumentations can determine if the module looks like an ESM module
|
|
41
|
+
// or not.
|
|
42
|
+
nrEsmProxy: Symbol('nr-esm-proxy')
|
|
38
43
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "newrelic",
|
|
3
|
-
"version": "11.6.
|
|
3
|
+
"version": "11.6.1",
|
|
4
4
|
"author": "New Relic Node.js agent team <nodejs@newrelic.com>",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"contributors": [
|
|
@@ -183,7 +183,7 @@
|
|
|
183
183
|
"dependencies": {
|
|
184
184
|
"@grpc/grpc-js": "^1.9.4",
|
|
185
185
|
"@grpc/proto-loader": "^0.7.5",
|
|
186
|
-
"@newrelic/aws-sdk": "^7.0.
|
|
186
|
+
"@newrelic/aws-sdk": "^7.0.3",
|
|
187
187
|
"@newrelic/koa": "^8.0.1",
|
|
188
188
|
"@newrelic/security-agent": "0.5.0",
|
|
189
189
|
"@newrelic/superagent": "^7.0.1",
|