newrelic 13.1.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 +46 -0
- package/README.md +2 -4
- package/THIRD_PARTY_NOTICES.md +3 -3
- package/esm-loader.mjs +9 -1
- package/index.js +2 -2
- package/lib/agent.js +1 -2
- package/lib/config/build-instrumentation-config.js +3 -0
- package/lib/config/default.js +1397 -1395
- 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/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/metrics/bootstrap-metrics.js +1 -1
- 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/trace/aggregator.js +7 -9
- package/lib/util/camel-case.js +1 -3
- package/lib/util/get-package-version.js +34 -0
- package/lib/w3c/tracestate.js +3 -3
- package/package.json +7 -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
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { create } = require('@apm-js-collab/code-transformer')
|
|
7
|
+
const Module = require('node:module')
|
|
8
|
+
const parse = require('module-details-from-path')
|
|
9
|
+
const getPackageVersion = require('./util/get-package-version')
|
|
10
|
+
const logger = require('./logger').child({ component: 'ModulePatch' })
|
|
11
|
+
|
|
12
|
+
class ModulePatch {
|
|
13
|
+
constructor({ packages = new Set(), instrumentations = [] } = {}) {
|
|
14
|
+
this.packages = packages
|
|
15
|
+
this.instrumentator = create(instrumentations)
|
|
16
|
+
this.transformers = new Map()
|
|
17
|
+
this.resolve = Module._resolveFilename
|
|
18
|
+
this.compile = Module.prototype._compile
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Patches the Node.js module class methods that are responsible for resolving filePaths and compiling code.
|
|
23
|
+
* If a module is found that has an instrumentator, it will transform the code before compiling it
|
|
24
|
+
* with tracing channel methods.
|
|
25
|
+
*/
|
|
26
|
+
patch() {
|
|
27
|
+
const self = this
|
|
28
|
+
Module._resolveFilename = function wrappedResolveFileName() {
|
|
29
|
+
const resolvedName = self.resolve.apply(this, arguments)
|
|
30
|
+
const resolvedModule = parse(resolvedName)
|
|
31
|
+
if (resolvedModule && self.packages.has(resolvedModule.name)) {
|
|
32
|
+
const version = getPackageVersion(resolvedModule.basedir, resolvedModule.name)
|
|
33
|
+
const transformer = self.instrumentator.getTransformer(resolvedModule.name, version, resolvedModule.path)
|
|
34
|
+
if (transformer) {
|
|
35
|
+
self.transformers.set(resolvedName, transformer)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return resolvedName
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
Module.prototype._compile = function wrappedCompile(...args) {
|
|
42
|
+
const [content, filename] = args
|
|
43
|
+
if (self.transformers.has(filename)) {
|
|
44
|
+
const transformer = self.transformers.get(filename)
|
|
45
|
+
try {
|
|
46
|
+
const transformedCode = transformer.transform(content, 'unknown')
|
|
47
|
+
args[0] = transformedCode
|
|
48
|
+
} catch (error) {
|
|
49
|
+
logger.error({ error }, `Error transforming module ${filename}`)
|
|
50
|
+
} finally {
|
|
51
|
+
transformer.free()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return self.compile.apply(this, args)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Clears all the transformers and restores the original Module methods that were wrapped.
|
|
61
|
+
* **Note**: This is intended to be used in testing only.
|
|
62
|
+
*/
|
|
63
|
+
unpatch() {
|
|
64
|
+
this.transformers.clear()
|
|
65
|
+
Module._resolveFilename = this.resolve
|
|
66
|
+
Module.prototype._compile = this.compile
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = ModulePatch
|
|
@@ -54,9 +54,7 @@ class AwsLambda {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
_detectEventType(event) {
|
|
57
|
-
const pathMatch = (obj, path) =>
|
|
58
|
-
return get(obj, path, null) !== null
|
|
59
|
-
}
|
|
57
|
+
const pathMatch = (obj, path) => get(obj, path, null) !== null
|
|
60
58
|
|
|
61
59
|
for (const typeInfo of Object.values(EVENT_SOURCE_INFO)) {
|
|
62
60
|
if (typeInfo.required_keys.every((path) => pathMatch(event, path))) {
|
|
@@ -34,9 +34,7 @@ function wrapMounter(spec, shim, fn, fnName) {
|
|
|
34
34
|
routeIdx = null
|
|
35
35
|
route = null
|
|
36
36
|
} else if (shim.isArray(route)) {
|
|
37
|
-
route = route.map((routeArg) =>
|
|
38
|
-
return shim._routeParser.call(this, shim, fn, fnName, routeArg)
|
|
39
|
-
})
|
|
37
|
+
route = route.map((routeArg) => shim._routeParser.call(this, shim, fn, fnName, routeArg))
|
|
40
38
|
} else {
|
|
41
39
|
route = shim._routeParser.call(this, shim, fn, fnName, route)
|
|
42
40
|
}
|
package/lib/shimmer.js
CHANGED
|
@@ -23,7 +23,9 @@ let pkgsToHook = []
|
|
|
23
23
|
const NAMES = require('./metrics/names')
|
|
24
24
|
const symbols = require('./symbols')
|
|
25
25
|
const { unsubscribe } = require('./instrumentation/undici')
|
|
26
|
-
const
|
|
26
|
+
const subscriptions = require('./subscriber-configs')
|
|
27
|
+
const createSubscriberConfigs = require('./subscribers/create-config')
|
|
28
|
+
const ModulePatch = require('./patch-module')
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* Unwrapping is only likely to be used by test code, and is a fairly drastic
|
|
@@ -329,14 +331,32 @@ const shimmer = (module.exports = {
|
|
|
329
331
|
}
|
|
330
332
|
|
|
331
333
|
pkgsToHook = []
|
|
332
|
-
|
|
334
|
+
if (agent?.config?.opentelemetry_bridge?.enabled === true) {
|
|
335
|
+
// We must lazy load the method in order to support stripping of the
|
|
336
|
+
// `@opentelemetry` packages for slim builds.
|
|
337
|
+
require('./otel/setup').teardownOtel(agent)
|
|
338
|
+
}
|
|
333
339
|
unsubscribe()
|
|
340
|
+
if (this._subscribers) {
|
|
341
|
+
shimmer.teardownSubscribers()
|
|
342
|
+
}
|
|
343
|
+
if (this._modulePatch) {
|
|
344
|
+
this._modulePatch.unpatch()
|
|
345
|
+
}
|
|
334
346
|
},
|
|
335
347
|
|
|
336
348
|
bootstrapInstrumentation: function bootstrapInstrumentation(agent) {
|
|
349
|
+
const subscriberConfigs = createSubscriberConfigs(subscriptions)
|
|
350
|
+
this._modulePatch = new ModulePatch(subscriberConfigs)
|
|
351
|
+
this._modulePatch.patch()
|
|
337
352
|
shimmer.registerCoreInstrumentation(agent)
|
|
338
353
|
shimmer.registerThirdPartyInstrumentation(agent)
|
|
339
|
-
|
|
354
|
+
shimmer.setupSubscribers(agent)
|
|
355
|
+
if (agent?.config?.opentelemetry_bridge?.enabled === true) {
|
|
356
|
+
// We must lazy load the method in order to support stripping of the
|
|
357
|
+
// `@opentelemetry` packages for slim builds.
|
|
358
|
+
require('./otel/setup').setupOtel(agent)
|
|
359
|
+
}
|
|
340
360
|
},
|
|
341
361
|
|
|
342
362
|
registerInstrumentation: function registerInstrumentation(opts) {
|
|
@@ -371,6 +391,35 @@ const shimmer = (module.exports = {
|
|
|
371
391
|
|
|
372
392
|
registeredInstrumentations: new InstrumentationTracker(),
|
|
373
393
|
|
|
394
|
+
setupSubscribers: function setupSubscribers(agent) {
|
|
395
|
+
this._subscribers = {}
|
|
396
|
+
for (const subscriberConfigList of Object.values(subscriptions)) {
|
|
397
|
+
for (const subscriberConfig of subscriberConfigList) {
|
|
398
|
+
const Subscriber = require(`./subscribers/${subscriberConfig.path}`)
|
|
399
|
+
const subscriber = new Subscriber({ agent, logger })
|
|
400
|
+
if (subscriber.enabled === false) {
|
|
401
|
+
logger.debug(
|
|
402
|
+
'Skipping subscriber %s because it is disabled in the config',
|
|
403
|
+
subscriber.id
|
|
404
|
+
)
|
|
405
|
+
continue
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
subscriber.enable()
|
|
409
|
+
subscriber.subscribe()
|
|
410
|
+
this._subscribers[subscriber.id] = subscriber
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
teardownSubscribers: function teardownSubscribers() {
|
|
416
|
+
for (const subscriber of Object.values(this._subscribers)) {
|
|
417
|
+
subscriber.disable()
|
|
418
|
+
subscriber.unsubscribe()
|
|
419
|
+
}
|
|
420
|
+
this._subscribers = {}
|
|
421
|
+
},
|
|
422
|
+
|
|
374
423
|
/**
|
|
375
424
|
* NOT FOR USE IN PRODUCTION CODE
|
|
376
425
|
*
|
|
@@ -432,8 +481,7 @@ const shimmer = (module.exports = {
|
|
|
432
481
|
isInstrumented(moduleName, resolvedName) {
|
|
433
482
|
const allItems = shimmer.registeredInstrumentations.getAllByName(moduleName)
|
|
434
483
|
const items = allItems.filter(
|
|
435
|
-
(item) =>
|
|
436
|
-
item.instrumentation.resolvedName === resolvedName && item.meta.instrumented === true
|
|
484
|
+
(item) => item.instrumentation.resolvedName === resolvedName && item.meta.instrumented === true
|
|
437
485
|
)
|
|
438
486
|
return items.length === allItems.length
|
|
439
487
|
},
|
|
@@ -669,12 +717,12 @@ function _firstPartyInstrumentation(agent, fileName, shim, nodule, moduleName) {
|
|
|
669
717
|
* to load the module), as well as the `onRequire` and `onError` hooks to
|
|
670
718
|
* attach to the module.
|
|
671
719
|
*
|
|
672
|
-
* @param {object} agent
|
|
720
|
+
* @param {object} agent agent instance
|
|
673
721
|
* @param {object} nodule The newly loaded module.
|
|
674
722
|
* @param {string} name The simple name used to load the module.
|
|
675
723
|
* @param {string} resolvedName The full file system path to the module.
|
|
676
|
-
* @param {object} [esmResolver]
|
|
677
|
-
* @returns {*|Object|undefined}
|
|
724
|
+
* @param {object} [esmResolver] If the module was loaded via ESM
|
|
725
|
+
* @returns {*|Object|undefined} The instrumented module, or the original
|
|
678
726
|
* @private
|
|
679
727
|
*/
|
|
680
728
|
function _postLoad(agent, nodule, name, resolvedName, esmResolver) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
// The expected export of these files is:
|
|
9
|
+
// 'package-name': [ { path: 'subscriberPath', instrumentations: [] }, ... ]
|
|
10
|
+
const subscribers = {
|
|
11
|
+
...require('./subscribers/elasticsearch/config'),
|
|
12
|
+
...require('./subscribers/ioredis/config'),
|
|
13
|
+
...require('./subscribers/mcp-sdk/config'),
|
|
14
|
+
...require('./subscribers/pino/config'),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = subscribers
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const Subscriber = require('./base')
|
|
7
|
+
const { isApplicationLoggingEnabled, isLocalDecoratingEnabled, isLogForwardingEnabled, isMetricsEnabled, createModuleUsageMetric, incrementLoggingLinesMetrics } = require('../util/application-logging')
|
|
8
|
+
|
|
9
|
+
class ApplicationLogsSubscriber extends Subscriber {
|
|
10
|
+
constructor({ agent, logger, packageName, channelName, }) {
|
|
11
|
+
super({ agent, logger, packageName, channelName, })
|
|
12
|
+
this.requireActiveTx = false
|
|
13
|
+
this.libMetricCreated = false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get enabled() {
|
|
17
|
+
return super.enabled && isApplicationLoggingEnabled(this.config)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The intent of this method is to create a module usage metric
|
|
22
|
+
* but only once. You should call this in your handler.
|
|
23
|
+
* We cannot recreate creating this metric on require of a logging library
|
|
24
|
+
* because we no longer monkey patch but subscribe to events.
|
|
25
|
+
* @param lib
|
|
26
|
+
*/
|
|
27
|
+
createModuleUsageMetric(lib) {
|
|
28
|
+
if (this.libMetricCreated === false) {
|
|
29
|
+
createModuleUsageMetric(lib, this.agent.metrics)
|
|
30
|
+
this.libMetricCreated = true
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
decorateLogLine() {
|
|
35
|
+
if (isLocalDecoratingEnabled(this.config)) {
|
|
36
|
+
return this.agent.getNRLinkingMetadata()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
incrementLinesMetric(level) {
|
|
41
|
+
if (isMetricsEnabled(this.config)) {
|
|
42
|
+
incrementLoggingLinesMetrics(level, this.agent.metrics)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
forwardLogLine(data) {
|
|
47
|
+
if (isLogForwardingEnabled(this.config, this.agent)) {
|
|
48
|
+
const ctx = this.agent.tracer.getContext()
|
|
49
|
+
const formatLogLine = this.reformatLogLine(data, ctx)
|
|
50
|
+
this.agent.logs.add(formatLogLine)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = ApplicationLogsSubscriber
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
8
|
+
const { tracingChannel } = require('node:diagnostics_channel')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The baseline parameters available to all subscribers.
|
|
12
|
+
*
|
|
13
|
+
* @typedef {object} SubscriberParams
|
|
14
|
+
* @property {object} agent A New Relic Node.js agent instance.
|
|
15
|
+
* @property {object} logger An agent logger instance.
|
|
16
|
+
* @property {string} packageName The npm installable name for the package
|
|
17
|
+
* being instrumented. This is what a developer would provide to the `require`
|
|
18
|
+
* function.
|
|
19
|
+
* @property {string} channelName A unique name for the diagnostics channel
|
|
20
|
+
* that will be created and monitored.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @property {object} agent A New Relic Node.js agent instance.
|
|
25
|
+
* @property {object} logger An agent logger instance.
|
|
26
|
+
* @property {object} config The agent configuration object.
|
|
27
|
+
* @property {string} packageName The name of the module being instrumented.
|
|
28
|
+
* This is the same string one would pass to the `require` function.
|
|
29
|
+
* @property {string} channelName A unique name for the diagnostics channel
|
|
30
|
+
* that will be registered.
|
|
31
|
+
* @property {string[]} [events=[]] Set of tracing channel event names to
|
|
32
|
+
* register handlers for. For any name in the set, a corresponding method
|
|
33
|
+
* must exist on the subscriber instance. The method will be passed the
|
|
34
|
+
* event object. Possible event names are `start`, `end`, `asyncStart`,
|
|
35
|
+
* `asyncEnd`, and `error`. @link https://nodejs.org/api/diagnostics_channel.html#class-tracingchannel
|
|
36
|
+
* @property {boolean} [opaque=false] If true, any children segments will not be created
|
|
37
|
+
* @property {boolean} [internal=false] If true, any children segments from the same library will not be created
|
|
38
|
+
* @property {string} [prefix='orchestrion:'] String to prepend to diagnostics
|
|
39
|
+
* channel event names. This provides a namespace for the events we are
|
|
40
|
+
* injecting into a module.
|
|
41
|
+
* @property {boolean} [requireActiveTx=true] If true, the subscriber will only handle events when there is an active transaction.
|
|
42
|
+
* @property {string} id A unique identifier for the subscriber, combining the prefix, package name, and channel name.
|
|
43
|
+
* @property {TracingChannel} channel The tracing channel instance this subscriber will be monitoring.
|
|
44
|
+
* @property {AsyncLocalStorage} store The async local storage instance used for context management.
|
|
45
|
+
*/
|
|
46
|
+
class Subscriber {
|
|
47
|
+
constructor({ agent, logger, packageName, channelName }) {
|
|
48
|
+
this.agent = agent
|
|
49
|
+
this.logger = logger.child({ component: `${packageName}-subscriber` })
|
|
50
|
+
this.config = agent.config
|
|
51
|
+
this.packageName = packageName
|
|
52
|
+
this.channelName = channelName
|
|
53
|
+
this.events = []
|
|
54
|
+
this.opaque = false
|
|
55
|
+
this.internal = false
|
|
56
|
+
this.prefix = 'orchestrion:'
|
|
57
|
+
this.requireActiveTx = true
|
|
58
|
+
this.id = `${this.prefix}${this.packageName}:${this.channelName}`
|
|
59
|
+
this.channel = tracingChannel(this.id)
|
|
60
|
+
this.store = agent.tracer._contextManager._asyncLocalStorage
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
shouldCreateSegment(parent) {
|
|
64
|
+
return !(parent?.opaque ||
|
|
65
|
+
(this.internal && this.packageName === parent?.shimId)
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Creates a segment with a name, parent, transaction and optional recorder.
|
|
71
|
+
* If the segment is successfully created, it will be started and added to the context.
|
|
72
|
+
* @param {Object} params - Parameters for creating the segment
|
|
73
|
+
* @param {string} params.name - The name of the segment
|
|
74
|
+
* @param {Object} params.recorder - Optional recorder for the segment
|
|
75
|
+
* @param {Context} params.ctx - The context containing the parent segment and transaction
|
|
76
|
+
* @returns {Context} - The updated context with the new segment or existing context if segment creation fails
|
|
77
|
+
*/
|
|
78
|
+
createSegment({ name, recorder, ctx }) {
|
|
79
|
+
const parent = ctx?.segment
|
|
80
|
+
|
|
81
|
+
if (this.shouldCreateSegment(parent) === false) {
|
|
82
|
+
this.logger.trace('Skipping segment creation for %s, %s(parent) is of the same package: %s and incoming segment is marked as internal', name, parent?.name, this.packageName)
|
|
83
|
+
return ctx
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const segment = this.agent.tracer.createSegment({
|
|
87
|
+
name,
|
|
88
|
+
parent,
|
|
89
|
+
recorder,
|
|
90
|
+
transaction: ctx?.transaction,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
if (segment) {
|
|
94
|
+
segment.opaque = this.opaque
|
|
95
|
+
segment.shimId = this.packageName
|
|
96
|
+
segment.start()
|
|
97
|
+
this.logger.trace('Created segment %s', name)
|
|
98
|
+
this.addAttributes(segment)
|
|
99
|
+
const newCtx = ctx.enterSegment({ segment })
|
|
100
|
+
return newCtx
|
|
101
|
+
} else {
|
|
102
|
+
this.logger.trace('Failed to create segment for %s', name)
|
|
103
|
+
return ctx
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* By default this is a no-op, but can be overridden by subclasses
|
|
109
|
+
* @param {Segment} segment - The segment to which attributes will be added
|
|
110
|
+
* @returns {void}
|
|
111
|
+
*/
|
|
112
|
+
addAttributes(segment) {
|
|
113
|
+
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Checks if the subscriber is enabled based on the agent's configuration.
|
|
118
|
+
*/
|
|
119
|
+
get enabled() {
|
|
120
|
+
return this.config.instrumentation[this.packageName].enabled === true
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Enables the subscriber by binding the store to the channel and setting up the handler.
|
|
125
|
+
* If the subscriber requires an active transaction, it will check the context before passing the event to the handler.
|
|
126
|
+
* @returns {Context} - The context after processing the event
|
|
127
|
+
*/
|
|
128
|
+
enable() {
|
|
129
|
+
this.channel.start.bindStore(this.store, (data) => {
|
|
130
|
+
const ctx = this.agent.tracer.getContext()
|
|
131
|
+
if (this.requireActiveTx && !ctx?.transaction?.isActive()) {
|
|
132
|
+
this.logger.debug('Not recording event for %s, transaction is not active', this.package)
|
|
133
|
+
return ctx
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return this.handler(data, ctx)
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Disables the subscriber by unbinding the store from the channel.
|
|
142
|
+
*/
|
|
143
|
+
disable() {
|
|
144
|
+
this.channel.start.unbindStore(this.store)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Common handler for when async events end.
|
|
149
|
+
* It gets the context and touches the segment if it exists.
|
|
150
|
+
*/
|
|
151
|
+
asyncEnd() {
|
|
152
|
+
const ctx = this.agent.tracer.getContext()
|
|
153
|
+
ctx?.segment?.touch()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/*
|
|
157
|
+
* Subscribes to the events defined in the `events` array.
|
|
158
|
+
*/
|
|
159
|
+
subscribe() {
|
|
160
|
+
this.subscriptions = this.events.reduce((events, curr) => {
|
|
161
|
+
events[curr] = this[curr].bind(this)
|
|
162
|
+
return events
|
|
163
|
+
}, {})
|
|
164
|
+
|
|
165
|
+
this.channel.subscribe(this.subscriptions)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Unsubscribes from the events defined in the `events` array..
|
|
170
|
+
*/
|
|
171
|
+
unsubscribe() {
|
|
172
|
+
this.channel.unsubscribe(this.subscriptions)
|
|
173
|
+
this.subscriptions = null
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = Subscriber
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Builds a set of packages and instrumentaions from a list of subscriber configurations.
|
|
10
|
+
*
|
|
11
|
+
* @param {Array} subscribers - An array of subscriber objects, each containing a package name and an array of instrumentations.
|
|
12
|
+
* @returns {Object} An object containing a Set of unique package names and an array of instrumentations.
|
|
13
|
+
*/
|
|
14
|
+
function createSubscribersConfig (subscribers = []) {
|
|
15
|
+
const packages = new Set()
|
|
16
|
+
const instrumentations = []
|
|
17
|
+
for (const [packageName, subscriberList] of Object.entries(subscribers)) {
|
|
18
|
+
packages.add(packageName)
|
|
19
|
+
for (const subscriber of subscriberList) {
|
|
20
|
+
instrumentations.push(...subscriber.instrumentations)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { packages, instrumentations }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = createSubscribersConfig
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DbSubscriber = require('./db')
|
|
7
|
+
const recordOperationMetrics = require('../metrics/recorders/database-operation')
|
|
8
|
+
const { DB } = require('../metrics/names')
|
|
9
|
+
|
|
10
|
+
class DbOperationSubscriber extends DbSubscriber {
|
|
11
|
+
handler(data, ctx) {
|
|
12
|
+
const name = `${DB.OPERATION}/${this.system}/${this.operation}`
|
|
13
|
+
return this.createSegment({
|
|
14
|
+
name,
|
|
15
|
+
ctx,
|
|
16
|
+
recorder: recordOperationMetrics.bind(this),
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = DbOperationSubscriber
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DbSubscriber = require('./db')
|
|
7
|
+
const recordQueryMetrics = require('../metrics/recorders/database')
|
|
8
|
+
const { DB } = require('../metrics/names')
|
|
9
|
+
const ParsedStatement = require('../db/parsed-statement')
|
|
10
|
+
const parseSql = require('../db/query-parsers/sql')
|
|
11
|
+
|
|
12
|
+
class DbQuerySubscriber extends DbSubscriber {
|
|
13
|
+
handler(data, ctx) {
|
|
14
|
+
const queryString = this.queryString
|
|
15
|
+
const parsed = this.parseQueryString(queryString)
|
|
16
|
+
const name = `${DB.STATEMENT}/${this.system}/${parsed.collection}/${parsed.operation}`
|
|
17
|
+
return this.createSegment({
|
|
18
|
+
name,
|
|
19
|
+
ctx,
|
|
20
|
+
recorder: recordQueryMetrics.bind(parsed),
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
parseQueryString(queryString) {
|
|
25
|
+
const parsed = this.parseQuery(queryString)
|
|
26
|
+
let collection = parsed.collection
|
|
27
|
+
// strip enclosing special characters from collection (table) name
|
|
28
|
+
if (typeof collection === 'string' && collection.length > 2) {
|
|
29
|
+
if (/^[[{'"`]/.test(collection)) {
|
|
30
|
+
collection = collection.substring(1)
|
|
31
|
+
}
|
|
32
|
+
if (/[\]}'"`]$/.test(collection)) {
|
|
33
|
+
collection = collection.substring(0, collection.length - 1)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const queryRecorded =
|
|
38
|
+
this.config.transaction_tracer.record_sql === 'raw' ||
|
|
39
|
+
this.config.transaction_tracer.record_sql === 'obfuscated'
|
|
40
|
+
|
|
41
|
+
return new ParsedStatement(
|
|
42
|
+
this._metrics.PREFIX,
|
|
43
|
+
parsed.operation,
|
|
44
|
+
collection,
|
|
45
|
+
queryRecorded ? parsed.query : null
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
parseQuery(queryString) {
|
|
50
|
+
return parseSql(queryString)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = DbQuerySubscriber
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const Subscriber = require('./base')
|
|
7
|
+
const { ALL, DB } = require('../metrics/names')
|
|
8
|
+
const urltils = require('../util/urltils')
|
|
9
|
+
|
|
10
|
+
class DbSubscriber extends Subscriber {
|
|
11
|
+
constructor({ agent, logger, packageName, channelName, system }) {
|
|
12
|
+
super({ agent, logger, packageName, channelName })
|
|
13
|
+
this.system = system
|
|
14
|
+
// must be prefixed with `_` as that's what the metrics recorder expects
|
|
15
|
+
this._metrics = {
|
|
16
|
+
PREFIX: this.system,
|
|
17
|
+
ALL: `${DB.PREFIX}${this.system}/${ALL}`
|
|
18
|
+
}
|
|
19
|
+
this.instanceKeys = ['host', 'port_path_or_id']
|
|
20
|
+
this.hostKey = 'host'
|
|
21
|
+
this.dbNameKey = 'database_name'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get instanceReporting() {
|
|
25
|
+
return this.config.datastore_tracer.instance_reporting.enabled
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get dbNameReporting() {
|
|
29
|
+
return this.config.datastore_tracer.database_name_reporting.enabled
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
addAttributes(segment) {
|
|
33
|
+
for (let [key, value] of Object.entries(this.parameters)) {
|
|
34
|
+
if (this.instanceKeys.includes(key) && !this.instanceReporting) {
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (key === this.dbNameKey && !this.dbNameReporting) {
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (key === this.hostKey && urltils.isLocalhost(value)) {
|
|
43
|
+
// eslint-disable-next-line sonarjs/updated-loop-counter
|
|
44
|
+
value = this.config.getHostnameSafe()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (key === this.dbNameKey && typeof value === 'number') {
|
|
48
|
+
// eslint-disable-next-line sonarjs/updated-loop-counter
|
|
49
|
+
value = String(value)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
segment.addAttribute(key, value)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = DbSubscriber
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
'@elastic/elasticsearch': [{
|
|
8
|
+
path: './elasticsearch/elasticsearch.js',
|
|
9
|
+
instrumentations: [
|
|
10
|
+
{
|
|
11
|
+
channelName: 'nr_request',
|
|
12
|
+
module: { name: '@elastic/elasticsearch', versionRange: '>=7.16.0', filePath: 'lib/Transport.js' },
|
|
13
|
+
functionQuery: {
|
|
14
|
+
className: 'Transport',
|
|
15
|
+
methodName: 'request',
|
|
16
|
+
kind: 'Async'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}],
|
|
21
|
+
'@elastic/transport': [{
|
|
22
|
+
path: './elasticsearch/transport.js',
|
|
23
|
+
instrumentations: [
|
|
24
|
+
{
|
|
25
|
+
channelName: 'nr_request',
|
|
26
|
+
module: { name: '@elastic/transport', versionRange: '>=8.0.0', filePath: 'lib/Transport.js' },
|
|
27
|
+
functionQuery: {
|
|
28
|
+
className: 'Transport',
|
|
29
|
+
methodName: 'request',
|
|
30
|
+
kind: 'Async'
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}],
|
|
35
|
+
'@opensearch-project/opensearch': [{
|
|
36
|
+
path: './elasticsearch/opensearch.js',
|
|
37
|
+
instrumentations: [
|
|
38
|
+
{
|
|
39
|
+
channelName: 'nr_request',
|
|
40
|
+
module: { name: '@opensearch-project/opensearch', versionRange: '>=2.1.0', filePath: 'lib/Transport.js' },
|
|
41
|
+
functionQuery: {
|
|
42
|
+
className: 'Transport',
|
|
43
|
+
methodName: 'request',
|
|
44
|
+
kind: 'Async'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}]
|
|
49
|
+
}
|