newrelic 14.1.2 → 14.2.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 +51 -0
- package/THIRD_PARTY_NOTICES.md +15 -15
- package/index.js +2 -2
- package/lib/collector/http-agents.js +5 -2
- package/lib/config/default.js +162 -139
- package/lib/config/index.js +22 -2
- package/lib/instrumentations.js +0 -1
- package/lib/profiling/index.js +2 -1
- package/lib/profiling/profilers/cpu.js +225 -3
- package/lib/subscriber-configs.js +1 -0
- package/lib/subscribers/base.js +14 -7
- package/lib/subscribers/db.js +11 -2
- package/lib/subscribers/mongodb/bulk.js +79 -0
- package/lib/subscribers/mongodb/collection.js +113 -0
- package/lib/subscribers/mongodb/config.js +72 -0
- package/lib/subscribers/mongodb/cursor.js +76 -0
- package/lib/subscribers/mongodb/db.js +101 -0
- package/lib/subscribers/mongodb/utils/admin-commands.js +19 -0
- package/lib/subscribers/mongodb/utils/get-host-details.js +85 -0
- package/lib/subscribers/mongodb/wrapper.js +109 -0
- package/lib/subscribers/resolve-package-version.js +42 -12
- package/lib/subscribers/winston/configure.js +13 -5
- package/lib/subscribers/winston/nr-winston-transport.js +6 -3
- package/lib/transaction/tracecontext.js +6 -19
- package/lib/transaction/tracer/index.js +16 -0
- package/package.json +5 -5
- package/lib/instrumentation/mongodb/common.js +0 -272
- package/lib/instrumentation/mongodb/constants.js +0 -87
- package/lib/instrumentation/mongodb/v4-mongo.js +0 -75
- package/lib/instrumentation/mongodb.js +0 -46
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const Wrapper = require('./wrapper.js')
|
|
9
|
+
const getHostDetails = require('./utils/get-host-details.js')
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
DB: DB_METRIC_CONSTANTS,
|
|
13
|
+
MONGODB: MONGODB_METRIC_CONSTANTS
|
|
14
|
+
} = require('#agentlib/metrics/names.js')
|
|
15
|
+
|
|
16
|
+
const ADMIN_COMMANDS = require('./utils/admin-commands.js')
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The "Db" class is the primary entrypoint into the mongodb client. Some
|
|
20
|
+
* methods are not present on newer versions of the class, e.g. `addUser`
|
|
21
|
+
* got removed in favor of a direct `command` invocation. This list is a list
|
|
22
|
+
* of all methods we are interested in instrumenting, regardless of client
|
|
23
|
+
* version. As we iterate through the list we will perform a presence check
|
|
24
|
+
* for each method.
|
|
25
|
+
*
|
|
26
|
+
* @type {string[]}
|
|
27
|
+
*/
|
|
28
|
+
const DB_METHODS = [
|
|
29
|
+
'_executeInsertCommand',
|
|
30
|
+
'_executeQueryCommand',
|
|
31
|
+
'addUser',
|
|
32
|
+
'authenticate',
|
|
33
|
+
'collection',
|
|
34
|
+
'collectionNames',
|
|
35
|
+
'collections',
|
|
36
|
+
'command',
|
|
37
|
+
'createCollection',
|
|
38
|
+
'createIndex',
|
|
39
|
+
'cursorInfo',
|
|
40
|
+
'dereference',
|
|
41
|
+
'dropCollection',
|
|
42
|
+
'dropDatabase',
|
|
43
|
+
'dropIndex',
|
|
44
|
+
'ensureIndex',
|
|
45
|
+
'eval',
|
|
46
|
+
'executeDbAdminCommand',
|
|
47
|
+
'indexInformation',
|
|
48
|
+
'logout',
|
|
49
|
+
'open',
|
|
50
|
+
'reIndex',
|
|
51
|
+
'removeUser',
|
|
52
|
+
'renameCollection',
|
|
53
|
+
'stats'
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
module.exports = class DbClassSubscriber extends Wrapper {
|
|
57
|
+
constructor({ agent, logger }) {
|
|
58
|
+
super({
|
|
59
|
+
agent,
|
|
60
|
+
logger,
|
|
61
|
+
channelName: 'nr_db',
|
|
62
|
+
packageName: 'mongodb',
|
|
63
|
+
system: MONGODB_METRIC_CONSTANTS.PREFIX
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
end(data, ctx) {
|
|
68
|
+
const { self: dbClient, arguments: args } = data
|
|
69
|
+
const [dbInstance, dbName] = args
|
|
70
|
+
const details = getHostDetails(dbInstance)
|
|
71
|
+
this.parameters = {
|
|
72
|
+
host: details.host,
|
|
73
|
+
port_path_or_id: details.port_path_or_id,
|
|
74
|
+
database_name: dbName,
|
|
75
|
+
product: this.system
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const method of DB_METHODS) {
|
|
79
|
+
if (typeof dbClient[method] !== 'function') {
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.wrapDatabaseMethod(dbClient, method, {
|
|
84
|
+
getSegmentName: (method) => `${DB_METRIC_CONSTANTS.OPERATION}/${this.system}/${method}`,
|
|
85
|
+
getRecorderContext: (method) => {
|
|
86
|
+
return {
|
|
87
|
+
operation: method,
|
|
88
|
+
type: this.type
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
getSegmentAttributes: (method) => (
|
|
92
|
+
ADMIN_COMMANDS.includes(method)
|
|
93
|
+
? { database_name: 'admin' }
|
|
94
|
+
: {}
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return ctx
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Represents the list of methods/commmands that are available in the MongoDB
|
|
10
|
+
* client which target the "admin" collection instead of the actually selected
|
|
11
|
+
* collection.
|
|
12
|
+
*
|
|
13
|
+
* @see https://jira.mongodb.org/browse/NODE-827
|
|
14
|
+
* @type {string[]}
|
|
15
|
+
*/
|
|
16
|
+
module.exports = [
|
|
17
|
+
'rename',
|
|
18
|
+
'renameCollection'
|
|
19
|
+
]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const LOCALHOST_ALIASES = [
|
|
9
|
+
'127.0.0.1',
|
|
10
|
+
'::1',
|
|
11
|
+
'[::1]'
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {object} MongodbHostDetails
|
|
16
|
+
* @property {string} [database_name] The name of the database.
|
|
17
|
+
* @property {string} host The first host being targeted by the connection.
|
|
18
|
+
* @property {string|number} port_path_or_id The destination port associated
|
|
19
|
+
* with `host`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Retrieves the connection details (host, port, collection_name) from the
|
|
24
|
+
* provided MongoDB object instance. Over versions of the driver, the location
|
|
25
|
+
* of these details get moved around. So we need to inspect various locations
|
|
26
|
+
* for availability and use the first one we can find.
|
|
27
|
+
*
|
|
28
|
+
* Important: in the case where the `mongoObject` is a direct instance of
|
|
29
|
+
* `MongoClient`, or other object that does not include database name details,
|
|
30
|
+
* the `database_name` field will be omitted. You must acquire this through
|
|
31
|
+
* other means.
|
|
32
|
+
*
|
|
33
|
+
* @param {object} mongoObject The MongoDB driver object to get the details
|
|
34
|
+
* from, e.g. an 'AbstractCursor' or 'BulkOperationBase' instance.
|
|
35
|
+
*
|
|
36
|
+
* @returns {MongodbHostDetails}
|
|
37
|
+
*/
|
|
38
|
+
module.exports = function getHostDetails(mongoObject) {
|
|
39
|
+
// We prefer to get the details from the "client" object. This is the object
|
|
40
|
+
// the driver returns when you do `require('mongodb').MongoClient.connect()`.
|
|
41
|
+
|
|
42
|
+
let databaseName
|
|
43
|
+
let host
|
|
44
|
+
let port
|
|
45
|
+
|
|
46
|
+
if (mongoObject.constructor.name === 'MongoClient') {
|
|
47
|
+
host = mongoObject.options.hosts[0].host
|
|
48
|
+
port = mongoObject.options.hosts[0].port
|
|
49
|
+
} else if (mongoObject?.databaseName && mongoObject?.client) {
|
|
50
|
+
// Direct `Db` instance.
|
|
51
|
+
databaseName = mongoObject.databaseName
|
|
52
|
+
host = mongoObject.client.options.hosts[0].host
|
|
53
|
+
port = mongoObject.client.options.hosts[0].port
|
|
54
|
+
} else if (mongoObject.dbName && mongoObject.s?.db?.s?.client?.options) {
|
|
55
|
+
// Likely an instance of `Collection`.
|
|
56
|
+
databaseName = mongoObject.dbName
|
|
57
|
+
host = mongoObject.s.db.s.client.options.hosts[0].host
|
|
58
|
+
port = mongoObject.s.db.s.client.options.hosts[0].port
|
|
59
|
+
} else if (mongoObject.databaseName && mongoObject.s?.client?.options) {
|
|
60
|
+
// Some `mongodb@4` object.
|
|
61
|
+
databaseName = mongoObject.databaseName
|
|
62
|
+
host = mongoObject.s.client.options.hosts[0].host
|
|
63
|
+
port = mongoObject.s.client.options.hosts[0].port
|
|
64
|
+
} else if (mongoObject?.s?.db && mongoObject.s.db.client) {
|
|
65
|
+
// mongodb@5 bulk operation
|
|
66
|
+
databaseName = mongoObject.s.db.databaseName
|
|
67
|
+
host = mongoObject.s.db.client.options.hosts[0].host
|
|
68
|
+
port = mongoObject.s.db.client.options.hosts[0].port
|
|
69
|
+
} else if (mongoObject.constructor.name === 'Topology') {
|
|
70
|
+
// A "topology" object (from ancient times). Remove this when we drop
|
|
71
|
+
// support for v4.
|
|
72
|
+
host = mongoObject.s.options.hosts[0].host
|
|
73
|
+
port = mongoObject.s.options.hosts[0].port
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (LOCALHOST_ALIASES.includes(host) === true) {
|
|
77
|
+
host = 'localhost'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
database_name: databaseName,
|
|
82
|
+
port_path_or_id: port,
|
|
83
|
+
host
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const DbSubscriber = require('../db.js')
|
|
9
|
+
const databaseRecorder = require('#agentlib/metrics/recorders/database.js')
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* This class provides a baseline for the MongoDB instrumentations that
|
|
13
|
+
* share a lot of common code when wrapping methods, but require some slight
|
|
14
|
+
* variations in the collected data. Anything extending this class is
|
|
15
|
+
* expected to leverage the {@link #wrapDatabaseMethod} method.
|
|
16
|
+
*/
|
|
17
|
+
module.exports = class WrapperSubscriber extends DbSubscriber {
|
|
18
|
+
constructor(params) {
|
|
19
|
+
super(params)
|
|
20
|
+
|
|
21
|
+
this.events = ['end']
|
|
22
|
+
// We set `opaque` to true because the library uses its own methods
|
|
23
|
+
// internally, and we only care about the entry point. That is, if the
|
|
24
|
+
// code is `.findOne('something')`, the method will invoke `.next`
|
|
25
|
+
// internally. We don't care to track `.next` with its own reported segment.
|
|
26
|
+
// We only want to record the overall operation of `.findOne`.
|
|
27
|
+
//
|
|
28
|
+
// This is the case for all instrumentations that inherit from this class.
|
|
29
|
+
// The details change around the methods, but the desired outcome
|
|
30
|
+
// is the same.
|
|
31
|
+
this.opaque = true
|
|
32
|
+
|
|
33
|
+
// The database metrics recorder reads `this.type` to get the name of the
|
|
34
|
+
// database system.
|
|
35
|
+
this.type = this.system
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Creates a standard database operation wrapper for a method.
|
|
40
|
+
* This method provides a common pattern for instrumenting database operations
|
|
41
|
+
* with transaction validation, segment creation, and database recording.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} instance The instance with the method to wrap.
|
|
44
|
+
* @param {string} methodName The name of the method to wrap.
|
|
45
|
+
* @param {object} callbacks Functions used to retrieve context specific
|
|
46
|
+
* data.
|
|
47
|
+
* @param {Function} callbacks.getSegmentName Each subscriber names its
|
|
48
|
+
* segment slightly differently. This function should return the full
|
|
49
|
+
* string representation for the segment name.
|
|
50
|
+
* @param {Function} callbacks.getRecorderContext Each subscriber instruments
|
|
51
|
+
* multiple methods that can be invoked during the evaluation of one
|
|
52
|
+
* operation. As such, the instances cannot store details relevant to metrics
|
|
53
|
+
* generation on the instance itself. Thus, we need a scoped context object
|
|
54
|
+
* to provide to the metrics recorder function. This function is used to
|
|
55
|
+
* get a copy of that scoped context object.
|
|
56
|
+
* @param {Function} [callbacks.getSegmentAttributes] In most cases, the
|
|
57
|
+
* `addAttributes` method on the base class can be used to get a copy of
|
|
58
|
+
* attributes to add to the segment being recorded. But there are some cases
|
|
59
|
+
* where the attributes need to be calculated within the scope of the
|
|
60
|
+
* transaction and passed in to the `addAttributes` method. This function
|
|
61
|
+
* is used to get a copy of the attributes object to provide to the
|
|
62
|
+
* base method.
|
|
63
|
+
*/
|
|
64
|
+
wrapDatabaseMethod(instance, methodName, callbacks) {
|
|
65
|
+
const self = this
|
|
66
|
+
const orig = instance[methodName]
|
|
67
|
+
const {
|
|
68
|
+
getSegmentName,
|
|
69
|
+
getRecorderContext,
|
|
70
|
+
getSegmentAttributes = () => { return {} }
|
|
71
|
+
} = callbacks
|
|
72
|
+
|
|
73
|
+
instance[methodName] = function nrWrappedMethod(...args) {
|
|
74
|
+
let ctx = self.agent.tracer.getContext()
|
|
75
|
+
if (ctx.transaction == null || ctx.transaction.isActive() === false) {
|
|
76
|
+
self.logger.debug(
|
|
77
|
+
'Not recording function %s, not in a transaction',
|
|
78
|
+
methodName
|
|
79
|
+
)
|
|
80
|
+
return orig.apply(instance, args)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
self.logger.debug('Recording function %s', methodName)
|
|
84
|
+
|
|
85
|
+
ctx = self.createSegment({
|
|
86
|
+
name: getSegmentName(methodName),
|
|
87
|
+
recorder: function dbRecorder(segment, scope, transaction) {
|
|
88
|
+
const recorderContext = getRecorderContext(methodName)
|
|
89
|
+
return databaseRecorder.call(
|
|
90
|
+
recorderContext,
|
|
91
|
+
segment,
|
|
92
|
+
scope,
|
|
93
|
+
transaction
|
|
94
|
+
)
|
|
95
|
+
},
|
|
96
|
+
attributes: getSegmentAttributes(methodName),
|
|
97
|
+
ctx
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return self.agent.tracer.runInContext({
|
|
101
|
+
handler: orig,
|
|
102
|
+
context: ctx,
|
|
103
|
+
full: true,
|
|
104
|
+
thisArg: instance,
|
|
105
|
+
args
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -15,6 +15,22 @@ const modPathReg = /at .+ \((.+):\d+:\d+\)/
|
|
|
15
15
|
|
|
16
16
|
module.exports = resolveModuleVersion
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Captures the current call stack, dropping frames up to and including the
|
|
20
|
+
* `Channel.publish` frame so that the first returned frame belongs to the
|
|
21
|
+
* module that published the diagnostics channel event.
|
|
22
|
+
*
|
|
23
|
+
* @returns {string[]} The remaining stack frames.
|
|
24
|
+
*/
|
|
25
|
+
function defaultGetStack() {
|
|
26
|
+
const err = Error()
|
|
27
|
+
const stack = err.stack.split('\n')
|
|
28
|
+
do {
|
|
29
|
+
stack.shift()
|
|
30
|
+
} while (dcFuncFrame.test(stack[0]) === false && stack.length > 0)
|
|
31
|
+
return stack
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
/**
|
|
19
35
|
* Given a module name, attempt to read the version string from its
|
|
20
36
|
* associated package manifest. If the module is a built-in, or one that has
|
|
@@ -32,30 +48,44 @@ module.exports = resolveModuleVersion
|
|
|
32
48
|
* @param {string} moduleSpecifier What would be passed to `resolve()`.
|
|
33
49
|
* @param {object} [deps] Optional dependencies.
|
|
34
50
|
* @param {object} [deps.logger] Agent logger instance.
|
|
51
|
+
* @param {Function} [deps.getStack] Returns the current call stack as an array
|
|
52
|
+
* of frame strings. Exposed for testing so a synthetic publisher frame can be
|
|
53
|
+
* supplied.
|
|
35
54
|
*
|
|
36
55
|
* @returns {string} The version string from the package manifest or "unknown".
|
|
37
56
|
*/
|
|
38
|
-
function resolveModuleVersion(
|
|
57
|
+
function resolveModuleVersion(
|
|
58
|
+
moduleSpecifier,
|
|
59
|
+
{ logger = defaultLogger, getStack = defaultGetStack } = {}
|
|
60
|
+
) {
|
|
39
61
|
let pkgPath
|
|
40
62
|
// We'd prefer to use `require.resolve(moduleSpecifier)` here, but it gets
|
|
41
63
|
// a bit confused when there are non-standard module directories in play.
|
|
42
64
|
// Once we are able to refactor our "on require" metric recording to
|
|
43
65
|
// utilize `module.registerHooks`, we should be able to eliminate this
|
|
44
66
|
// slow algorithm.
|
|
45
|
-
const
|
|
46
|
-
const stack = err.stack.split('\n')
|
|
47
|
-
do {
|
|
48
|
-
stack.shift()
|
|
49
|
-
} while (dcFuncFrame.test(stack[0]) === false && stack.length > 0)
|
|
67
|
+
const stack = getStack()
|
|
50
68
|
const matches = modPathReg.exec(stack[1])
|
|
51
69
|
pkgPath = matches?.[1]
|
|
52
70
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
// When the diagnostics channel event is published by a Node.js built-in or
|
|
72
|
+
// bundled module (e.g. `node:internal/deps/undici/undici`), `pkgPath` is not
|
|
73
|
+
// a real filesystem path, so the directory walk below cannot use it. This
|
|
74
|
+
// happens on modern Node.js versions where userland `undici` reuses Node's
|
|
75
|
+
// internally bundled undici channels. In that case, resolve the userland
|
|
76
|
+
// package manifest directly to report the version the application installed.
|
|
77
|
+
if (!pkgPath || pkgPath.startsWith('node:')) {
|
|
78
|
+
try {
|
|
79
|
+
const { version } = require(`${moduleSpecifier}/package.json`)
|
|
80
|
+
logger.trace({ moduleSpecifier, version }, 'Resolved package version.')
|
|
81
|
+
return version ?? 'unknown'
|
|
82
|
+
} catch {
|
|
83
|
+
logger.warn(
|
|
84
|
+
{ moduleSpecifier },
|
|
85
|
+
'Could not resolve module path. Possibly a built-in or Node.js bundled module.'
|
|
86
|
+
)
|
|
87
|
+
return 'unknown'
|
|
88
|
+
}
|
|
59
89
|
}
|
|
60
90
|
|
|
61
91
|
const cwd = process.cwd()
|
|
@@ -12,11 +12,18 @@ const NrTransport = require('./nr-winston-transport.js')
|
|
|
12
12
|
* during initial construction and on any subsequent reconfiguration.
|
|
13
13
|
*
|
|
14
14
|
* Handles `NrTransport` injection (after `configure` runs, via `end`).
|
|
15
|
+
*
|
|
16
|
+
* @property {boolean} handlingExceptions Indicates if this instrumentation has
|
|
17
|
+
* already registered a global exception handler or not. We only need a single
|
|
18
|
+
* exception handler per process.
|
|
15
19
|
*/
|
|
16
20
|
module.exports = class WinstonConfigure extends ApplicationLogsSubscriber {
|
|
17
21
|
constructor({ agent, logger }) {
|
|
18
22
|
super({ agent, logger, channelName: 'nr_configure', packageName: 'winston' })
|
|
19
23
|
this.events = ['end']
|
|
24
|
+
// we only want to assign `handleExceptions` to the first NrTransport
|
|
25
|
+
// customers could be constructing multiple logger instances
|
|
26
|
+
this.handlingExceptions = false
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
handler(data, ctx) {
|
|
@@ -39,11 +46,12 @@ module.exports = class WinstonConfigure extends ApplicationLogsSubscriber {
|
|
|
39
46
|
const hasNrTransport = logger.transports?.some((t) => t.name === 'newrelic')
|
|
40
47
|
if (!hasNrTransport) {
|
|
41
48
|
const nrTransport = new NrTransport({ agent: this.agent })
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
nrTransport.handleExceptions =
|
|
49
|
+
// assigning `transport.handleExceptions` adds a global `process.on('uncaughtException')`
|
|
50
|
+
// handler. Doing it for more than one NrTransport instance will cause duplicate log lines
|
|
51
|
+
if (this.handlingExceptions === false && logger.transports.length > 0) {
|
|
52
|
+
// See: https://github.com/winstonjs/winston#handling-uncaught-exceptions-with-winston
|
|
53
|
+
nrTransport.handleExceptions = true
|
|
54
|
+
this.handlingExceptions = true
|
|
47
55
|
}
|
|
48
56
|
logger.add(nrTransport)
|
|
49
57
|
}
|
|
@@ -14,12 +14,15 @@ const { truncate } = require('../../util/application-logging')
|
|
|
14
14
|
*
|
|
15
15
|
* Note*: This copies the log line so no other transports will get the
|
|
16
16
|
* mutated data.
|
|
17
|
+
*
|
|
18
|
+
* @property {string} name name of the transport
|
|
19
|
+
* @property {Agent} agent the instance of agent
|
|
20
|
+
* @property {object} config the agent configuration
|
|
21
|
+
* @property {boolean} handleExceptions set in `lib/subscribers/winston/configure.js` for the first NrTransport instance
|
|
22
|
+
* to ensure that only one is handling uncaught exceptions in winston transports
|
|
17
23
|
*/
|
|
18
24
|
class NrTransport extends TransportStream {
|
|
19
25
|
constructor(opts = {}) {
|
|
20
|
-
// set this option to have winston handle uncaught exceptions
|
|
21
|
-
// See: https://github.com/winstonjs/winston#handling-uncaught-exceptions-with-winston
|
|
22
|
-
opts.handleExceptions = true
|
|
23
26
|
super(opts)
|
|
24
27
|
this.name = 'newrelic'
|
|
25
28
|
this.agent = opts.agent
|
|
@@ -72,10 +72,9 @@ class TraceContext {
|
|
|
72
72
|
/**
|
|
73
73
|
* Creates a W3C TraceContext tracestate header payload.
|
|
74
74
|
*
|
|
75
|
-
* @param {object} spanContext if passed in, it'll use this to construct tracestate. only used in otel bridge mode.
|
|
76
75
|
* @returns {string} tracestate, a hyphen-delimited string of trace information fields
|
|
77
76
|
*/
|
|
78
|
-
createTracestate(
|
|
77
|
+
createTracestate() {
|
|
79
78
|
const config = this.transaction.agent.config
|
|
80
79
|
const trustedAccountKey = config.trusted_account_key
|
|
81
80
|
const version = Tracestate.NR_TRACESTATE_VERSION
|
|
@@ -99,29 +98,17 @@ class TraceContext {
|
|
|
99
98
|
return this._traceStateRaw || ''
|
|
100
99
|
}
|
|
101
100
|
|
|
102
|
-
// If no segment/span is in context, we do not send one as
|
|
103
|
-
// we technically do not have a "span" on the agent side and
|
|
104
|
-
// this trace data is newrelic specific.
|
|
105
|
-
let spanId = ''
|
|
106
|
-
if (config.span_events.enabled) {
|
|
107
|
-
const segment = spanContext?.spanId ? { id: spanContext.spanId } : this.transaction.agent.tracer.getSegment()
|
|
108
|
-
if (segment) {
|
|
109
|
-
spanId = segment.id
|
|
110
|
-
} else {
|
|
111
|
-
logger.debug('No segment/span in context. Not sending spanId in tracestate.')
|
|
112
|
-
}
|
|
113
|
-
} else {
|
|
114
|
-
logger.trace('Span events disabled. Not sending spanId in tracestate.')
|
|
115
|
-
}
|
|
116
|
-
|
|
117
101
|
const transactionId = config.transaction_events.enabled ? this.transaction.id : ''
|
|
118
102
|
const sampled = this.transaction.sampled ? '1' : '0'
|
|
119
103
|
const priority = this.transaction.priority ? this.transaction.priority.toFixed(6) : ''
|
|
120
104
|
const timestamp = Date.now()
|
|
121
105
|
|
|
106
|
+
// As of 2026-06 we no longer emit spanId under any circumstance.
|
|
107
|
+
// So we omit it from the list here as this is the source
|
|
108
|
+
// when building the outgoing tracestate header.
|
|
122
109
|
const nrTraceState =
|
|
123
110
|
`${trustedAccountKey}@nr=${version}-${parentType}-${accountId}` +
|
|
124
|
-
`-${appId}
|
|
111
|
+
`-${appId}--${transactionId}-${sampled}-${priority}-${timestamp}`
|
|
125
112
|
|
|
126
113
|
if (this._traceStateRaw) {
|
|
127
114
|
return `${nrTraceState},${this._traceStateRaw}`
|
|
@@ -151,7 +138,7 @@ class TraceContext {
|
|
|
151
138
|
|
|
152
139
|
logger.trace('traceparent added with %s', traceParent)
|
|
153
140
|
|
|
154
|
-
const tracestate = this.createTracestate(
|
|
141
|
+
const tracestate = this.createTracestate()
|
|
155
142
|
if (tracestate) {
|
|
156
143
|
if (setter) {
|
|
157
144
|
setter.set(headers, TRACE_CONTEXT_STATE_HEADER, tracestate)
|
|
@@ -306,6 +306,22 @@ function _maybeBindPromise({ result, full, segment }) {
|
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Invokes a function within the scope of the given context. If the original
|
|
311
|
+
* function returns a promise, that promise will be bound to the correct
|
|
312
|
+
* segment.
|
|
313
|
+
*
|
|
314
|
+
* @param {object} params Function parameters.
|
|
315
|
+
* @param {Function} params.handler The original function to invoke.
|
|
316
|
+
* @param {AsyncContext} params.context The context for the invocation.
|
|
317
|
+
* @param {boolean} [params.full] When set to `true`, the provided segment
|
|
318
|
+
* will be started and ended automatically.
|
|
319
|
+
* @param {object} params.thisArg Entity to use as the `this` binding when
|
|
320
|
+
* invoking the original function.
|
|
321
|
+
* @param {*[]} params.args Array of arguments to provide to the function.
|
|
322
|
+
*
|
|
323
|
+
* @returns {*} Whatever the instrumented function returns.
|
|
324
|
+
*/
|
|
309
325
|
function runInContext({ handler, context, full, thisArg, args }) {
|
|
310
326
|
const { segment } = context
|
|
311
327
|
if (segment && full) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "newrelic",
|
|
3
|
-
"version": "14.1
|
|
3
|
+
"version": "14.2.1",
|
|
4
4
|
"author": "New Relic Node.js agent team <nodejs@newrelic.com>",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"contributors": [
|
|
@@ -170,7 +170,7 @@
|
|
|
170
170
|
"lint:fix": "eslint --fix .",
|
|
171
171
|
"public-docs": "jsdoc -c ./jsdoc-conf.jsonc",
|
|
172
172
|
"publish-docs": "./bin/publish-docs.sh",
|
|
173
|
-
"services": "
|
|
173
|
+
"services": "./bin/start-services.sh",
|
|
174
174
|
"services:start": "npm run services",
|
|
175
175
|
"services:stop": "docker compose down",
|
|
176
176
|
"smoke": "time borp --timeout 180000 --reporter ./test/lib/test-reporter.mjs 'test/smoke/**/*.test.js'",
|
|
@@ -223,9 +223,9 @@
|
|
|
223
223
|
"winston-transport": "^4.5.0"
|
|
224
224
|
},
|
|
225
225
|
"optionalDependencies": {
|
|
226
|
-
"@datadog/pprof": "^5.
|
|
227
|
-
"@newrelic/fn-inspect": "^5.
|
|
228
|
-
"@newrelic/native-metrics": "^13.
|
|
226
|
+
"@datadog/pprof": "^5.14.2",
|
|
227
|
+
"@newrelic/fn-inspect": "^5.1.0",
|
|
228
|
+
"@newrelic/native-metrics": "^13.1.0",
|
|
229
229
|
"@prisma/prisma-fmt-wasm": "^4.17.0-16.27eb2449f178cd9fe1a4b892d732cc4795f75085"
|
|
230
230
|
},
|
|
231
231
|
"devDependencies": {
|