newrelic 9.2.0 → 9.4.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 CHANGED
@@ -1,3 +1,40 @@
1
+ ### v9.4.0 (2022-10-24)
2
+
3
+ * Removed legacy agent async context propagation. The default behavior is now what was behind the `feature_flag.new_promise_tracking`. You can read more about the difference [here](https://docs.newrelic.com/docs/release-notes/agent-release-notes/nodejs-release-notes/node-agent-7-3-0#new-features).
4
+
5
+ * Fixed an issue with the ES Module loader that properly registers instrumentation when the file path included url encoded characters.
6
+
7
+ * Added an API for enqueuing application logs for forwarding
8
+
9
+ ```js
10
+ newrelic.recordLogEvent({ message: 'hello world', level: 'info' })`
11
+ ```
12
+
13
+
14
+ **Note**: If you are including a serialized error make sure it is on the `error` key of the log event:
15
+
16
+ ```js
17
+ const error = new Error('testing errors');
18
+ newrelic.recordLogEvent({ message: 'error example', level: 'error', error })
19
+ ```
20
+
21
+ * Fixed `cassandra-driver` instrumentation to properly set instance details on query segments/spans.
22
+
23
+ * Added a new context manager that leverages AsyncLocalStorage for async context propagation.
24
+ * This will be available via a feature flag `config.feature_flag.async_local_context`
25
+ * Alternatively you can set the environment variable of `NEW_RELIC_FEATURE_FLAG_ASYNC_LOCAL_CONTEXT=1`
26
+ * By enabling this feature flag it should make the agent use less memory and CPU.
27
+
28
+ ### v9.3.0 (2022-10-17)
29
+
30
+ * Added instrumentation to bunyan to support application logging use cases: forwarding, local decorating, and metrics.
31
+
32
+ Big thanks to @brianphillips for his contribution 🚀
33
+
34
+ * Added c8 to track code coverage.
35
+
36
+ * Added documentation about custom instrumentation in ES module applications
37
+
1
38
  ### v9.2.0 (2022-10-06)
2
39
 
3
40
  * Added ability to instrument ES Modules with the New Relic ESM Loader.
package/README.md CHANGED
@@ -84,6 +84,47 @@ $ node --experimental-loader newrelic/esm-loader.mjs your-program.js
84
84
 
85
85
  **Note**: Unlike the CommonJS methods listed above, there are no alternatives to running the agent without the `--experimental-loader` flag.
86
86
 
87
+ ### Custom Instrumentation
88
+
89
+ The agent supports adding your own custom instrumentation to ES module applications. In order to load custom instrumentation in an ES module app, you'll need to update your `newrelic.cjs` file to include the following:
90
+
91
+ ```js
92
+ /* File: newrelic.cjs */
93
+ 'use strict'
94
+ /**
95
+ * New Relic agent configuration.
96
+ *
97
+ * See lib/config/default.js in the agent distribution for a more complete
98
+ * description of configuration variables and their potential values.
99
+ */
100
+ exports.config = {
101
+ app_name: ['Your application or service name'],
102
+ license_key: 'your new relic license key',
103
+ api: {
104
+ esm: {
105
+ custom_instrumentation_entrypoint: '/path/to/my/instrumentation.js'
106
+ }
107
+ }
108
+ /* ... rest of configuration .. */
109
+ }
110
+ ```
111
+
112
+ If you do not use a configuration file, then use the environment variable `NEW_RELIC_API_ESM_CUSTOM_INSTRUMENTATION_ENTRYPOINT` instead.
113
+
114
+ By updating the configuration, the agent's ES module loader will ensure that your custom instrumentation is added at module load. This is required in ES module applications due to the immutability of module export bindings: we are unable to apply our instrumentation after loading is complete.
115
+
116
+ We support the following custom instrumentation API methods in ES module apps:
117
+
118
+ * `newrelic.instrument`
119
+ * `newrelic.instrumentConglomerate`
120
+ * `newrelic.instrumentDatastore`
121
+ * `newrelic.instrumentMessages`
122
+ * `newrelic.instrumentWebframework`
123
+
124
+ Note that we _do not_ support `newrelic.instrumentLoadedModule`, for the same issue of immutability mentioned above.
125
+
126
+ If you want to see an example of how to write custom instrumentation in an ES module app, check out our [examples](https://github.com/newrelic/newrelic-node-examples/tree/main/esm-app) repo for a working demo.
127
+
87
128
  ## Getting Started
88
129
 
89
130
  For more information on getting started, [check the Node.js docs](https://docs.newrelic.com/docs/agents/nodejs-agent/getting-started/introduction-new-relic-nodejs).
@@ -37,6 +37,7 @@ code, the source code can be found at [https://github.com/newrelic/node-newrelic
37
37
  * [@slack/bolt](#slackbolt)
38
38
  * [ajv](#ajv)
39
39
  * [async](#async)
40
+ * [c8](#c8)
40
41
  * [chai](#chai)
41
42
  * [commander](#commander)
42
43
  * [eslint-config-prettier](#eslint-config-prettier)
@@ -2091,6 +2092,28 @@ THE SOFTWARE.
2091
2092
 
2092
2093
  ```
2093
2094
 
2095
+ ### c8
2096
+
2097
+ This product includes source derived from [c8](https://github.com/bcoe/c8) ([v7.12.0](https://github.com/bcoe/c8/tree/v7.12.0)), distributed under the [ISC License](https://github.com/bcoe/c8/blob/v7.12.0/LICENSE.txt):
2098
+
2099
+ ```
2100
+ Copyright (c) 2017, Contributors
2101
+
2102
+ Permission to use, copy, modify, and/or distribute this software
2103
+ for any purpose with or without fee is hereby granted, provided
2104
+ that the above copyright notice and this permission notice
2105
+ appear in all copies.
2106
+
2107
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
2108
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
2109
+ OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE
2110
+ LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
2111
+ OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
2112
+ WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
2113
+ ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
2114
+
2115
+ ```
2116
+
2094
2117
  ### chai
2095
2118
 
2096
2119
  This product includes source derived from [chai](https://github.com/chaijs/chai) ([v4.3.6](https://github.com/chaijs/chai/tree/v4.3.6)), distributed under the [MIT License](https://github.com/chaijs/chai/blob/v4.3.6/LICENSE):
package/api.js CHANGED
@@ -18,6 +18,7 @@ const isValidType = require('./lib/util/attribute-types')
18
18
  const TransactionShim = require('./lib/shim/transaction-shim')
19
19
  const TransactionHandle = require('./lib/transaction/handle')
20
20
  const AwsLambda = require('./lib/serverless/aws-lambda')
21
+ const applicationLogging = require('./lib/util/application-logging')
21
22
 
22
23
  const ATTR_DEST = require('./lib/config/attribute-filter').DESTINATIONS
23
24
  const MODULE_TYPE = require('./lib/shim/constants').MODULE_TYPE
@@ -426,6 +427,86 @@ API.prototype.noticeError = function noticeError(error, customAttributes) {
426
427
  this.agent.errors.addUserError(transaction, error, filteredAttributes)
427
428
  }
428
429
 
430
+ /**
431
+ * Sends an application log message to New Relic. The agent already
432
+ * automatically does this for some instrumented logging libraries,
433
+ * but in case you are using another logging method that is not
434
+ * already instrumented by the agent, you can use this function
435
+ * instead.
436
+ *
437
+ * If application log forwarding is disabled in the agent
438
+ * configuration, this function does nothing.
439
+ *
440
+ * An example of using this function is
441
+ *
442
+ * newrelic.recordLogEvent({
443
+ * message: 'cannot find file',
444
+ * level: 'ERROR',
445
+ * error: new SystemError('missing.txt')
446
+ * })
447
+ *
448
+ * @param {object} logEvent The log event object to send. Any
449
+ * attributes besides `message`, `level`, `timestamp`, and `error` are
450
+ * recorded unchanged. The `logEvent` object itself will be mutated by
451
+ * this function.
452
+ * @param {string} logEvent.message The log message.
453
+ * @param {string} logEvent.level The log level severity. If this key is
454
+ * missing, it will default to UNKNOWN
455
+ * @param {number} logEvent.timestamp ECMAScript epoch number denoting the
456
+ * time that this log message was produced. If this key is missing,
457
+ * it will default to the output of `Date.now()`.
458
+ * @param {Error} logEvent.error Error associated to this log event. Ignored if missing.
459
+ */
460
+ API.prototype.recordLogEvent = function recordLogEvent(logEvent = {}) {
461
+ const metric = this.agent.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/recordLogEvent')
462
+ metric.incrementCallCount()
463
+
464
+ if (!applicationLogging.isLogForwardingEnabled(this.agent.config, this.agent)) {
465
+ logger.warnOnce(
466
+ 'Record logs',
467
+ 'Application log forwarding disabled, method API#recordLogEvent will not record messages'
468
+ )
469
+ return
470
+ }
471
+
472
+ // If they don't pass a logEvent object, or it doesn't have the
473
+ // required `message` key, bail out.
474
+ if (typeof logEvent !== 'object' || logEvent.message === undefined) {
475
+ logger.warn(
476
+ 'recordLogEvent requires an object with a `message` attribute for its single argument, got %s (%s)',
477
+ stringify(logEvent),
478
+ typeof logEvent
479
+ )
480
+ return
481
+ }
482
+ logEvent.message = applicationLogging.truncate(logEvent.message)
483
+
484
+ if (!logEvent.level) {
485
+ logger.debug('no log level set, setting it to UNKNOWN')
486
+ logEvent.level = 'UNKNOWN'
487
+ }
488
+
489
+ if (typeof logEvent.timestamp !== 'number') {
490
+ logger.debug('no timestamp set, setting it to `Date.now()`')
491
+ logEvent.timestamp = Date.now()
492
+ }
493
+
494
+ if (logEvent.error) {
495
+ logEvent['error.message'] = applicationLogging.truncate(logEvent.error.message)
496
+ logEvent['error.stack'] = applicationLogging.truncate(logEvent.error.stack)
497
+ logEvent['error.class'] =
498
+ logEvent.error.name === 'Error' ? logEvent.error.constructor.name : logEvent.error.name
499
+ delete logEvent.error
500
+ }
501
+
502
+ if (applicationLogging.isMetricsEnabled(this.agent.config)) {
503
+ applicationLogging.incrementLoggingLinesMetrics(logEvent.level, this.agent.metrics)
504
+ }
505
+
506
+ const metadata = this.agent.getLinkingMetadata()
507
+ this.agent.logs.add(Object.assign({}, logEvent, metadata))
508
+ }
509
+
429
510
  /**
430
511
  * If the URL for a transaction matches the provided pattern, name the
431
512
  * transaction with the provided name. If there are capture groups in the
package/esm-loader.mjs CHANGED
@@ -9,6 +9,7 @@ import loggingModule from './lib/logger.js'
9
9
  import NAMES from './lib/metrics/names.js'
10
10
  import semver from 'semver'
11
11
  import path from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
12
13
 
13
14
  const isSupportedVersion = () => semver.gte(process.version, 'v16.12.0')
14
15
  // This check will prevent resolve hooks executing from within this file
@@ -70,7 +71,7 @@ export async function resolve(specifier, context, nextResolve) {
70
71
  const instrumentationDefinitionCopy = Object.assign({}, instrumentationDefinition)
71
72
 
72
73
  // Stripping the prefix is necessary because the code downstream gets this url without it
73
- instrumentationDefinitionCopy.moduleName = url.replace('file://', '')
74
+ instrumentationDefinitionCopy.moduleName = fileURLToPath(url)
74
75
 
75
76
  // Added to keep our Supportability metrics from exploding/including customer info via full filepath
76
77
  instrumentationDefinitionCopy.specifier = specifier
@@ -178,6 +179,9 @@ function addESMSupportabilityMetrics(agent) {
178
179
  * This is done by injecting the ESM shim which proxies every property on the exported
179
180
  * module and registers the module with shimmer so instrumentation can be registered properly.
180
181
  *
182
+ * Note: this autogenerated code _requires_ that the import have the file:// prefix!
183
+ * Without it, Node.js throws an ERR_INVALID_URL error: you've been warned.
184
+ *
181
185
  * @param {string} url the URL returned by the resolve chain
182
186
  * @param {string} specifier string identifier in an import statement or import() expression
183
187
  * @returns {string} source code rewritten to wrap with our esm-shim
@@ -185,7 +189,7 @@ function addESMSupportabilityMetrics(agent) {
185
189
  async function wrapEsmSource(url, specifier) {
186
190
  const pkg = await import(url)
187
191
  const props = Object.keys(pkg)
188
- const trimmedUrl = url.replace('file://', '')
192
+ const trimmedUrl = fileURLToPath(url)
189
193
 
190
194
  return `
191
195
  import wrapModule from '${esmShimPath.href}'
@@ -0,0 +1,69 @@
1
+ /*
2
+ * Copyright 2021 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ const { AsyncLocalStorage } = require('async_hooks')
9
+
10
+ /**
11
+ * Class for managing state in the agent.
12
+ * Uses AsyncLocalStorage for context propagation of state across async boundaries.
13
+ *
14
+ * Given current usage with every instrumented function, the functions in this
15
+ * class should do as little work as possible to avoid unnecessary overhead.
16
+ *
17
+ * @class
18
+ */
19
+ class AsyncLocalContextManager {
20
+ /**
21
+ * @param {object} config New Relic config instance
22
+ */
23
+ constructor(config) {
24
+ this._config = config
25
+
26
+ this._asyncLocalStorage = new AsyncLocalStorage()
27
+ }
28
+
29
+ /**
30
+ * Get the currently active context.
31
+ *
32
+ * @returns {object} The current active context.
33
+ */
34
+ getContext() {
35
+ return this._asyncLocalStorage.getStore() || null
36
+ }
37
+
38
+ /**
39
+ * Set a new active context. Not bound to function execution.
40
+ * The AsyncLocalStorage method is considered experimental
41
+ *
42
+ * @param {object} newContext The context to set as active.
43
+ */
44
+ setContext(newContext) {
45
+ this._asyncLocalStorage.enterWith(newContext)
46
+ }
47
+
48
+ /**
49
+ * Run a function with the passed in context as the active context.
50
+ * Restores the previously active context upon completion.
51
+ *
52
+ * @param {object} context The context to set as active during callback execution.
53
+ * @param {Function} callback The function to execute in context.
54
+ * @param {Function} [cbThis] Optional `this` to apply to the callback.
55
+ * @param {Array<*>} [args] Optional arguments object or args array to invoke the callback with.
56
+ * @returns {*} Returns the value returned by the callback function.
57
+ */
58
+ runInContext(context, callback, cbThis, args) {
59
+ const toInvoke = cbThis ? callback.bind(cbThis) : callback
60
+
61
+ if (args) {
62
+ return this._asyncLocalStorage.run(context, toInvoke, ...args)
63
+ }
64
+
65
+ return this._asyncLocalStorage.run(context, toInvoke)
66
+ }
67
+ }
68
+
69
+ module.exports = AsyncLocalContextManager
@@ -5,6 +5,8 @@
5
5
 
6
6
  'use strict'
7
7
 
8
+ const semver = require('semver')
9
+
8
10
  const logger = require('../logger')
9
11
 
10
12
  /**
@@ -16,6 +18,26 @@ const logger = require('../logger')
16
18
  * the current configuration.
17
19
  */
18
20
  function createContextManager(config) {
21
+ if (config.feature_flag.async_local_context) {
22
+ // TODO: Remove >=16 check when only support 16+. AsyncLocal became stable in 16.4.0
23
+ if (semver.satisfies(process.version, '>=16.4.0')) {
24
+ return createAsyncLocalContextManager(config)
25
+ }
26
+
27
+ logger.warn('The AsyncLocalContextManager is only supported on Node version 16.4.0 and later.')
28
+ }
29
+
30
+ return createLegacyContextManager(config)
31
+ }
32
+
33
+ function createAsyncLocalContextManager(config) {
34
+ logger.info('Using AsyncLocalContextManager')
35
+
36
+ const AsyncLocalContextManager = require('./async-local-context-manager')
37
+ return new AsyncLocalContextManager(config)
38
+ }
39
+
40
+ function createLegacyContextManager(config) {
19
41
  if (config.logging.diagnostics) {
20
42
  logger.info('Using LegacyDiagnosticContextManager')
21
43
 
@@ -8,12 +8,12 @@
8
8
  // unreleased flags gating an active feature
9
9
  exports.prerelease = {
10
10
  express5: false,
11
- new_promise_tracking: false,
12
11
  promise_segments: false,
13
12
  reverse_naming_rules: false,
14
13
  undici_instrumentation: false,
15
14
  undici_async_tracking: true,
16
- unresolved_promise_cleanup: true
15
+ unresolved_promise_cleanup: true,
16
+ async_local_context: false
17
17
  }
18
18
 
19
19
  // flags that are no longer used for released features
@@ -24,6 +24,7 @@ exports.released = [
24
24
  'custom_metrics',
25
25
  'express_segments',
26
26
  'native_metrics',
27
+ 'new_promise_tracking',
27
28
  'protocol_17',
28
29
  'serverless_mode',
29
30
  'send_request_uri_attribute',
@@ -212,7 +212,7 @@ function wrapPromiseChannel(shim) {
212
212
 
213
213
  const proto = libPModel.Channel.prototype
214
214
  if (shim.isWrapped(proto.consume)) {
215
- shim.logger.trace('Promise model already isntrumented.')
215
+ shim.logger.trace('Promise model already instrumented.')
216
216
  return
217
217
  }
218
218
 
@@ -0,0 +1,101 @@
1
+ /*
2
+ * Copyright 2022 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ const {
9
+ isApplicationLoggingEnabled,
10
+ isLogForwardingEnabled,
11
+ isLocalDecoratingEnabled,
12
+ isMetricsEnabled,
13
+ createModuleUsageMetric,
14
+ incrementLoggingLinesMetrics,
15
+ truncate
16
+ } = require('../util/application-logging')
17
+
18
+ const logger = require('../logger').child({ component: 'bunyan' })
19
+
20
+ function augmentLogData(originalLog, agent, nameFromLevel) {
21
+ // shallow copy, since we're modifying things
22
+ const newLog = Object.assign({}, originalLog)
23
+ newLog.timestamp = Date.now()
24
+ newLog.level = nameFromLevel[originalLog.level]
25
+
26
+ // put log message into a consistent spot and ensure it's not too long
27
+ newLog.message = truncate(newLog.msg)
28
+
29
+ // tidy up the error output to help with max length restrictions
30
+ if (newLog.err) {
31
+ newLog['error.message'] = truncate(newLog.err.message)
32
+ newLog['error.stack'] = truncate(newLog.err.stack)
33
+ newLog['error.class'] =
34
+ newLog.err.name === 'Error' ? newLog.err.constructor.name : newLog.err.name
35
+ // clear out the old error message
36
+ delete newLog.err
37
+ }
38
+
39
+ // Add the metadata to the object being logged
40
+ const metadata = agent.getLinkingMetadata(true)
41
+ Object.keys(metadata).forEach((m) => {
42
+ newLog[m] = metadata[m]
43
+ })
44
+
45
+ return newLog
46
+ }
47
+
48
+ function createLoggerWrapper(shim, fn, fnName, bunyanLogger, nameFromLevel) {
49
+ const agent = shim.agent
50
+
51
+ createModuleUsageMetric('bunyan', agent.metrics)
52
+
53
+ // forward logs via the agent logs aggregator
54
+ bunyanLogger.addStream({
55
+ name: 'NRLogForwarder',
56
+ type: 'raw',
57
+ level: bunyanLogger.level(),
58
+ stream: {
59
+ write: function nrLogWrite(logLine) {
60
+ agent.logs.add(augmentLogData(logLine, agent, nameFromLevel))
61
+ }
62
+ }
63
+ })
64
+ // no return here means the original return value is preserved
65
+ }
66
+
67
+ module.exports = function instrument(agent, bunyan, _, shim) {
68
+ const config = agent.config
69
+
70
+ if (!isApplicationLoggingEnabled(config)) {
71
+ logger.debug('Application logging not enabled. Not instrumenting bunyan.')
72
+ return
73
+ }
74
+
75
+ const logForwardingEnabled = isLogForwardingEnabled(config, agent)
76
+ const localDecoratingEnabled = isLocalDecoratingEnabled(config)
77
+ const metricsEnabled = isMetricsEnabled(config)
78
+
79
+ if (logForwardingEnabled) {
80
+ shim.wrapReturn(bunyan, 'createLogger', createLoggerWrapper, [bunyan.nameFromLevel])
81
+ }
82
+
83
+ if (metricsEnabled || localDecoratingEnabled) {
84
+ shim.wrap(bunyan.prototype, '_emit', function wrapEmit(_shim, emit) {
85
+ return function wrappedEmit() {
86
+ const args = shim.argsToArray.apply(shim, arguments)
87
+ const rec = args[0]
88
+
89
+ if (metricsEnabled) {
90
+ incrementLoggingLinesMetrics(bunyan.nameFromLevel[rec.level], agent.metrics)
91
+ }
92
+
93
+ if (localDecoratingEnabled) {
94
+ rec.message = truncate(rec.msg) + agent.getNRLinkingMetadata()
95
+ }
96
+ args[0] = rec
97
+ return emit.apply(this, args)
98
+ }
99
+ })
100
+ }
101
+ }
@@ -5,16 +5,91 @@
5
5
 
6
6
  'use strict'
7
7
 
8
- module.exports = function initialize(agent, cassandra, moduleName, shim) {
9
- const proto = cassandra.Client.prototype
8
+ const semver = require('semver')
9
+
10
+ /**
11
+ * Instruments the `cassandra-driver` module, function that is
12
+ * passed to `onRequire` when instantiating instrumentation.
13
+ *
14
+ * @param {object} _agent - NewRelic agent
15
+ * @param {object} cassandra - The cassandra-driver library definition
16
+ * @param {string} _moduleName - String representation of require/import path
17
+ * @param {object} shim - shim for instrumentation
18
+ */
19
+ module.exports = function initialize(_agent, cassandra, _moduleName, shim) {
20
+ const cassandraVersion = shim.require('./package.json').version
21
+
10
22
  shim.setDatastore(shim.CASSANDRA)
11
- shim.recordOperation(proto, ['connect', 'shutdown'], { callback: shim.LAST })
12
- if (proto._innerExecute) {
13
- shim.recordQuery(proto, '_innerExecute', { query: shim.FIRST, callback: shim.LAST })
23
+
24
+ const ClientProto = cassandra.Client.prototype
25
+ const RequestExecutionProto = shim.require('./lib/request-execution.js').prototype
26
+
27
+ shim.recordOperation(ClientProto, ['connect', 'shutdown'], { callback: shim.LAST })
28
+
29
+ if (semver.satisfies(cassandraVersion, '>=4.4.0')) {
30
+ shim.recordQuery(ClientProto, '_execute', {
31
+ query: shim.FIRST,
32
+ callback: shim.LAST
33
+ })
34
+
35
+ shim.wrap(
36
+ RequestExecutionProto,
37
+ '_sendOnConnection',
38
+ function wrapSendOnConnection(shim, _sendOnConnection) {
39
+ return function wrappedSendOnConnection() {
40
+ shim.captureInstanceAttributes(
41
+ this._connection.address,
42
+ this._connection.port,
43
+ this._connection.keyspace
44
+ )
45
+
46
+ return _sendOnConnection.apply(this, arguments)
47
+ }
48
+ }
49
+ )
14
50
  } else {
15
- shim.recordQuery(proto, '_execute', { query: shim.FIRST, callback: shim.LAST })
51
+ shim.recordQuery(ClientProto, '_innerExecute', {
52
+ query: shim.FIRST,
53
+ callback: shim.LAST
54
+ })
55
+
56
+ shim.wrap(RequestExecutionProto, 'start', function wrapStart(shim, start) {
57
+ return function wrappedStart() {
58
+ const parent = shim.getSegment()
59
+ const self = this
60
+
61
+ const args = shim.argsToArray.apply(shim, arguments)
62
+
63
+ /**
64
+ * In older versions of cassandra-driver, we can't rely on RequestExecution._sendOnConnection,
65
+ * so instead we use the callback passed to RequestExecution.start as a sort of hook point,
66
+ * because we know for sure that the connection was set and in scope right before the callback
67
+ * is executed.
68
+ *
69
+ * We also have to set the active segment to the RequestExecution.start's segment to ensure that
70
+ * we're adding the connection attributes to the correct segment.
71
+ *
72
+ * See: https://github.com/datastax/nodejs-driver/blob/v3.4.0/lib/request-execution.js#L51
73
+ */
74
+ args[0] = shim.wrap(args[0], function wrapGetHostCallback(shim, getHostCallback) {
75
+ return function wrappedGetHostCallback() {
76
+ shim.setActiveSegment(parent)
77
+
78
+ shim.captureInstanceAttributes(
79
+ self._connection.address,
80
+ self._connection.port,
81
+ self._connection.keyspace
82
+ )
83
+ return getHostCallback.apply(this, arguments)
84
+ }
85
+ })
86
+
87
+ return start.apply(this, args)
88
+ }
89
+ })
16
90
  }
17
- shim.recordBatchQuery(proto, 'batch', {
91
+
92
+ shim.recordBatchQuery(ClientProto, 'batch', {
18
93
  query: findBatchQueryArg,
19
94
  callback: shim.LAST
20
95
  })
@@ -24,9 +99,13 @@ module.exports = function initialize(agent, cassandra, moduleName, shim) {
24
99
  * Given the arguments for Cassandra's `batch` method, this finds the first
25
100
  * query in the batch.
26
101
  *
27
- * @return {string} The query for this batch request.
102
+ * @param {object} _shim - shim for instrumentation
103
+ * @param {Function} _batch - original batch function
104
+ * @param {string} _fnName - the function name (batch)
105
+ * @param {Array} args - original arguments passed to the batch function
106
+ * @returns {string} The query for this batch request.
28
107
  */
29
- function findBatchQueryArg(shim, batch, fnName, args) {
108
+ function findBatchQueryArg(_shim, _batch, _fnName, args) {
30
109
  const sql = (args[0] && args[0][0]) || ''
31
110
  return sql.query || sql
32
111
  }
@@ -11,17 +11,20 @@ const asyncHooks = require('async_hooks')
11
11
  module.exports = initialize
12
12
 
13
13
  function initialize(agent, shim) {
14
+ if (agent.config.feature_flag.async_local_context) {
15
+ logger.debug(
16
+ 'New AsyncLocalStorage context enabled. Not enabling manual async_hooks or promise instrumentation'
17
+ )
18
+
19
+ return
20
+ }
21
+
14
22
  // this map is reused to track the segment that was active when
15
23
  // the before callback is called to be replaced in the after callback
16
24
  const segmentMap = new Map()
17
25
  module.exports.segmentMap = segmentMap
18
26
 
19
- let hookHandlers = getStandardHooks(segmentMap, agent, shim)
20
-
21
- if (agent.config.feature_flag.new_promise_tracking) {
22
- logger.info('Enabling new promise tracking style via new_promise_tracking feature flag.')
23
- hookHandlers = getPromiseResolveStyleHooks(segmentMap, agent, shim)
24
- }
27
+ const hookHandlers = getPromiseResolveStyleHooks(segmentMap, agent, shim)
25
28
 
26
29
  const hook = asyncHooks.createHook(hookHandlers)
27
30
  hook.enable()
@@ -33,63 +36,6 @@ function initialize(agent, shim) {
33
36
  return true
34
37
  }
35
38
 
36
- function getStandardHooks(segmentMap, agent, shim) {
37
- const hooks = {
38
- init: function initHook(id, type, triggerId, promiseWrap) {
39
- if (type !== 'PROMISE') {
40
- return
41
- }
42
-
43
- const parentSegment = segmentMap.get(triggerId)
44
-
45
- if (parentSegment && !parentSegment.transaction.isActive()) {
46
- // Stop propagating if the transaction was ended.
47
- return
48
- }
49
-
50
- if (!parentSegment && !agent.getTransaction()) {
51
- return
52
- }
53
-
54
- const activeSegment = shim.getActiveSegment() || parentSegment
55
- if (promiseWrap && promiseWrap.promise) {
56
- promiseWrap.promise.__NR_id = id
57
- }
58
- segmentMap.set(id, activeSegment)
59
- },
60
-
61
- before: function beforeHook(id) {
62
- const hookSegment = segmentMap.get(id)
63
-
64
- if (!hookSegment) {
65
- return
66
- }
67
-
68
- segmentMap.set(id, shim.getActiveSegment())
69
- shim.setActiveSegment(hookSegment)
70
- },
71
- after: function afterHook(id) {
72
- const hookSegment = segmentMap.get(id)
73
-
74
- // hookSegment is the segment that was active before the promise
75
- // executed. If the promise is executing before a segment has been
76
- // restored, hookSegment will be null and should be restored. Thus
77
- // undefined is the only invalid value here.
78
- if (hookSegment === undefined) {
79
- return
80
- }
81
-
82
- segmentMap.set(id, shim.getActiveSegment())
83
- shim.setActiveSegment(hookSegment)
84
- },
85
- destroy: function destHook(id) {
86
- segmentMap.delete(id)
87
- }
88
- }
89
-
90
- return hooks
91
- }
92
-
93
39
  function getPromiseResolveStyleHooks(segmentMap, agent, shim) {
94
40
  const hooks = {
95
41
  init: function initHook(id, type, triggerId, asyncResource) {
@@ -8,27 +8,9 @@
8
8
  module.exports = initialize
9
9
 
10
10
  function initialize(agent, timers, moduleName, shim) {
11
- const processMethods = ['nextTick', '_nextDomainTick', '_tickDomainCallback']
12
-
13
- shim.wrap(process, processMethods, function wrapProcess(shim, fn) {
14
- return function wrappedProcess() {
15
- const segment = shim.getActiveSegment()
16
- if (!segment) {
17
- return fn.apply(this, arguments)
18
- }
19
-
20
- // Manual copy because helper methods add significant overhead in some usages
21
- const len = arguments.length
22
- const args = new Array(len)
23
- for (let i = 0; i < len; ++i) {
24
- args[i] = arguments[i]
25
- }
26
-
27
- shim.bindSegment(args, shim.FIRST, segment)
28
-
29
- return fn.apply(this, args)
30
- }
31
- })
11
+ if (!agent.config.feature_flag.async_local_context) {
12
+ instrumentProcessMethods(shim, process)
13
+ }
32
14
 
33
15
  instrumentTimerMethods(timers)
34
16
 
@@ -43,9 +25,11 @@ function initialize(agent, timers, moduleName, shim) {
43
25
 
44
26
  shim.record(nodule, asynchronizers, recordAsynchronizers)
45
27
 
46
- // We don't want to create segments for setImmediate calls, as the
47
- // object allocation may incur too much overhead in some situations
48
- shim.wrap(nodule, 'setImmediate', wrapSetImmediate)
28
+ if (!agent.config.feature_flag.async_local_context) {
29
+ // We don't want to create segments for setImmediate calls, as the
30
+ // object allocation may incur too much overhead in some situations
31
+ shim.wrap(nodule, 'setImmediate', wrapSetImmediate)
32
+ }
49
33
 
50
34
  shim.wrap(nodule, 'clearTimeout', wrapClearTimeout)
51
35
 
@@ -84,6 +68,30 @@ function initialize(agent, timers, moduleName, shim) {
84
68
  }
85
69
  }
86
70
 
71
+ function instrumentProcessMethods(shim, process) {
72
+ const processMethods = ['nextTick', '_nextDomainTick', '_tickDomainCallback']
73
+
74
+ shim.wrap(process, processMethods, function wrapProcess(shim, fn) {
75
+ return function wrappedProcess() {
76
+ const segment = shim.getActiveSegment()
77
+ if (!segment) {
78
+ return fn.apply(this, arguments)
79
+ }
80
+
81
+ // Manual copy because helper methods add significant overhead in some usages
82
+ const len = arguments.length
83
+ const args = new Array(len)
84
+ for (let i = 0; i < len; ++i) {
85
+ args[i] = arguments[i]
86
+ }
87
+
88
+ shim.bindSegment(args, shim.FIRST, segment)
89
+
90
+ return fn.apply(this, args)
91
+ }
92
+ })
93
+ }
94
+
87
95
  function makeWrappedPromisifyCompatible(shim, timers) {
88
96
  const originalSetTimeout = shim.getOriginal(timers.setTimeout)
89
97
  Object.getOwnPropertySymbols(originalSetTimeout).forEach((symbol) => {
@@ -95,8 +103,10 @@ function makeWrappedPromisifyCompatible(shim, timers) {
95
103
  timers.setInterval[symbol] = originalSetInterval[symbol]
96
104
  })
97
105
 
98
- const originalSetImmediate = shim.getOriginal(timers.setImmediate)
99
- Object.getOwnPropertySymbols(originalSetImmediate).forEach((symbol) => {
100
- timers.setImmediate[symbol] = originalSetImmediate[symbol]
101
- })
106
+ if (!shim.agent.config.feature_flag.async_local_context) {
107
+ const originalSetImmediate = shim.getOriginal(timers.setImmediate)
108
+ Object.getOwnPropertySymbols(originalSetImmediate).forEach((symbol) => {
109
+ timers.setImmediate[symbol] = originalSetImmediate[symbol]
110
+ })
111
+ }
102
112
  }
@@ -15,6 +15,7 @@ module.exports = function instrumentations() {
15
15
  'cassandra-driver': { type: MODULE_TYPE.DATASTORE },
16
16
  'connect': { type: MODULE_TYPE.WEB_FRAMEWORK },
17
17
  'bluebird': { type: MODULE_TYPE.PROMISE },
18
+ 'bunyan': { type: MODULE_TYPE.GENERIC },
18
19
  'director': { type: MODULE_TYPE.WEB_FRAMEWORK },
19
20
  'express': { type: MODULE_TYPE.WEB_FRAMEWORK },
20
21
  'fastify': { type: MODULE_TYPE.WEB_FRAMEWORK },
@@ -277,11 +277,13 @@ const LOGGING = {
277
277
  INFO: `${LOGGING_LINES_PREFIX}/INFO`,
278
278
  WARN: `${LOGGING_LINES_PREFIX}/WARN`,
279
279
  ERROR: `${LOGGING_LINES_PREFIX}/ERROR`,
280
+ FATAL: `${LOGGING_LINES_PREFIX}/FATAL`,
280
281
  DEBUG: `${LOGGING_LINES_PREFIX}/DEBUG`,
281
282
  TRACE: `${LOGGING_LINES_PREFIX}/TRACE`,
282
283
  UNKNOWN: `${LOGGING_LINES_PREFIX}/UNKNOWN`
283
284
  },
284
285
  LIBS: {
286
+ BUNYAN: `${SUPPORTABILITY.LOGGING}/${NODEJS.PREFIX}bunyan/enabled`,
285
287
  PINO: `${SUPPORTABILITY.LOGGING}/${NODEJS.PREFIX}pino/enabled`,
286
288
  WINSTON: `${SUPPORTABILITY.LOGGING}/${NODEJS.PREFIX}winston/enabled`
287
289
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "9.2.0",
3
+ "version": "9.4.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [
@@ -146,7 +146,8 @@
146
146
  "bench": "node ./bin/run-bench.js",
147
147
  "docker-env": "./bin/docker-env-vars.sh",
148
148
  "docs": "npm ci && jsdoc -c ./jsdoc-conf.json --private -r .",
149
- "integration": "npm run prepare-test && npm run sub-install && time tap test/integration/**/**/*.tap.js --timeout=180 --no-coverage --reporter classic",
149
+ "integration": "npm run prepare-test && npm run sub-install && time tap --test-regex='(\\/|^test\\/integration\\/.*\\.tap\\.js)$' --timeout=180 --no-coverage --reporter classic",
150
+ "integration:async-local": "npm run prepare-test && npm run sub-install && time tap --test-regex='(\\/|^test\\/integration\\/.*\\.tap\\.js)$' --timeout=180 --no-coverage --reporter classic --test-env=NEW_RELIC_FEATURE_FLAG_ASYNC_LOCAL_CONTEXT=1",
150
151
  "prepare-test": "npm run ssl && npm run docker-env",
151
152
  "lint": "eslint ./*.{js,mjs} lib test bin examples",
152
153
  "lint:fix": "eslint --fix, ./*.{js,mjs} lib test bin examples",
@@ -158,16 +159,19 @@
158
159
  "sub-install": "node test/bin/install_sub_deps",
159
160
  "test": "npm run integration && npm run unit && npm run unit:esm",
160
161
  "third-party-updates": "oss third-party manifest --includeOptDeps && oss third-party notices --includeOptDeps && git add THIRD_PARTY_NOTICES.md third_party_manifest.json",
161
- "unit": "rm -f newrelic_agent.log && time tap --test-regex='(\\/|^test\\/unit\\/.*\\.test\\.js)$' --timeout=180 --no-coverage --reporter classic",
162
- "unit:esm": "rm -f newrelic_agent.log && time tap --test-regex='(\\/|^test\\/unit\\/.*\\.test\\.mjs)$' --timeout=180 --no-coverage --reporter classic --node-arg=--experimental-loader=testdouble",
162
+ "unit": "rm -f newrelic_agent.log && time c8 -o ./coverage/unit tap --test-regex='(\\/|^test\\/unit\\/.*\\.test\\.js)$' --timeout=180 --no-coverage --reporter classic",
163
+ "unit:async-local": "rm -f newrelic_agent.log && time tap --test-regex='(\\/|^test\\/unit\\/.*\\.test\\.js)$' --timeout=180 --no-coverage --reporter classic --test-env=NEW_RELIC_FEATURE_FLAG_ASYNC_LOCAL_CONTEXT=1",
164
+ "unit:esm": "rm -f newrelic_agent.log && time c8 -o ./coverage/esm-unit --include *.mjs --include **/*.mjs --exclude false tap --test-regex='(\\/|^test\\/unit\\/.*\\.test\\.mjs)$' --timeout=180 --no-coverage --reporter classic --node-arg=--experimental-loader=testdouble",
163
165
  "update-cross-agent-tests": "./bin/update-cats.sh",
164
166
  "versioned-tests": "./bin/run-versioned-tests.sh",
165
167
  "update-changelog-version": "node ./bin/update-changelog-version",
166
168
  "checkout-external-versioned": "node ./test/versioned-external/checkout-external-tests.js",
167
169
  "versioned": "npm run versioned:npm7",
168
- "versioned:major": "npm run checkout-external-versioned && npm run prepare-test && VERSIONED_MODE=--major NPM7=1 time ./bin/run-versioned-tests.sh",
170
+ "versioned:major": "VERSIONED_MODE=--major npm run versioned:npm7",
169
171
  "versioned:npm6": "npm run checkout-external-versioned && npm run prepare-test && time ./bin/run-versioned-tests.sh",
170
172
  "versioned:npm7": "npm run checkout-external-versioned && npm run prepare-test && NPM7=1 time ./bin/run-versioned-tests.sh",
173
+ "versioned:async-local": "NEW_RELIC_FEATURE_FLAG_ASYNC_LOCAL_CONTEXT=1 npm run versioned:npm7",
174
+ "versioned:async-local:major": "NEW_RELIC_FEATURE_FLAG_ASYNC_LOCAL_CONTEXT=1 npm run versioned:major",
171
175
  "prepare": "husky install"
172
176
  },
173
177
  "bin": {
@@ -199,6 +203,7 @@
199
203
  "@slack/bolt": "^3.7.0",
200
204
  "ajv": "^6.12.6",
201
205
  "async": "^3.2.4",
206
+ "c8": "^7.12.0",
202
207
  "chai": "^4.1.2",
203
208
  "commander": "^7.0.0",
204
209
  "eslint": "^8.24.0",