newrelic 13.0.0 → 13.2.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 +92 -0
- package/README.md +2 -4
- package/THIRD_PARTY_NOTICES.md +429 -9
- package/esm-loader.mjs +9 -1
- package/index.js +38 -3
- package/lib/agent.js +8 -2
- package/lib/config/build-instrumentation-config.js +3 -0
- package/lib/config/default.js +1397 -1383
- package/lib/config/index.js +15 -19
- package/lib/context-manager/context.js +13 -4
- package/lib/harvester.js +11 -13
- package/lib/instrumentation/@google/genai.js +8 -3
- package/lib/instrumentation/@hapi/hapi.js +4 -6
- package/lib/instrumentation/@nestjs/core.js +12 -14
- package/lib/instrumentation/aws-sdk/v3/bedrock.js +2 -2
- package/lib/instrumentation/core/http-outbound.js +11 -26
- package/lib/instrumentation/kafkajs/producer.js +13 -19
- package/lib/instrumentation/openai.js +2 -2
- package/lib/instrumentation/undici.js +48 -40
- package/lib/instrumentations.js +14 -5
- package/lib/llm-events/aws-bedrock/utils.js +1 -1
- package/lib/metrics/names.js +8 -0
- package/lib/metrics/normalizer.js +1 -3
- package/lib/otel/context.js +18 -14
- package/lib/otel/logs/bootstrap-logs.js +84 -0
- package/lib/otel/logs/no-op-exporter.js +25 -0
- package/lib/otel/logs/normalize-timestamp.js +59 -0
- package/lib/otel/logs/proxying-provider.js +46 -0
- package/lib/otel/logs/severity-to-string.js +56 -0
- package/lib/otel/metrics/bootstrap-metrics.js +1 -1
- package/lib/otel/setup.js +21 -8
- package/lib/patch-module.js +70 -0
- package/lib/serverless/aws-lambda.js +1 -3
- package/lib/shim/webframework-shim/middleware-mounter.js +1 -3
- package/lib/shimmer.js +56 -8
- package/lib/subscriber-configs.js +17 -0
- package/lib/subscribers/application-logs.js +55 -0
- package/lib/subscribers/base.js +177 -0
- package/lib/subscribers/create-config.js +27 -0
- package/lib/subscribers/db-operation.js +21 -0
- package/lib/subscribers/db-query.js +54 -0
- package/lib/subscribers/db.js +57 -0
- package/lib/subscribers/elasticsearch/config.js +49 -0
- package/lib/subscribers/elasticsearch/elasticsearch.js +42 -0
- package/lib/subscribers/elasticsearch/opensearch.js +15 -0
- package/lib/subscribers/elasticsearch/transport.js +14 -0
- package/lib/subscribers/ioredis/config.js +37 -0
- package/lib/subscribers/ioredis/index.js +48 -0
- package/lib/subscribers/mcp-sdk/client-prompt.js +23 -0
- package/lib/subscribers/mcp-sdk/client-resource.js +24 -0
- package/lib/subscribers/mcp-sdk/client-tool.js +23 -0
- package/lib/subscribers/mcp-sdk/client.js +29 -0
- package/lib/subscribers/mcp-sdk/config.js +104 -0
- package/lib/subscribers/pino/config.js +20 -0
- package/lib/subscribers/pino/index.js +102 -0
- package/lib/transaction/index.js +4 -1
- package/lib/transaction/trace/aggregator.js +7 -9
- package/lib/util/camel-case.js +1 -3
- package/lib/util/get-package-version.js +34 -0
- package/lib/util/urltils.js +19 -13
- package/lib/w3c/tracestate.js +3 -3
- package/newrelic.js +10 -0
- package/package.json +9 -5
- package/lib/instrumentation/@elastic/elasticsearch.js +0 -62
- package/lib/instrumentation/@opensearch-project/opensearch.js +0 -66
- package/lib/instrumentation/ioredis.js +0 -50
- package/lib/instrumentation/pino/nr-hooks.js +0 -19
- package/lib/instrumentation/pino/pino.js +0 -168
package/lib/config/index.js
CHANGED
|
@@ -44,28 +44,24 @@ const exists = fs.existsSync
|
|
|
44
44
|
let logger = null // Lazy-loaded in `initialize`.
|
|
45
45
|
let _configInstance = null
|
|
46
46
|
|
|
47
|
-
const getConfigFileNames = () =>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
: undefined
|
|
62
|
-
].filter(Boolean)
|
|
47
|
+
const getConfigFileNames = () => [process.env.NEW_RELIC_CONFIG_FILENAME, 'newrelic.js', 'newrelic.cjs', 'newrelic.mjs'].filter(
|
|
48
|
+
Boolean
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const getConfigFileLocations = () => [
|
|
52
|
+
process.env.NEW_RELIC_HOME,
|
|
53
|
+
process.cwd(),
|
|
54
|
+
process.env.HOME,
|
|
55
|
+
path.join(__dirname, '../../../..'), // above node_modules
|
|
56
|
+
// the REPL has no main module
|
|
57
|
+
process.mainModule && process.mainModule.filename
|
|
58
|
+
? path.dirname(process.mainModule.filename)
|
|
59
|
+
: undefined
|
|
60
|
+
].filter(Boolean)
|
|
63
61
|
|
|
64
62
|
function _findConfigFile() {
|
|
65
63
|
const configFileCandidates = getConfigFileLocations().reduce((files, configPath) => {
|
|
66
|
-
const configFiles = getConfigFileNames().map((filename) =>
|
|
67
|
-
path.join(path.resolve(configPath), filename)
|
|
68
|
-
)
|
|
64
|
+
const configFiles = getConfigFileNames().map((filename) => path.join(path.resolve(configPath), filename))
|
|
69
65
|
|
|
70
66
|
return files.concat(configFiles)
|
|
71
67
|
}, [])
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
'use strict'
|
|
7
7
|
|
|
8
8
|
module.exports = class Context {
|
|
9
|
-
constructor(transaction, segment) {
|
|
9
|
+
constructor({ transaction, segment, extras = {} } = {}) {
|
|
10
10
|
this._transaction = transaction
|
|
11
11
|
this._segment = segment
|
|
12
|
+
this._extras = extras
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
get segment() {
|
|
@@ -19,6 +20,14 @@ module.exports = class Context {
|
|
|
19
20
|
return this._transaction
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
get extras() {
|
|
24
|
+
return this._extras
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
set extras(extras) {
|
|
28
|
+
this._extras = { ...this._extras, ...extras }
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
/**
|
|
23
32
|
* Constructs a new context from segment about to be bound to context manager
|
|
24
33
|
* along with the current transaction.
|
|
@@ -28,8 +37,8 @@ module.exports = class Context {
|
|
|
28
37
|
* @param {Transaction} params.transaction active transaction
|
|
29
38
|
* @returns {Context} a newly constructed context
|
|
30
39
|
*/
|
|
31
|
-
enterSegment({ segment, transaction = this.
|
|
32
|
-
return new this.constructor(transaction, segment)
|
|
40
|
+
enterSegment({ segment, transaction = this.transaction }) {
|
|
41
|
+
return new this.constructor({ transaction, segment, extras: this.extras })
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
/**
|
|
@@ -40,6 +49,6 @@ module.exports = class Context {
|
|
|
40
49
|
* @returns {Context} a newly constructed context
|
|
41
50
|
*/
|
|
42
51
|
enterTransaction(transaction) {
|
|
43
|
-
return new this.constructor(transaction, transaction?.trace?.root)
|
|
52
|
+
return new this.constructor({ transaction, segment: transaction?.trace?.root, extras: this.extras })
|
|
44
53
|
}
|
|
45
54
|
}
|
package/lib/harvester.js
CHANGED
|
@@ -34,20 +34,18 @@ module.exports = class Harvester {
|
|
|
34
34
|
*/
|
|
35
35
|
clear(callback) {
|
|
36
36
|
Promise.all(
|
|
37
|
-
this.aggregators.map((aggregator) => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
aggregator.once(`finished_data_send-${aggregator.method}`, function finish() {
|
|
41
|
-
resolve()
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
aggregator.send()
|
|
45
|
-
} else {
|
|
46
|
-
// No data to flush because aggregator is not enabled
|
|
37
|
+
this.aggregators.map((aggregator) => new Promise((resolve) => {
|
|
38
|
+
if (aggregator.enabled) {
|
|
39
|
+
aggregator.once(`finished_data_send-${aggregator.method}`, function finish() {
|
|
47
40
|
resolve()
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
aggregator.send()
|
|
44
|
+
} else {
|
|
45
|
+
// No data to flush because aggregator is not enabled
|
|
46
|
+
resolve()
|
|
47
|
+
}
|
|
48
|
+
}))
|
|
51
49
|
).then(() => {
|
|
52
50
|
// Get out of the promise so callback errors aren't treated as
|
|
53
51
|
// promise rejections.
|
|
@@ -196,7 +196,9 @@ module.exports = function initialize(agent, googleGenAi, moduleName, shim) {
|
|
|
196
196
|
|
|
197
197
|
// Instruments chat completion creation
|
|
198
198
|
// and creates the LLM events
|
|
199
|
-
shim.record(
|
|
199
|
+
shim.record(
|
|
200
|
+
models.prototype,
|
|
201
|
+
'generateContentInternal',
|
|
200
202
|
function wrapGenerateContent(shim, func, name, args) {
|
|
201
203
|
const [request] = args
|
|
202
204
|
|
|
@@ -222,7 +224,9 @@ module.exports = function initialize(agent, googleGenAi, moduleName, shim) {
|
|
|
222
224
|
|
|
223
225
|
// Instruments chat completion streaming
|
|
224
226
|
// and creates the LLM events
|
|
225
|
-
shim.record(
|
|
227
|
+
shim.record(
|
|
228
|
+
models.prototype,
|
|
229
|
+
'generateContentStreamInternal',
|
|
226
230
|
function wrapGenerateContentStream(shim, func, name, args) {
|
|
227
231
|
if (!agent.config.ai_monitoring.streaming.enabled) {
|
|
228
232
|
shim.logger.warn(
|
|
@@ -241,7 +245,8 @@ module.exports = function initialize(agent, googleGenAi, moduleName, shim) {
|
|
|
241
245
|
addLlmMeta({ agent, transaction })
|
|
242
246
|
}
|
|
243
247
|
})
|
|
244
|
-
}
|
|
248
|
+
}
|
|
249
|
+
)
|
|
245
250
|
|
|
246
251
|
// Instruments embedding creation
|
|
247
252
|
// and creates LlmEmbedding event
|
|
@@ -204,12 +204,10 @@ function wrapPreHandlers(shim, container, path) {
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
function wrapPreHandler(shim, container, path) {
|
|
207
|
-
return shim.record(container, (shim) => {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
})
|
|
212
|
-
})
|
|
207
|
+
return shim.record(container, (shim) => new SegmentSpec({
|
|
208
|
+
name: [shim.HAPI, ' pre handler: ', '(', path, ')'].join(''),
|
|
209
|
+
recorder: record
|
|
210
|
+
}))
|
|
213
211
|
}
|
|
214
212
|
|
|
215
213
|
function wrapRouteHandler(shim, handler, path) {
|
|
@@ -18,20 +18,18 @@ module.exports = function initialize(agent, core, moduleName, shim) {
|
|
|
18
18
|
return
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
shim.wrap(core.BaseExceptionFilter.prototype, 'handleUnknownError', (shim, original) => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return original.apply(this, arguments)
|
|
21
|
+
shim.wrap(core.BaseExceptionFilter.prototype, 'handleUnknownError', (shim, original) => function wrappedHandleUnknownError(exception) {
|
|
22
|
+
const transaction = shim.tracer.getTransaction()
|
|
23
|
+
if (transaction) {
|
|
24
|
+
shim.agent.errors.add(transaction, exception)
|
|
25
|
+
logger.trace(exception, 'Captured error handled by Nest.js exception filter.')
|
|
26
|
+
} else {
|
|
27
|
+
logger.trace(
|
|
28
|
+
exception,
|
|
29
|
+
'Ignoring error handled by Nest.js exception filter: not in a transaction'
|
|
30
|
+
)
|
|
35
31
|
}
|
|
32
|
+
|
|
33
|
+
return original.apply(this, arguments)
|
|
36
34
|
})
|
|
37
35
|
}
|
|
@@ -204,7 +204,7 @@ function recordEmbeddingMessage({
|
|
|
204
204
|
return
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
const embeddings = bedrockCommand.prompt.map(prompt => new LlmEmbedding({
|
|
207
|
+
const embeddings = bedrockCommand.prompt.map((prompt) => new LlmEmbedding({
|
|
208
208
|
agent,
|
|
209
209
|
segment,
|
|
210
210
|
transaction,
|
|
@@ -214,7 +214,7 @@ function recordEmbeddingMessage({
|
|
|
214
214
|
isError: err !== null
|
|
215
215
|
}))
|
|
216
216
|
|
|
217
|
-
embeddings.forEach(embedding => {
|
|
217
|
+
embeddings.forEach((embedding) => {
|
|
218
218
|
recordEvent({ agent, type: 'LlmEmbedding', msg: embedding })
|
|
219
219
|
})
|
|
220
220
|
|
|
@@ -11,7 +11,6 @@ const urltils = require('../../util/urltils')
|
|
|
11
11
|
const logger = require('../../logger').child({ component: 'outbound' })
|
|
12
12
|
const shimmer = require('../../shimmer')
|
|
13
13
|
const url = require('url')
|
|
14
|
-
const copy = require('../../util/copy')
|
|
15
14
|
const symbols = require('../../symbols')
|
|
16
15
|
const synthetics = require('../../synthetics')
|
|
17
16
|
const { URL } = require('node:url')
|
|
@@ -57,24 +56,6 @@ function getDefaultHostName(opts) {
|
|
|
57
56
|
return opts.hostname || opts.host || DEFAULT_HOST
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
/**
|
|
61
|
-
* Parses http opts to an object
|
|
62
|
-
* If string will call url.parse, otherwise it will
|
|
63
|
-
* do a shallow copy
|
|
64
|
-
*
|
|
65
|
-
* @param {string|object} opts a url string or HTTP request options
|
|
66
|
-
* @returns {object} parsed http opts
|
|
67
|
-
*/
|
|
68
|
-
function parseOpts(opts) {
|
|
69
|
-
if (typeof opts === 'string') {
|
|
70
|
-
opts = url.parse(opts)
|
|
71
|
-
} else {
|
|
72
|
-
opts = copy.shallow(opts)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return opts
|
|
76
|
-
}
|
|
77
|
-
|
|
78
59
|
/**
|
|
79
60
|
* Extracts host, hostname, port from http request options
|
|
80
61
|
*
|
|
@@ -122,8 +103,6 @@ function extractHostPortViaProxy(opts) {
|
|
|
122
103
|
* @returns {object} The instrumented outbound HTTP request.
|
|
123
104
|
*/
|
|
124
105
|
module.exports = function instrumentOutbound(agent, opts, makeRequest) {
|
|
125
|
-
opts = parseOpts(opts)
|
|
126
|
-
|
|
127
106
|
const viaProxy = extractHostPortViaProxy(opts)
|
|
128
107
|
const { host, hostname, port } = viaProxy ? viaProxy : extractHostPort(opts)
|
|
129
108
|
|
|
@@ -265,23 +244,29 @@ function assignOutgoingHeaders(currentHeaders, outboundHeaders) {
|
|
|
265
244
|
function applySegment({ opts, makeRequest, host, port, hostname, segment, config }) {
|
|
266
245
|
segment.start()
|
|
267
246
|
const request = makeRequest(opts)
|
|
268
|
-
|
|
269
|
-
|
|
247
|
+
|
|
248
|
+
// Attempt to parse into new URL.
|
|
249
|
+
// If no href, revert to url.parse.
|
|
250
|
+
// Rarely, the href doesn't contain the
|
|
251
|
+
// path, so we have to use url.parse to get the path.
|
|
252
|
+
const urlObject = (opts?.href?.includes(request.path)) ? new URL(opts.href) : url.parse(request.path, true)
|
|
253
|
+
const parsed = urltils.scrubAndParseParameters(urlObject)
|
|
254
|
+
const obfuscatedPath = urltils.obfuscatePath(config, parsed.path)
|
|
255
|
+
|
|
270
256
|
const proto = parsed.protocol || opts.protocol || 'http:'
|
|
271
|
-
segment.name +=
|
|
257
|
+
segment.name += obfuscatedPath
|
|
272
258
|
segment.captureExternalAttributes({
|
|
273
259
|
protocol: proto,
|
|
274
260
|
hostname,
|
|
275
261
|
host,
|
|
276
262
|
method: opts.method,
|
|
277
263
|
port,
|
|
278
|
-
path:
|
|
264
|
+
path: obfuscatedPath,
|
|
279
265
|
queryParams: parsed.parameters
|
|
280
266
|
})
|
|
281
267
|
request[symbols.segment] = segment
|
|
282
268
|
return request
|
|
283
269
|
}
|
|
284
|
-
|
|
285
270
|
/**
|
|
286
271
|
* Wrap the emit method. We're doing a special wrapper instead of using
|
|
287
272
|
* `tracer.bindEmitter` because we want to do some logic based on certain
|
|
@@ -42,15 +42,13 @@ function nrSend(shim, fn, name, args) {
|
|
|
42
42
|
promise: true,
|
|
43
43
|
destinationName: data.topic,
|
|
44
44
|
destinationType: shim.TOPIC,
|
|
45
|
-
messageHeaders: (inject) => {
|
|
46
|
-
|
|
47
|
-
if (msg.headers) {
|
|
48
|
-
return inject(msg.headers)
|
|
49
|
-
}
|
|
50
|
-
msg.headers = {}
|
|
45
|
+
messageHeaders: (inject) => data.messages.map((msg) => {
|
|
46
|
+
if (msg.headers) {
|
|
51
47
|
return inject(msg.headers)
|
|
52
|
-
}
|
|
53
|
-
|
|
48
|
+
}
|
|
49
|
+
msg.headers = {}
|
|
50
|
+
return inject(msg.headers)
|
|
51
|
+
})
|
|
54
52
|
})
|
|
55
53
|
}
|
|
56
54
|
|
|
@@ -74,16 +72,12 @@ function nrSendBatch(shim, fn, name, args) {
|
|
|
74
72
|
promise: true,
|
|
75
73
|
destinationName: data.topicMessages[0].topic,
|
|
76
74
|
destinationType: shim.TOPIC,
|
|
77
|
-
messageHeaders: (inject) => {
|
|
78
|
-
|
|
79
|
-
return
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return inject(m.headers)
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
}
|
|
75
|
+
messageHeaders: (inject) => data.topicMessages.map((tm) => tm.messages.map((m) => {
|
|
76
|
+
if (m.headers) {
|
|
77
|
+
return inject(m.headers)
|
|
78
|
+
}
|
|
79
|
+
m.headers = {}
|
|
80
|
+
return inject(m.headers)
|
|
81
|
+
}))
|
|
88
82
|
})
|
|
89
83
|
}
|
|
@@ -64,13 +64,13 @@ function getMessagesFromRequest(request, shim) {
|
|
|
64
64
|
|
|
65
65
|
if (Array.isArray(request?.input)) {
|
|
66
66
|
// Handle array of input messages
|
|
67
|
-
messages = request.input.filter(msg => msg?.content && msg?.role)
|
|
67
|
+
messages = request.input.filter((msg) => msg?.content && msg?.role)
|
|
68
68
|
} else if (typeof request?.input === 'string') {
|
|
69
69
|
// Handle single string input as a user message
|
|
70
70
|
messages = [{ content: request.input, role: 'user' }]
|
|
71
71
|
} else if (Array.isArray(request?.messages)) {
|
|
72
72
|
// Handle array of messages
|
|
73
|
-
messages = request.messages.filter(msg => msg?.content && msg?.role)
|
|
73
|
+
messages = request.messages.filter((msg) => msg?.content && msg?.role)
|
|
74
74
|
} else {
|
|
75
75
|
shim.logger.warn('No valid messages found in OpenAI request object.')
|
|
76
76
|
}
|
|
@@ -9,7 +9,6 @@ const cat = require('../util/cat')
|
|
|
9
9
|
const recordExternal = require('../metrics/recorders/http_external')
|
|
10
10
|
const logger = require('../logger').child({ component: 'undici' })
|
|
11
11
|
const NAMES = require('../metrics/names')
|
|
12
|
-
const symbols = require('../symbols')
|
|
13
12
|
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
14
13
|
const diagnosticsChannel = require('diagnostics_channel')
|
|
15
14
|
const synthetics = require('../synthetics')
|
|
@@ -27,13 +26,10 @@ const channels = [
|
|
|
27
26
|
* See: https://github.com/nodejs/undici/blob/main/docs/api/DiagnosticsChannel.md
|
|
28
27
|
*
|
|
29
28
|
* @param agent
|
|
30
|
-
* @param _undici
|
|
31
|
-
* @param _modName
|
|
32
|
-
* @param shim
|
|
33
29
|
*/
|
|
34
|
-
module.exports = function addUndiciChannels(agent
|
|
30
|
+
module.exports = function addUndiciChannels(agent) {
|
|
35
31
|
channels.forEach(({ channel, hook }, index) => {
|
|
36
|
-
const boundHook = hook.bind(null,
|
|
32
|
+
const boundHook = hook.bind(null, agent)
|
|
37
33
|
diagnosticsChannel.subscribe(channel, boundHook)
|
|
38
34
|
// store the bound hook for unsubscription later
|
|
39
35
|
channels[index].boundHook = boundHook
|
|
@@ -75,18 +71,25 @@ function addDTHeaders({ transaction, config, request }) {
|
|
|
75
71
|
* Creates the external segment with url, procedure and request.parameters attributes
|
|
76
72
|
*
|
|
77
73
|
* @param {object} params object to fn
|
|
78
|
-
* @param {
|
|
79
|
-
* @param {object} params.
|
|
80
|
-
* @param
|
|
74
|
+
* @param {Agent} params.agent NR agent instance
|
|
75
|
+
* @param {object} params.context active context
|
|
76
|
+
* @param params.request
|
|
81
77
|
*/
|
|
82
|
-
function createExternalSegment({
|
|
78
|
+
function createExternalSegment({ agent, request, context }) {
|
|
83
79
|
const url = new URL(request.origin + request.path)
|
|
84
|
-
const obfuscatedPath = urltils.obfuscatePath(
|
|
80
|
+
const obfuscatedPath = urltils.obfuscatePath(agent.config, url.pathname)
|
|
85
81
|
const name = NAMES.EXTERNAL.PREFIX + url.host + obfuscatedPath
|
|
82
|
+
const transaction = context?.transaction
|
|
83
|
+
const parent = context?.extras?.undiciParent
|
|
86
84
|
// Metrics for `External/<host>` will have a suffix of undici
|
|
87
85
|
// We will have to see if this matters for people only using fetch
|
|
88
86
|
// It's undici under the hood so ¯\_(ツ)_/¯
|
|
89
|
-
const externalSegment =
|
|
87
|
+
const externalSegment = agent.tracer.createSegment({
|
|
88
|
+
name,
|
|
89
|
+
recorder: recordExternal(url.host, 'undici'),
|
|
90
|
+
parent,
|
|
91
|
+
transaction
|
|
92
|
+
})
|
|
90
93
|
|
|
91
94
|
// the captureExternalAttributes expects queryParams to be an object, do conversion
|
|
92
95
|
// to object see: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
|
|
@@ -94,7 +97,11 @@ function createExternalSegment({ shim, request, segment }) {
|
|
|
94
97
|
|
|
95
98
|
if (externalSegment) {
|
|
96
99
|
externalSegment.start()
|
|
97
|
-
|
|
100
|
+
// storing the undici external segment in the context extras
|
|
101
|
+
// this has to be done because the various hook points produce different segments
|
|
102
|
+
// and we need to be able to access the segment later
|
|
103
|
+
context.extras = { undiciSegment: externalSegment }
|
|
104
|
+
agent.tracer.setSegment({ segment: externalSegment, transaction })
|
|
98
105
|
externalSegment.captureExternalAttributes({
|
|
99
106
|
protocol: url.protocol,
|
|
100
107
|
hostname: url.hostname,
|
|
@@ -104,8 +111,6 @@ function createExternalSegment({ shim, request, segment }) {
|
|
|
104
111
|
path: obfuscatedPath,
|
|
105
112
|
queryParams
|
|
106
113
|
})
|
|
107
|
-
|
|
108
|
-
request[symbols.segment] = externalSegment
|
|
109
114
|
}
|
|
110
115
|
}
|
|
111
116
|
|
|
@@ -115,15 +120,18 @@ function createExternalSegment({ shim, request, segment }) {
|
|
|
115
120
|
* external segment with the standard url/procedure/request.parameters
|
|
116
121
|
* attributes. We will also attach relevant DT headers to outgoing http request.
|
|
117
122
|
*
|
|
118
|
-
* @param {
|
|
123
|
+
* @param {Agent} agent NR agent instance
|
|
119
124
|
* @param {object} params object from undici hook
|
|
120
125
|
* @param {object} params.request undici request object
|
|
121
126
|
*/
|
|
122
|
-
function requestCreateHook(
|
|
123
|
-
const { config } =
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
+
function requestCreateHook(agent, { request }) {
|
|
128
|
+
const { config } = agent
|
|
129
|
+
const context = agent.tracer.getContext()
|
|
130
|
+
const { segment, transaction } = context
|
|
131
|
+
// storing the parent of the undici external segment in the context extras
|
|
132
|
+
// this has to be done because the various hook points produce different segments
|
|
133
|
+
// and we need to be able to access the segment later
|
|
134
|
+
context.extras = { undiciParent: segment }
|
|
127
135
|
if (!(segment || transaction) || segment?.opaque) {
|
|
128
136
|
logger.trace(
|
|
129
137
|
'Not capturing data for outbound request (%s) because parent segment opaque (%s)',
|
|
@@ -135,7 +143,7 @@ function requestCreateHook(shim, { request }) {
|
|
|
135
143
|
}
|
|
136
144
|
|
|
137
145
|
try {
|
|
138
|
-
createExternalSegment({
|
|
146
|
+
createExternalSegment({ agent, request, context })
|
|
139
147
|
addDTHeaders({ transaction, config, request })
|
|
140
148
|
} catch (err) {
|
|
141
149
|
logger.warn(err, 'Unable to create external segment')
|
|
@@ -147,15 +155,15 @@ function requestCreateHook(shim, { request }) {
|
|
|
147
155
|
* We will add the relevant http response attributes to active segment.
|
|
148
156
|
* Also add CAT specific keys to active segment.
|
|
149
157
|
*
|
|
150
|
-
* @param {
|
|
158
|
+
* @param {object} agent
|
|
151
159
|
* @param {object} params object from undici hook
|
|
152
|
-
* @param {object} params.request undici request object
|
|
153
160
|
* @param {object} params.response { statusCode, headers, statusText }
|
|
154
161
|
*/
|
|
155
|
-
function requestHeadersHook(
|
|
156
|
-
const { config } =
|
|
157
|
-
const
|
|
158
|
-
const
|
|
162
|
+
function requestHeadersHook(agent, { response }) {
|
|
163
|
+
const { config } = agent
|
|
164
|
+
const context = agent.tracer.getContext()
|
|
165
|
+
const activeSegment = context?.extras?.undiciSegment
|
|
166
|
+
const transaction = context?.transaction
|
|
159
167
|
if (!activeSegment) {
|
|
160
168
|
return
|
|
161
169
|
}
|
|
@@ -185,37 +193,37 @@ function requestHeadersHook(shim, { request, response }) {
|
|
|
185
193
|
* Gets the active segment, parent segment and transaction from given ctx(request, client connector)
|
|
186
194
|
* and ends segment and sets the previous parent segment as the active segment. If an error exists it will add the error to the transaction
|
|
187
195
|
*
|
|
188
|
-
* @param {
|
|
196
|
+
* @param {Agent} agent NR agent instance
|
|
189
197
|
* @param {object} params object from undici hook
|
|
190
|
-
* @param {object} params.request or client connector
|
|
191
198
|
* @param {Error} params.error error from undici request
|
|
192
199
|
*/
|
|
193
|
-
function endAndRestoreSegment(
|
|
194
|
-
const { config } =
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
const
|
|
200
|
+
function endAndRestoreSegment(agent, { error }) {
|
|
201
|
+
const { config } = agent
|
|
202
|
+
const context = agent.tracer.getContext()
|
|
203
|
+
const activeSegment = context?.extras?.undiciSegment
|
|
204
|
+
const parentSegment = context?.extras?.undiciParent
|
|
205
|
+
const tx = context?.transaction
|
|
198
206
|
if (activeSegment) {
|
|
199
207
|
activeSegment.end()
|
|
200
208
|
}
|
|
201
209
|
|
|
202
210
|
if (error && tx && config.feature_flag.undici_error_tracking === true) {
|
|
203
|
-
handleError(
|
|
211
|
+
handleError(agent, tx, error)
|
|
204
212
|
}
|
|
205
213
|
|
|
206
214
|
if (parentSegment) {
|
|
207
|
-
|
|
215
|
+
agent.tracer.setSegment({ segment: parentSegment, transaction: tx })
|
|
208
216
|
}
|
|
209
217
|
}
|
|
210
218
|
|
|
211
219
|
/**
|
|
212
220
|
* Adds the error to the active transaction
|
|
213
221
|
*
|
|
214
|
-
* @param {
|
|
222
|
+
* @param {Agent} agent NR agent instance
|
|
215
223
|
* @param {Transaction} tx active transaction
|
|
216
224
|
* @param {Error} error error from undici request
|
|
217
225
|
*/
|
|
218
|
-
function handleError(
|
|
226
|
+
function handleError(agent, tx, error) {
|
|
219
227
|
logger.trace(error, 'Captured outbound error on behalf of the user.')
|
|
220
|
-
|
|
228
|
+
agent.errors.add(tx, error)
|
|
221
229
|
}
|
package/lib/instrumentations.js
CHANGED
|
@@ -11,8 +11,6 @@ const InstrumentationDescriptor = require('./instrumentation-descriptor')
|
|
|
11
11
|
module.exports = function instrumentations() {
|
|
12
12
|
return {
|
|
13
13
|
'@azure/functions': { type: InstrumentationDescriptor.TYPE_GENERIC },
|
|
14
|
-
'@elastic/elasticsearch': { type: InstrumentationDescriptor.TYPE_DATASTORE },
|
|
15
|
-
'@opensearch-project/opensearch': { type: InstrumentationDescriptor.TYPE_DATASTORE },
|
|
16
14
|
'@google/genai': { type: InstrumentationDescriptor.TYPE_GENERIC },
|
|
17
15
|
'@grpc/grpc-js': { module: './instrumentation/grpc-js' },
|
|
18
16
|
'@hapi/hapi': { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
|
|
@@ -30,7 +28,6 @@ module.exports = function instrumentations() {
|
|
|
30
28
|
express: { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
|
|
31
29
|
fastify: { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
|
|
32
30
|
'generic-pool': { type: InstrumentationDescriptor.TYPE_GENERIC },
|
|
33
|
-
ioredis: { type: InstrumentationDescriptor.TYPE_DATASTORE },
|
|
34
31
|
kafkajs: { type: InstrumentationDescriptor.TYPE_MESSAGE },
|
|
35
32
|
koa: { module: './instrumentation/koa' },
|
|
36
33
|
langchain: { module: './instrumentation/langchain' },
|
|
@@ -40,7 +37,6 @@ module.exports = function instrumentations() {
|
|
|
40
37
|
next: { module: './instrumentation/nextjs' },
|
|
41
38
|
openai: { type: InstrumentationDescriptor.TYPE_GENERIC },
|
|
42
39
|
pg: { type: InstrumentationDescriptor.TYPE_DATASTORE },
|
|
43
|
-
pino: { module: './instrumentation/pino' },
|
|
44
40
|
q: { type: null },
|
|
45
41
|
redis: { type: InstrumentationDescriptor.TYPE_DATASTORE },
|
|
46
42
|
restify: { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
|
|
@@ -62,6 +58,19 @@ module.exports = function instrumentations() {
|
|
|
62
58
|
'fancy-log': { type: InstrumentationDescriptor.TYPE_TRACKING },
|
|
63
59
|
knex: { type: InstrumentationDescriptor.TYPE_TRACKING },
|
|
64
60
|
loglevel: { type: InstrumentationDescriptor.TYPE_TRACKING },
|
|
65
|
-
npmlog: { type: InstrumentationDescriptor.TYPE_TRACKING }
|
|
61
|
+
npmlog: { type: InstrumentationDescriptor.TYPE_TRACKING },
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The modules below are listed here is a temporary solution to maintaining
|
|
65
|
+
* the Supportability/Features/onRequire/<module> metrics for libraries
|
|
66
|
+
* that have been migrated to use tracing chanel instrumentation.
|
|
67
|
+
* Once orchestrion can emit the package version, these can be removed.
|
|
68
|
+
* {@link https://github.com/newrelic/node-newrelic/issues/3308 Github Issue}
|
|
69
|
+
*/
|
|
70
|
+
'@elastic/elasticsearch': { type: InstrumentationDescriptor.TYPE_TRACKING },
|
|
71
|
+
'@modelcontextprotocol/sdk/client/index.js': { type: InstrumentationDescriptor.TYPE_TRACKING },
|
|
72
|
+
'@opensearch-project/opensearch': { type: InstrumentationDescriptor.TYPE_TRACKING },
|
|
73
|
+
ioredis: { type: InstrumentationDescriptor.TYPE_TRACKING },
|
|
74
|
+
pino: { type: InstrumentationDescriptor.TYPE_TRACKING }
|
|
66
75
|
}
|
|
67
76
|
}
|
|
@@ -56,7 +56,7 @@ function stringifyConverseChunkedMessage(chunks) {
|
|
|
56
56
|
return `<tool_result>${
|
|
57
57
|
stringifyConverseChunkedMessage(
|
|
58
58
|
(chunk.toolResult.content ?? [])
|
|
59
|
-
.filter(subChunk => !('toolUse' in subChunk || 'toolResult' in subChunk))
|
|
59
|
+
.filter((subChunk) => !('toolUse' in subChunk || 'toolResult' in subChunk))
|
|
60
60
|
)
|
|
61
61
|
}</tool_result>`
|
|
62
62
|
} else if ('guardContent' in chunk) {
|
package/lib/metrics/names.js
CHANGED
|
@@ -199,6 +199,13 @@ AI.LANGCHAIN = {
|
|
|
199
199
|
VECTORSTORE: `${AI.VECTORSTORE}/Langchain`
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
const MCP = {
|
|
203
|
+
TRACKING_PREFIX: `${AI.TRACKING_PREFIX}/MCP`,
|
|
204
|
+
TOOL: `${AI.TOOL}/MCP`,
|
|
205
|
+
RESOURCE: 'Llm/resource/MCP',
|
|
206
|
+
PROMPT: 'Llm/prompt/MCP'
|
|
207
|
+
}
|
|
208
|
+
|
|
202
209
|
const RESTIFY = {
|
|
203
210
|
PREFIX: 'Restify/'
|
|
204
211
|
}
|
|
@@ -380,6 +387,7 @@ module.exports = {
|
|
|
380
387
|
KAFKA,
|
|
381
388
|
LOOP,
|
|
382
389
|
LOGGING,
|
|
390
|
+
MCP,
|
|
383
391
|
MEMCACHE,
|
|
384
392
|
MEMORY,
|
|
385
393
|
MESSAGE_TRANSACTION,
|
|
@@ -109,9 +109,7 @@ MetricNormalizer.prototype.load = function load(json) {
|
|
|
109
109
|
/* I (FLN) always forget this, so making a note: JS sort is always
|
|
110
110
|
* IN-PLACE, even though it returns the sorted array.
|
|
111
111
|
*/
|
|
112
|
-
this.rules.sort((a, b) =>
|
|
113
|
-
return a.precedence - b.precedence
|
|
114
|
-
})
|
|
112
|
+
this.rules.sort((a, b) => a.precedence - b.precedence)
|
|
115
113
|
|
|
116
114
|
logger.debug('Loaded %s %s normalization rule(s).', this.rules.length, this.type)
|
|
117
115
|
}
|