newrelic 12.20.0 → 12.21.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 +21 -0
- package/lib/collector/facts.js +1 -5
- package/lib/config/default.js +36 -1
- package/lib/config/index.js +13 -4
- package/lib/feature_flags.js +10 -10
- package/lib/instrumentation/@google/genai.js +289 -0
- package/lib/instrumentation/openai.js +27 -25
- package/lib/instrumentations.js +1 -0
- package/lib/llm-events/error-message.js +17 -2
- package/lib/llm-events/google-genai/chat-completion-message.js +46 -0
- package/lib/llm-events/google-genai/chat-completion-summary.js +24 -0
- package/lib/llm-events/google-genai/embedding.js +25 -0
- package/lib/llm-events/google-genai/event.js +29 -0
- package/lib/llm-events/google-genai/index.js +18 -0
- package/lib/metrics/names.js +6 -0
- package/lib/otel/attr-reconciler.js +7 -9
- package/lib/otel/exception-mapping.js +25 -0
- package/lib/otel/fake-span.js +1 -1
- package/lib/otel/rules.js +55 -26
- package/lib/otel/segment-synthesis.js +6 -6
- package/lib/otel/segments/consumer.js +7 -20
- package/lib/otel/segments/database.js +40 -75
- package/lib/otel/segments/http-external.js +25 -25
- package/lib/otel/segments/internal.js +2 -2
- package/lib/otel/segments/producer.js +5 -15
- package/lib/otel/segments/server.js +6 -49
- package/lib/otel/setup.js +22 -6
- package/lib/otel/span-processor.js +144 -116
- package/lib/otel/transformation-rules.json +1228 -0
- package/lib/otel/utils.js +162 -0
- package/lib/shim/datastore-shim.js +1 -1
- package/lib/transaction/tracer/index.js +1 -1
- package/package.json +1 -1
- package/lib/otel/attr-mapping/db.js +0 -83
- package/lib/otel/attr-mapping/exceptions.js +0 -23
- package/lib/otel/attr-mapping/faas.js +0 -25
- package/lib/otel/attr-mapping/http.js +0 -193
- package/lib/otel/attr-mapping/messaging.js +0 -105
- package/lib/otel/attr-mapping/utils.js +0 -75
- package/lib/otel/rules.json +0 -500
package/NEWS.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
### v12.21.0 (2025-06-04)
|
|
2
|
+
|
|
3
|
+
#### Features
|
|
4
|
+
|
|
5
|
+
* Added `@google/genai` Gemini AI LLM instrumentation ([#3119](https://github.com/newrelic/node-newrelic/pull/3119)) ([7f28a29](https://github.com/newrelic/node-newrelic/commit/7f28a293fbd623464ed1f3f809de59eaea0ff367))
|
|
6
|
+
* Updated opentelemetry bridge to rely on configuration based transformation rules ([#3125](https://github.com/newrelic/node-newrelic/pull/3125)) ([ad116d2](https://github.com/newrelic/node-newrelic/commit/ad116d2aa49818c2824bca606a22c48abecf9580))
|
|
7
|
+
|
|
8
|
+
#### Bug fixes
|
|
9
|
+
|
|
10
|
+
* Refactored getHostnameSafe to accommodate GCP ([#3132](https://github.com/newrelic/node-newrelic/pull/3132)) ([f1eea8d](https://github.com/newrelic/node-newrelic/commit/f1eea8d076586cd0b6c4d671c261deefe5e0f027))
|
|
11
|
+
* Updated openai instrumentation to work with 5.0.0+ ([#3129](https://github.com/newrelic/node-newrelic/pull/3129)) ([09146a9](https://github.com/newrelic/node-newrelic/commit/09146a9d550c0cdbb23ec54325be6f8343db8b78))
|
|
12
|
+
* Updated rules to work with 1.17.0 and 1.24.0 for consumer and producer span kind ([#3128](https://github.com/newrelic/node-newrelic/pull/3128)) ([686c747](https://github.com/newrelic/node-newrelic/commit/686c747cedc2582061608c20402f11912092d5b5))
|
|
13
|
+
|
|
14
|
+
#### Documentation
|
|
15
|
+
|
|
16
|
+
* Updated compatibility report ([#3124](https://github.com/newrelic/node-newrelic/pull/3124)) ([c51d45f](https://github.com/newrelic/node-newrelic/commit/c51d45f4fc3c4fd56bdaa80c1028a83c7f4aadd2)) ([#3117](https://github.com/newrelic/node-newrelic/pull/3117)) ([084e599](https://github.com/newrelic/node-newrelic/commit/084e599428ac9c21e6991e27b5c154db2830ce17))
|
|
17
|
+
|
|
18
|
+
#### Miscellaneous chores
|
|
19
|
+
|
|
20
|
+
* Added preliminary otel configuration ([#3121](https://github.com/newrelic/node-newrelic/pull/3121)) ([664ee4a](https://github.com/newrelic/node-newrelic/commit/664ee4ae956d9cc6ebc6b68ad79a47948510422f))
|
|
21
|
+
|
|
1
22
|
### v12.20.0 (2025-05-27)
|
|
2
23
|
|
|
3
24
|
#### Features
|
package/lib/collector/facts.js
CHANGED
|
@@ -31,11 +31,7 @@ async function facts(agent, callback, { logger = defaultLogger } = {}) {
|
|
|
31
31
|
systemInfo = systemInfo || Object.create(null)
|
|
32
32
|
environment = environment || []
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
if (agent.config.gcp_cloud_run.use_instance_as_host && process.env.K_SERVICE) {
|
|
36
|
-
// K_SERVICE is the name of the service in GCP Cloud Run
|
|
37
|
-
hostname = systemInfo.vendors?.gcp?.id
|
|
38
|
-
}
|
|
34
|
+
const hostname = agent.config.getHostnameSafe(systemInfo?.vendors?.gcp?.id)
|
|
39
35
|
const results = {
|
|
40
36
|
utilization: {
|
|
41
37
|
metadata_version: 5,
|
package/lib/config/default.js
CHANGED
|
@@ -1673,7 +1673,42 @@ defaultConfig.definition = () => ({
|
|
|
1673
1673
|
* **Note**: Disabling a given library may affect the instrumentation of libraries used after
|
|
1674
1674
|
* the disabled library. Use at your own risk.
|
|
1675
1675
|
*/
|
|
1676
|
-
instrumentation: pkgInstrumentation
|
|
1676
|
+
instrumentation: pkgInstrumentation,
|
|
1677
|
+
|
|
1678
|
+
/**
|
|
1679
|
+
* Governs the various OpenTelemetry based features provided by the
|
|
1680
|
+
* agent.
|
|
1681
|
+
*
|
|
1682
|
+
* NOTICE: this configuration is subject to change while the OTEL
|
|
1683
|
+
* feature set is in development.
|
|
1684
|
+
*/
|
|
1685
|
+
opentelemetry_bridge: {
|
|
1686
|
+
/**
|
|
1687
|
+
* Global switch for the whole OpenTelemetry feature. If it is set to
|
|
1688
|
+
* `false`, any other sub-feature, e.g. `traces`, will not be enabled
|
|
1689
|
+
* regardless of that specific sub-feature setting.
|
|
1690
|
+
*/
|
|
1691
|
+
enabled: { default: false, formatter: boolean },
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* `traces` are instrumentations, e.g. `@fastify/otel`. Enabling `traces`
|
|
1695
|
+
* enables bridging OpenTelemetry instrumentations into the New Relic
|
|
1696
|
+
* agent.
|
|
1697
|
+
*/
|
|
1698
|
+
traces: {
|
|
1699
|
+
enabled: { default: false, formatter: boolean }
|
|
1700
|
+
},
|
|
1701
|
+
|
|
1702
|
+
/**
|
|
1703
|
+
* `metrics` governs automatic configuration of the OpenTelemetry
|
|
1704
|
+
* metrics API. When `true`, the agent will automatically configure the
|
|
1705
|
+
* metrics API to send metrics to New Relic and attach them to the
|
|
1706
|
+
* application entity that is instrumented by the New Relic agent.
|
|
1707
|
+
*/
|
|
1708
|
+
metrics: {
|
|
1709
|
+
enabled: { default: false, formatter: boolean }
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1677
1712
|
})
|
|
1678
1713
|
|
|
1679
1714
|
/**
|
package/lib/config/index.js
CHANGED
|
@@ -972,16 +972,25 @@ Config.prototype.getIPAddresses = function getIPAddresses() {
|
|
|
972
972
|
return addresses
|
|
973
973
|
}
|
|
974
974
|
|
|
975
|
-
|
|
975
|
+
/**
|
|
976
|
+
* Gets the system's host name. If that fails, it just returns ipv4/6
|
|
977
|
+
* based on the user's process_host.ipv_preference setting.
|
|
978
|
+
* @param {string} gcpId GCP metadata ID
|
|
979
|
+
* @returns {string} host name
|
|
980
|
+
*/
|
|
981
|
+
function getHostnameSafe(gcpId = null) {
|
|
976
982
|
let _hostname
|
|
977
983
|
const config = this
|
|
978
984
|
this.getHostnameSafe = function getCachedHostname() {
|
|
979
985
|
return _hostname
|
|
980
986
|
}
|
|
981
987
|
try {
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
988
|
+
// Check for any special hostname scenarios.
|
|
989
|
+
// If not applicable, use the os.hostname()
|
|
990
|
+
if (config.heroku.use_dyno_names && process.env.DYNO) {
|
|
991
|
+
_hostname = process.env.DYNO || os.hostname()
|
|
992
|
+
} else if (config.gcp_cloud_run.use_instance_as_host && process.env.K_SERVICE && gcpId) {
|
|
993
|
+
_hostname = gcpId
|
|
985
994
|
} else {
|
|
986
995
|
_hostname = os.hostname()
|
|
987
996
|
}
|
package/lib/feature_flags.js
CHANGED
|
@@ -10,7 +10,6 @@ exports.prerelease = {
|
|
|
10
10
|
// internal_test_only is used for testing our feature flag implementation.
|
|
11
11
|
// It is not used to gate any features.
|
|
12
12
|
internal_test_only: false,
|
|
13
|
-
opentelemetry_bridge: false,
|
|
14
13
|
promise_segments: false,
|
|
15
14
|
reverse_naming_rules: false,
|
|
16
15
|
unresolved_promise_cleanup: true,
|
|
@@ -20,26 +19,27 @@ exports.prerelease = {
|
|
|
20
19
|
|
|
21
20
|
// flags that are no longer used for released features
|
|
22
21
|
exports.released = [
|
|
23
|
-
'
|
|
22
|
+
'async_local_context',
|
|
23
|
+
'await_support',
|
|
24
|
+
'aws_bedrock_instrumentation',
|
|
24
25
|
'cat',
|
|
26
|
+
'certificate_bundle',
|
|
25
27
|
'custom_instrumentation',
|
|
26
28
|
'custom_metrics',
|
|
29
|
+
'dt_format_w3c',
|
|
27
30
|
'express_segments',
|
|
31
|
+
'fastify_instrumentation',
|
|
32
|
+
'langchain_instrumentation',
|
|
28
33
|
'native_metrics',
|
|
29
34
|
'new_promise_tracking',
|
|
35
|
+
'opentelemetry_bridge',
|
|
30
36
|
'protocol_17',
|
|
31
|
-
'
|
|
37
|
+
'released',
|
|
32
38
|
'send_request_uri_attribute',
|
|
39
|
+
'serverless_mode',
|
|
33
40
|
'synthetics',
|
|
34
|
-
'dt_format_w3c',
|
|
35
|
-
'fastify_instrumentation',
|
|
36
|
-
'await_support',
|
|
37
|
-
'certificate_bundle',
|
|
38
|
-
'async_local_context',
|
|
39
41
|
'undici_async_tracking',
|
|
40
42
|
'undici_instrumentation',
|
|
41
|
-
'aws_bedrock_instrumentation',
|
|
42
|
-
'langchain_instrumentation'
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
// flags that are no longer used for unreleased features
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
LlmChatCompletionMessage,
|
|
10
|
+
LlmChatCompletionSummary,
|
|
11
|
+
LlmEmbedding,
|
|
12
|
+
LlmErrorMessage
|
|
13
|
+
} = require('../../../lib/llm-events/google-genai')
|
|
14
|
+
const { RecorderSpec } = require('../../../lib/shim/specs')
|
|
15
|
+
const { extractLlmContext } = require('../../util/llm-utils')
|
|
16
|
+
|
|
17
|
+
const { AI } = require('../../../lib/metrics/names')
|
|
18
|
+
const { GEMINI } = AI
|
|
19
|
+
const { DESTINATIONS } = require('../../config/attribute-filter')
|
|
20
|
+
let TRACKING_METRIC = GEMINI.TRACKING_PREFIX
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Enqueues a LLM event to the custom event aggregator
|
|
24
|
+
*
|
|
25
|
+
* @param {object} params input params
|
|
26
|
+
* @param {Agent} params.agent NR agent instance
|
|
27
|
+
* @param {string} params.type LLM event type
|
|
28
|
+
* @param {object} params.msg LLM event
|
|
29
|
+
*/
|
|
30
|
+
function recordEvent({ agent, type, msg }) {
|
|
31
|
+
const llmContext = extractLlmContext(agent)
|
|
32
|
+
|
|
33
|
+
agent.customEventAggregator.add([
|
|
34
|
+
{ type, timestamp: Date.now() },
|
|
35
|
+
Object.assign({}, msg, llmContext)
|
|
36
|
+
])
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Increments the tracking metric and sets the llm attribute on transactions
|
|
41
|
+
*
|
|
42
|
+
* @param {object} params input params
|
|
43
|
+
* @param {Agent} params.agent NR agent instance
|
|
44
|
+
* @param {Transaction} params.transaction active transaction
|
|
45
|
+
*/
|
|
46
|
+
function addLlmMeta({ agent, transaction }) {
|
|
47
|
+
agent.metrics.getOrCreateMetric(TRACKING_METRIC).incrementCallCount()
|
|
48
|
+
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generates LlmChatCompletionSummary for a chat completion creation.
|
|
53
|
+
* Also iterates over both input messages and the first response message
|
|
54
|
+
* and creates LlmChatCompletionMessage.
|
|
55
|
+
*
|
|
56
|
+
* Also assigns relevant ids by response id for LlmFeedbackEvent creation
|
|
57
|
+
*
|
|
58
|
+
* @param {object} params input params
|
|
59
|
+
* @param {Agent} params.agent NR agent instance
|
|
60
|
+
* @param {Shim} params.shim the current shim instance
|
|
61
|
+
* @param {TraceSegment} params.segment active segment from chat completion
|
|
62
|
+
* @param {object} params.request chat completion params
|
|
63
|
+
* @param {object} params.response chat completion response
|
|
64
|
+
* @param {boolean} [params.err] err if it exists
|
|
65
|
+
* @param {Transaction} params.transaction active transaction
|
|
66
|
+
*/
|
|
67
|
+
function recordChatCompletionMessages({
|
|
68
|
+
agent,
|
|
69
|
+
shim,
|
|
70
|
+
segment,
|
|
71
|
+
request,
|
|
72
|
+
response,
|
|
73
|
+
err,
|
|
74
|
+
transaction
|
|
75
|
+
}) {
|
|
76
|
+
if (!response) {
|
|
77
|
+
// If we get an error, it is possible that `response = null`.
|
|
78
|
+
// In that case, we define it to be an empty object.
|
|
79
|
+
response = {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Explicitly end segment to provide consistent duration
|
|
83
|
+
// for both LLM events and the segment
|
|
84
|
+
segment.end()
|
|
85
|
+
const completionSummary = new LlmChatCompletionSummary({
|
|
86
|
+
agent,
|
|
87
|
+
segment,
|
|
88
|
+
transaction,
|
|
89
|
+
request,
|
|
90
|
+
response,
|
|
91
|
+
withError: err != null
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// Only take the first response message and append to input messages
|
|
95
|
+
// request.contents can be a string or an array of strings
|
|
96
|
+
// response.candidates is an array of candidates (choices); we only take the first one
|
|
97
|
+
const inputMessages = Array.isArray(request.contents) ? request.contents : [request.contents]
|
|
98
|
+
const responseMessage = response?.candidates?.[0]?.content
|
|
99
|
+
const messages = responseMessage !== undefined ? [...inputMessages, responseMessage] : inputMessages
|
|
100
|
+
messages.forEach((message, index) => {
|
|
101
|
+
const completionMsg = new LlmChatCompletionMessage({
|
|
102
|
+
agent,
|
|
103
|
+
segment,
|
|
104
|
+
transaction,
|
|
105
|
+
request,
|
|
106
|
+
response,
|
|
107
|
+
index,
|
|
108
|
+
completionId: completionSummary.id,
|
|
109
|
+
message
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
recordEvent({ agent, type: 'LlmChatCompletionMessage', msg: completionMsg })
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
recordEvent({ agent, type: 'LlmChatCompletionSummary', msg: completionSummary })
|
|
116
|
+
|
|
117
|
+
if (err) {
|
|
118
|
+
const llmError = new LlmErrorMessage({ cause: err, summary: completionSummary, response })
|
|
119
|
+
agent.errors.add(transaction, err, llmError)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function instrumentStream ({ agent, shim, request, response, segment, transaction }) {
|
|
124
|
+
let err
|
|
125
|
+
let content
|
|
126
|
+
let modelVersion
|
|
127
|
+
let finishReason
|
|
128
|
+
let entireMessage = ''
|
|
129
|
+
shim.wrap(response, 'next', function wrapNext(shim, originalNext) {
|
|
130
|
+
return async function wrappedNext(...nextArgs) {
|
|
131
|
+
let result = {}
|
|
132
|
+
try {
|
|
133
|
+
result = await originalNext.apply(response, nextArgs)
|
|
134
|
+
if (result?.value?.text) {
|
|
135
|
+
modelVersion = result.value.modelVersion
|
|
136
|
+
content = result.value.candidates[0].content
|
|
137
|
+
entireMessage += result.value.text // readonly variable that equates to result.value.candidates[0].content.parts[0].text
|
|
138
|
+
}
|
|
139
|
+
if (result?.value?.candidates?.[0]?.finishReason) {
|
|
140
|
+
finishReason = result.value.candidates[0].finishReason
|
|
141
|
+
}
|
|
142
|
+
} catch (streamErr) {
|
|
143
|
+
err = streamErr
|
|
144
|
+
throw err
|
|
145
|
+
} finally {
|
|
146
|
+
// Update segment duration since we want to extend the
|
|
147
|
+
// time it took to handle the stream
|
|
148
|
+
segment.touch()
|
|
149
|
+
|
|
150
|
+
// result will be {value: undefined, done: true}
|
|
151
|
+
// when the stream is done, so we need to create
|
|
152
|
+
// a mock GenerateContentResponse object with
|
|
153
|
+
// the entire message
|
|
154
|
+
//
|
|
155
|
+
// also need to enter this block if there was an
|
|
156
|
+
// error, so we can record it
|
|
157
|
+
if (result?.done || err) {
|
|
158
|
+
if (content) {
|
|
159
|
+
content.parts[0].text = entireMessage
|
|
160
|
+
result.value = {
|
|
161
|
+
candidates: [
|
|
162
|
+
{ content, finishReason }
|
|
163
|
+
],
|
|
164
|
+
modelVersion
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
recordChatCompletionMessages({
|
|
169
|
+
agent: shim.agent,
|
|
170
|
+
shim,
|
|
171
|
+
segment,
|
|
172
|
+
transaction,
|
|
173
|
+
request,
|
|
174
|
+
response: result?.value,
|
|
175
|
+
err
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return result
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = function initialize(agent, googleGenAi, moduleName, shim) {
|
|
185
|
+
if (agent?.config?.ai_monitoring?.enabled !== true) {
|
|
186
|
+
shim.logger.debug('config.ai_monitoring.enabled is set to false.')
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Update the tracking metric name with the version of the library
|
|
191
|
+
// being instrumented. We do not have access to the version when
|
|
192
|
+
// initially declaring the variable.
|
|
193
|
+
TRACKING_METRIC = `${TRACKING_METRIC}/${shim.pkgVersion}`
|
|
194
|
+
|
|
195
|
+
const models = googleGenAi.Models
|
|
196
|
+
|
|
197
|
+
// Instruments chat completion creation
|
|
198
|
+
// and creates the LLM events
|
|
199
|
+
shim.record(models.prototype, 'generateContentInternal',
|
|
200
|
+
function wrapGenerateContent(shim, func, name, args) {
|
|
201
|
+
const [request] = args
|
|
202
|
+
|
|
203
|
+
return new RecorderSpec({
|
|
204
|
+
name: GEMINI.COMPLETION,
|
|
205
|
+
promise: true,
|
|
206
|
+
after({ error: err, result: response, segment, transaction }) {
|
|
207
|
+
recordChatCompletionMessages({
|
|
208
|
+
agent,
|
|
209
|
+
shim,
|
|
210
|
+
segment,
|
|
211
|
+
transaction,
|
|
212
|
+
request,
|
|
213
|
+
response,
|
|
214
|
+
err
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
addLlmMeta({ agent, transaction })
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
// Instruments chat completion streaming
|
|
224
|
+
// and creates the LLM events
|
|
225
|
+
shim.record(models.prototype, 'generateContentStreamInternal',
|
|
226
|
+
function wrapGenerateContentStream(shim, func, name, args) {
|
|
227
|
+
if (!agent.config.ai_monitoring.streaming.enabled) {
|
|
228
|
+
shim.logger.warn(
|
|
229
|
+
'`ai_monitoring.streaming.enabled` is set to `false`, stream will not be instrumented.'
|
|
230
|
+
)
|
|
231
|
+
agent.metrics.getOrCreateMetric(AI.STREAMING_DISABLED).incrementCallCount()
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
const [request] = args
|
|
235
|
+
|
|
236
|
+
return new RecorderSpec({
|
|
237
|
+
name: GEMINI.COMPLETION,
|
|
238
|
+
promise: true,
|
|
239
|
+
after({ result: response, segment, transaction }) {
|
|
240
|
+
instrumentStream({ agent, shim, request, response, segment, transaction })
|
|
241
|
+
addLlmMeta({ agent, transaction })
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// Instruments embedding creation
|
|
247
|
+
// and creates LlmEmbedding event
|
|
248
|
+
shim.record(
|
|
249
|
+
models.prototype,
|
|
250
|
+
'embedContent',
|
|
251
|
+
function wrapEmbedContent(shim, func, name, args) {
|
|
252
|
+
const [request] = args
|
|
253
|
+
|
|
254
|
+
return new RecorderSpec({
|
|
255
|
+
name: GEMINI.EMBEDDING,
|
|
256
|
+
promise: true,
|
|
257
|
+
after({ error: err, result: response, segment, transaction }) {
|
|
258
|
+
addLlmMeta({ agent, transaction })
|
|
259
|
+
|
|
260
|
+
if (!response) {
|
|
261
|
+
// If we get an error, it is possible that `response = null`.
|
|
262
|
+
// In that case, we define it to be an empty object.
|
|
263
|
+
response = {}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Explicitly end segment to get consistent duration
|
|
267
|
+
// for both LLM events and the segment
|
|
268
|
+
segment.end()
|
|
269
|
+
|
|
270
|
+
const embedding = new LlmEmbedding({
|
|
271
|
+
agent,
|
|
272
|
+
segment,
|
|
273
|
+
transaction,
|
|
274
|
+
request,
|
|
275
|
+
response,
|
|
276
|
+
withError: err != null
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
recordEvent({ agent, type: 'LlmEmbedding', msg: embedding })
|
|
280
|
+
|
|
281
|
+
if (err) {
|
|
282
|
+
const llmError = new LlmErrorMessage({ cause: err, embedding, response })
|
|
283
|
+
shim.agent.errors.add(transaction, err, llmError)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
)
|
|
289
|
+
}
|
|
@@ -25,7 +25,7 @@ let TRACKING_METRIC = OPENAI.TRACKING_PREFIX
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Checks if we should skip instrumentation.
|
|
28
|
-
* Currently it checks if `ai_monitoring.enabled` is true
|
|
28
|
+
* Currently, it checks if `ai_monitoring.enabled` is true
|
|
29
29
|
* and the package version >= 4.0.0
|
|
30
30
|
*
|
|
31
31
|
* @param {object} config agent config
|
|
@@ -172,14 +172,20 @@ function recordChatCompletionMessages({
|
|
|
172
172
|
delete response.headers
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
*
|
|
177
|
-
* This wraps the iterator which is a generator function
|
|
175
|
+
/**
|
|
176
|
+
* `chat.completions.create` can return a stream once promise resolves.
|
|
177
|
+
* This wraps the iterator which is a generator function.
|
|
178
178
|
* We will call the original iterator, intercept chunks and yield
|
|
179
|
-
* to the original. On complete we will construct the new message object
|
|
179
|
+
* to the original. On complete, we will construct the new message object
|
|
180
180
|
* with what we have seen in the stream and create the chat completion
|
|
181
|
-
* messages
|
|
182
|
-
*
|
|
181
|
+
* messages.
|
|
182
|
+
* @param {object} params input params
|
|
183
|
+
* @param {Agent} params.agent NR agent instance
|
|
184
|
+
* @param {Shim} params.shim the current shim instance
|
|
185
|
+
* @param {object} params.request chat completion params
|
|
186
|
+
* @param {object} params.response chat completion response
|
|
187
|
+
* @param {TraceSegment} params.segment active segment from chat completion
|
|
188
|
+
* @param {Transaction} params.transaction active transaction
|
|
183
189
|
*/
|
|
184
190
|
function instrumentStream({ agent, shim, request, response, segment, transaction }) {
|
|
185
191
|
if (!agent.config.ai_monitoring.streaming.enabled) {
|
|
@@ -243,12 +249,13 @@ module.exports = function initialize(agent, openai, moduleName, shim) {
|
|
|
243
249
|
// initially declaring the variable.
|
|
244
250
|
TRACKING_METRIC = `${TRACKING_METRIC}/${shim.pkgVersion}`
|
|
245
251
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
+
// openai@^5.0.0 uses a different prototype structure
|
|
253
|
+
const openaiClient = semver.gte(shim.pkgVersion, '5.0.0') ? openai.OpenAI : openai
|
|
254
|
+
|
|
255
|
+
// Instrumentation is only done to get the response headers and attach
|
|
256
|
+
// to the active segment as openai hides the headers from the functions we are
|
|
257
|
+
// trying to instrument
|
|
258
|
+
shim.wrap(openaiClient.prototype, 'makeRequest', function wrapRequest(shim, makeRequest) {
|
|
252
259
|
return function wrappedRequest() {
|
|
253
260
|
const apiKey = this.apiKey
|
|
254
261
|
const request = makeRequest.apply(this, arguments)
|
|
@@ -266,14 +273,11 @@ module.exports = function initialize(agent, openai, moduleName, shim) {
|
|
|
266
273
|
}
|
|
267
274
|
})
|
|
268
275
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
*
|
|
273
|
-
* **Note**: Currently only for promises. streams will come later
|
|
274
|
-
*/
|
|
276
|
+
// Instruments chat completion creation
|
|
277
|
+
// and creates the LLM events
|
|
278
|
+
// **Note**: Currently only for promises. streams will come later
|
|
275
279
|
shim.record(
|
|
276
|
-
|
|
280
|
+
openaiClient.Chat.Completions.prototype,
|
|
277
281
|
'create',
|
|
278
282
|
function wrapCreate(shim, create, name, args) {
|
|
279
283
|
const [request] = args
|
|
@@ -308,12 +312,10 @@ module.exports = function initialize(agent, openai, moduleName, shim) {
|
|
|
308
312
|
}
|
|
309
313
|
)
|
|
310
314
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
* and creates LlmEmbedding event
|
|
314
|
-
*/
|
|
315
|
+
// Instruments embedding creation
|
|
316
|
+
// and creates LlmEmbedding event
|
|
315
317
|
shim.record(
|
|
316
|
-
|
|
318
|
+
openaiClient.Embeddings.prototype,
|
|
317
319
|
'create',
|
|
318
320
|
function wrapEmbeddingCreate(shim, embeddingCreate, name, args) {
|
|
319
321
|
const [request] = args
|
package/lib/instrumentations.js
CHANGED
|
@@ -13,6 +13,7 @@ module.exports = function instrumentations() {
|
|
|
13
13
|
'@azure/functions': { type: InstrumentationDescriptor.TYPE_GENERIC },
|
|
14
14
|
'@elastic/elasticsearch': { type: InstrumentationDescriptor.TYPE_DATASTORE },
|
|
15
15
|
'@opensearch-project/opensearch': { type: InstrumentationDescriptor.TYPE_DATASTORE },
|
|
16
|
+
'@google/genai': { type: InstrumentationDescriptor.TYPE_GENERIC },
|
|
16
17
|
'@grpc/grpc-js': { module: './instrumentation/grpc-js' },
|
|
17
18
|
'@hapi/hapi': { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
|
|
18
19
|
'@hapi/vision': { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
|
|
@@ -23,9 +23,24 @@ module.exports = class LlmErrorMessage {
|
|
|
23
23
|
* @param {LlmTool} [params.tool] Details about the tool event if it was a tool event.
|
|
24
24
|
*/
|
|
25
25
|
constructor({ response, cause, summary, embedding, vectorsearch, tool } = {}) {
|
|
26
|
-
|
|
26
|
+
// For @google/genai only, cause does not have the `error` or `status` fields,
|
|
27
|
+
// but it does have `message` with the info we need. So, we need to parse
|
|
28
|
+
// the relevant fields from cause.message to get `status` and `error`.
|
|
29
|
+
let parsedError
|
|
30
|
+
const isGeminiVendor = embedding?.vendor === 'gemini' || summary?.vendor === 'gemini'
|
|
31
|
+
if (isGeminiVendor && cause?.message) {
|
|
32
|
+
try {
|
|
33
|
+
// Extract the JSON portion of the cause.message
|
|
34
|
+
const jsonStartIndex = cause.message.indexOf('{')
|
|
35
|
+
const jsonString = cause.message.substring(jsonStartIndex)
|
|
36
|
+
parsedError = JSON.parse(jsonString)?.error
|
|
37
|
+
} catch {
|
|
38
|
+
parsedError = undefined
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
this['http.statusCode'] = response?.status ?? cause?.status ?? parsedError?.code
|
|
27
42
|
this['error.message'] = cause?.message
|
|
28
|
-
this['error.code'] = response?.code ?? cause?.error?.code
|
|
43
|
+
this['error.code'] = response?.code ?? cause?.error?.code ?? parsedError?.code
|
|
29
44
|
this['error.param'] = response?.param ?? cause?.error?.param
|
|
30
45
|
this.completion_id = summary?.id
|
|
31
46
|
this.embedding_id = embedding?.id
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
const LlmEvent = require('./event')
|
|
8
|
+
const { makeId } = require('../../util/hashes')
|
|
9
|
+
|
|
10
|
+
module.exports = class LlmChatCompletionMessage extends LlmEvent {
|
|
11
|
+
constructor({
|
|
12
|
+
agent,
|
|
13
|
+
segment,
|
|
14
|
+
request = {},
|
|
15
|
+
response = {},
|
|
16
|
+
index = 0,
|
|
17
|
+
message,
|
|
18
|
+
completionId,
|
|
19
|
+
transaction
|
|
20
|
+
}) {
|
|
21
|
+
super({ agent, segment, request, response, transaction })
|
|
22
|
+
this.id = makeId(36)
|
|
23
|
+
// message?.role is only defined if the message is
|
|
24
|
+
// a response and it is always 'model'.
|
|
25
|
+
// request messages do not have a role
|
|
26
|
+
this.role = message?.role ?? 'user'
|
|
27
|
+
this.sequence = index
|
|
28
|
+
this.completion_id = completionId
|
|
29
|
+
const responseText = response?.text ?? response?.candidates?.[0]?.content?.parts?.[0]?.text
|
|
30
|
+
this.is_response = responseText === message?.parts?.[0]?.text
|
|
31
|
+
|
|
32
|
+
if (agent.config.ai_monitoring.record_content.enabled === true) {
|
|
33
|
+
this.content = this.is_response ? message?.parts?.[0]?.text : message
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const tokenCB = agent.llm?.tokenCountCallback
|
|
37
|
+
|
|
38
|
+
if (typeof tokenCB === 'function') {
|
|
39
|
+
if (this.is_response) {
|
|
40
|
+
this.token_count = tokenCB(this['response.model'], this.content)
|
|
41
|
+
} else {
|
|
42
|
+
this.token_count = tokenCB(request.model, this.content)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 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, transaction }) {
|
|
11
|
+
super({ agent, segment, request, response, responseAttrs: true, transaction })
|
|
12
|
+
this.error = withError
|
|
13
|
+
let requestMessagesLength = 0
|
|
14
|
+
if (Array.isArray(request?.contents)) {
|
|
15
|
+
requestMessagesLength = request.contents.length
|
|
16
|
+
} else if (typeof request?.contents === 'string') {
|
|
17
|
+
requestMessagesLength = 1
|
|
18
|
+
}
|
|
19
|
+
this['response.number_of_messages'] = requestMessagesLength + (response?.candidates?.length || 0)
|
|
20
|
+
this['response.choices.finish_reason'] = response?.candidates?.[0]?.finishReason
|
|
21
|
+
this['request.max_tokens'] = request.config?.maxOutputTokens
|
|
22
|
+
this['request.temperature'] = request.config?.temperature
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const LlmEvent = require('./event')
|
|
9
|
+
|
|
10
|
+
class LlmEmbedding extends LlmEvent {
|
|
11
|
+
constructor({ agent, segment, request = {}, response = {}, withError = false, transaction }) {
|
|
12
|
+
super({ agent, segment, request, response, responseAttrs: true, transaction })
|
|
13
|
+
this.error = withError
|
|
14
|
+
|
|
15
|
+
if (agent.config.ai_monitoring.record_content.enabled === true) {
|
|
16
|
+
this.input = request.contents?.toString()
|
|
17
|
+
}
|
|
18
|
+
this.token_count = agent.llm?.tokenCountCallback?.(
|
|
19
|
+
this['request.model'],
|
|
20
|
+
request.contents?.toString()
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = LlmEmbedding
|