newrelic 8.4.0 → 8.5.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,37 +1,53 @@
1
- ### v8.4.0 (2021-09-28)
2
-
1
+ ### v8.5.0 (2021-10-12)
2
+
3
+ * Added full support for Fastify v2 and v3. Fastify instrumentation is now GA.
4
+ * Removed fastify feature flag.
5
+ * Instrumented Fastify routes by wrapping `addHook`.
6
+ * Added middleware mounting for fastify v3.
7
+ * Fixed capturing of mount point for middleware naming.
8
+ * Fixed the WebFramework spec definitions for Fastify middleware and route handlers to properly retrieve the IncomingMessage from a request object.
9
+ * Added proper definition to middleware handlers so that the relationship to consecutive middleware and route handler are siblings and not direct children.
10
+
11
+ * Added experimental instrumentation for the [undici](https://github.com/nodejs/undici) http client behind a feature flag.
12
+
13
+ To enable undici support, add the following into your config: `{ feature_flag: { undici_instrumentation: true } }`. The support for undici client is Node.js 16.x as it takes advantage of the [diagnostics_channel](https://nodejs.org/dist/latest-v16.x/docs/api/diagnostics_channel.html). Lastly, you must be using [v4.7.0+](https://github.com/nodejs/undici/releases/tag/v4.7.0) of the undici client for any of the instrumentation to work.
14
+
15
+ Note: There are currently some state issues if requests to an app are made with keep alive and you have multiple undici requests being made in parallel. In this case, set feature_flag: `{ undici_async_tracking: false }` which avoids these state issues at the cost of some broken segment nesting.
16
+
17
+ ### v8.4.0 (2021-09-28)
18
+
3
19
  * **Deprecation Warning**: Cross Application Tracing (CAT) has been deprecated and will be removed in a future major release. For applications that explicitly disable Distributed Tracing (DT) to leverage CAT, we recommend migrating to DT to avoid loss of cross-service visibility.
4
20
  * Disables CAT by default. You must explicitly enable CAT along with turning off DT.
5
- * Adds a deprecation warning when CAT is enabled and active (DT disabled).
6
-
21
+ * Adds a deprecation warning when CAT is enabled and active (DT disabled).
22
+
7
23
  * Fixed issue with `clearTimeout` that could result in dropping parent segments or spans.
8
24
 
9
- This bug resulted in some MongoDB calls being dropped from Transaction Traces and Distributed Traces (spans): https://github.com/newrelic/node-newrelic/issues/922.
10
-
11
- * Removed warnings from agent tests for `no-var` eslint rule.
12
-
13
- * Added support for Cassandra driver v4.0.0 and above.
14
-
15
- * Fixed issue where DT headers would not be processed by `transaction-shim.handleCATHeaders()` when CAT was explicitly disabled. This primarily impacts `amqplib` instrumentation.
16
-
17
- * Transitioned aws-lambda.test.js to use Tap over Mocha.
18
-
19
- * Removed warnings from agent for `no-var` eslint rule.
20
-
21
- * Refactored `transaction-shim`, `http` and `http-outbound` to use centralized CAT methods in `util/cat`
22
-
23
- * Replaced http-outbound test call to use example.com to avoid unpredictable connection resets.
24
-
25
- * Migrated sql query parser tests to tap
26
-
25
+ This bug resulted in some MongoDB calls being dropped from Transaction Traces and Distributed Traces (spans): https://github.com/newrelic/node-newrelic/issues/922.
26
+
27
+ * Removed warnings from agent tests for `no-var` eslint rule.
28
+
29
+ * Added support for Cassandra driver v4.0.0 and above.
30
+
31
+ * Fixed issue where DT headers would not be processed by `transaction-shim.handleCATHeaders()` when CAT was explicitly disabled. This primarily impacts `amqplib` instrumentation.
32
+
33
+ * Transitioned aws-lambda.test.js to use Tap over Mocha.
34
+
35
+ * Removed warnings from agent for `no-var` eslint rule.
36
+
37
+ * Refactored `transaction-shim`, `http` and `http-outbound` to use centralized CAT methods in `util/cat`
38
+
39
+ * Replaced http-outbound test call to use example.com to avoid unpredictable connection resets.
40
+
41
+ * Migrated sql query parser tests to tap
42
+
27
43
  * Added more API usage examples.
28
-
29
- * Added a README to the `examples/` folder discussing how to use the examples.
30
-
31
- * Fixed `message-shim` test assertion to avoid flakiness based on precision differences(ms vs ns)
32
-
33
- * Applied new lint rules barring the use of `var` and preferring the use of `const` wherever possible.
34
-
44
+
45
+ * Added a README to the `examples/` folder discussing how to use the examples.
46
+
47
+ * Fixed `message-shim` test assertion to avoid flakiness based on precision differences(ms vs ns)
48
+
49
+ * Applied new lint rules barring the use of `var` and preferring the use of `const` wherever possible.
50
+
35
51
  ### v8.3.0 (2021-09-09)
36
52
 
37
53
  * Enabled Distributed Tracing (DT) by default.
@@ -41,7 +41,9 @@ code, the source code can be found at [https://github.com/newrelic/node-newrelic
41
41
  * [chai](#chai)
42
42
  * [commander](#commander)
43
43
  * [eslint-config-prettier](#eslint-config-prettier)
44
+ * [eslint-plugin-disable](#eslint-plugin-disable)
44
45
  * [eslint-plugin-header](#eslint-plugin-header)
46
+ * [eslint-plugin-jsdoc](#eslint-plugin-jsdoc)
45
47
  * [eslint-plugin-node](#eslint-plugin-node)
46
48
  * [eslint-plugin-prettier](#eslint-plugin-prettier)
47
49
  * [eslint](#eslint)
@@ -49,7 +51,6 @@ code, the source code can be found at [https://github.com/newrelic/node-newrelic
49
51
  * [generic-pool](#generic-pool)
50
52
  * [glob](#glob)
51
53
  * [got](#got)
52
- * [http-errors](#http-errors)
53
54
  * [husky](#husky)
54
55
  * [jsdoc](#jsdoc)
55
56
  * [lint-staged](#lint-staged)
@@ -2210,6 +2211,33 @@ THE SOFTWARE.
2210
2211
 
2211
2212
  ```
2212
2213
 
2214
+ ### eslint-plugin-disable
2215
+
2216
+ This product includes source derived from [eslint-plugin-disable](https://github.com/mradionov/eslint-plugin-disable) ([v2.0.1](https://github.com/mradionov/eslint-plugin-disable/tree/v2.0.1)), distributed under the [MIT License](https://github.com/mradionov/eslint-plugin-disable/blob/v2.0.1/LICENSE):
2217
+
2218
+ ```
2219
+ Copyright (c) 2015 Michael Radionov (https://github.com/mradionov)
2220
+
2221
+ Permission is hereby granted, free of charge, to any person obtaining a copy
2222
+ of this software and associated documentation files (the "Software"), to deal
2223
+ in the Software without restriction, including without limitation the rights
2224
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
2225
+ copies of the Software, and to permit persons to whom the Software is
2226
+ furnished to do so, subject to the following conditions:
2227
+
2228
+ The above copyright notice and this permission notice shall be included in
2229
+ all copies or substantial portions of the Software.
2230
+
2231
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2232
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2233
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2234
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2235
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2236
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2237
+ THE SOFTWARE.
2238
+
2239
+ ```
2240
+
2213
2241
  ### eslint-plugin-header
2214
2242
 
2215
2243
  This product includes source derived from [eslint-plugin-header](https://github.com/Stuk/eslint-plugin-header) ([v3.1.1](https://github.com/Stuk/eslint-plugin-header/tree/v3.1.1)), distributed under the [MIT License](https://github.com/Stuk/eslint-plugin-header/blob/v3.1.1/README.md):
@@ -2226,6 +2254,38 @@ The above copyright notice and this permission notice shall be included in all c
2226
2254
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2227
2255
  ```
2228
2256
 
2257
+ ### eslint-plugin-jsdoc
2258
+
2259
+ This product includes source derived from [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) ([v36.1.0](https://github.com/gajus/eslint-plugin-jsdoc/tree/v36.1.0)), distributed under the [BSD-3-Clause License](https://github.com/gajus/eslint-plugin-jsdoc/blob/v36.1.0/LICENSE):
2260
+
2261
+ ```
2262
+ Copyright (c) 2018, Gajus Kuizinas (http://gajus.com/)
2263
+ All rights reserved.
2264
+
2265
+ Redistribution and use in source and binary forms, with or without
2266
+ modification, are permitted provided that the following conditions are met:
2267
+ * Redistributions of source code must retain the above copyright
2268
+ notice, this list of conditions and the following disclaimer.
2269
+ * Redistributions in binary form must reproduce the above copyright
2270
+ notice, this list of conditions and the following disclaimer in the
2271
+ documentation and/or other materials provided with the distribution.
2272
+ * Neither the name of the Gajus Kuizinas (http://gajus.com/) nor the
2273
+ names of its contributors may be used to endorse or promote products
2274
+ derived from this software without specific prior written permission.
2275
+
2276
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
2277
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
2278
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
2279
+ DISCLAIMED. IN NO EVENT SHALL ANUARY BE LIABLE FOR ANY
2280
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
2281
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
2282
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
2283
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
2284
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
2285
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2286
+
2287
+ ```
2288
+
2229
2289
  ### eslint-plugin-node
2230
2290
 
2231
2291
  This product includes source derived from [eslint-plugin-node](https://github.com/mysticatea/eslint-plugin-node) ([v11.1.0](https://github.com/mysticatea/eslint-plugin-node/tree/v11.1.0)), distributed under the [MIT License](https://github.com/mysticatea/eslint-plugin-node/blob/v11.1.0/LICENSE):
@@ -2409,37 +2469,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
2409
2469
 
2410
2470
  ```
2411
2471
 
2412
- ### http-errors
2413
-
2414
- This product includes source derived from [http-errors](https://github.com/jshttp/http-errors) ([v1.8.0](https://github.com/jshttp/http-errors/tree/v1.8.0)), distributed under the [MIT License](https://github.com/jshttp/http-errors/blob/v1.8.0/LICENSE):
2415
-
2416
- ```
2417
-
2418
- The MIT License (MIT)
2419
-
2420
- Copyright (c) 2014 Jonathan Ong me@jongleberry.com
2421
- Copyright (c) 2016 Douglas Christopher Wilson doug@somethingdoug.com
2422
-
2423
- Permission is hereby granted, free of charge, to any person obtaining a copy
2424
- of this software and associated documentation files (the "Software"), to deal
2425
- in the Software without restriction, including without limitation the rights
2426
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
2427
- copies of the Software, and to permit persons to whom the Software is
2428
- furnished to do so, subject to the following conditions:
2429
-
2430
- The above copyright notice and this permission notice shall be included in
2431
- all copies or substantial portions of the Software.
2432
-
2433
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2434
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2435
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2436
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2437
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2438
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2439
- THE SOFTWARE.
2440
-
2441
- ```
2442
-
2443
2472
  ### husky
2444
2473
 
2445
2474
  This product includes source derived from [husky](https://github.com/typicode/husky) ([v6.0.0](https://github.com/typicode/husky/tree/v6.0.0)), distributed under the [MIT License](https://github.com/typicode/husky/blob/v6.0.0/LICENSE):
@@ -7,13 +7,14 @@
7
7
 
8
8
  // unreleased flags gating an active feature
9
9
  exports.prerelease = {
10
- express5: false,
11
10
  await_support: true,
12
- promise_segments: false,
13
- reverse_naming_rules: false,
14
- fastify_instrumentation: false,
15
11
  certificate_bundle: false,
12
+ express5: false,
16
13
  new_promise_tracking: false,
14
+ promise_segments: false,
15
+ reverse_naming_rules: false,
16
+ undici_instrumentation: false,
17
+ undici_async_tracking: true,
17
18
  unresolved_promise_cleanup: true
18
19
  }
19
20
 
@@ -29,7 +30,8 @@ exports.released = [
29
30
  'serverless_mode',
30
31
  'send_request_uri_attribute',
31
32
  'synthetics',
32
- 'dt_format_w3c'
33
+ 'dt_format_w3c',
34
+ 'fastify_instrumentation'
33
35
  ]
34
36
 
35
37
  // flags that are no longer used for unreleased features
@@ -96,7 +96,7 @@ module.exports = function instrumentOutbound(agent, opts, makeRequest) {
96
96
  } else if (agent.config.cross_application_tracer.enabled) {
97
97
  cat.addCatHeaders(agent.config, transaction, outboundHeaders)
98
98
  } else {
99
- logger.trace('CAT disabled, not adding headers!')
99
+ logger.trace('Both DT and CAT are disabled, not adding headers!')
100
100
  }
101
101
 
102
102
  if (Array.isArray(opts.headers)) {
@@ -1,19 +1,43 @@
1
1
  /*
2
- * Copyright 2020 New Relic Corporation. All rights reserved.
2
+ * Copyright 2021 New Relic Corporation. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
 
6
6
  'use strict'
7
7
 
8
- const getRawRequestFromFastifyRequest = (shim, fn, fnName, args) => {
9
- const request = args[0]
8
+ /**
9
+ * Retrieves the IncomingMessage from a Fastify request. Depending on the
10
+ * context of this function it either exists on `request.raw` or just `request`
11
+ *
12
+ * @param {WebFrameworkShim} shim
13
+ * @param {Function} fn middleware function executing
14
+ * @param {string} fnName name of middleware executing
15
+ * @param {args} args that are passed to middleware
16
+ * @returns {IncomingMessage}
17
+ */
18
+ const getRequestFromFastify = (shim, fn, fnName, args) => {
19
+ const [request] = args
20
+
21
+ // request is Fastify request
22
+ // object, get IncomingMessage from .raw
10
23
  if (request && request.raw) {
11
24
  return request.raw
12
25
  }
26
+
27
+ return request
13
28
  }
14
29
 
30
+ /**
31
+ * Retrieves the params from the Fastify request.
32
+ *
33
+ * @param {WebFrameworkShim} shim
34
+ * @param {Function} fn middleware function executing
35
+ * @param {string} fnName name of middleware executing
36
+ * @param {args} args that are passed to middleware
37
+ * @returns {object} URL params on a Fastify request
38
+ */
15
39
  const getParamsFromFastifyRequest = (shim, fn, fnName, args) => {
16
- const req = args[0]
40
+ const [req] = args
17
41
  return req && req.params
18
42
  }
19
43
 
@@ -21,32 +45,17 @@ const getParamsFromFastifyRequest = (shim, fn, fnName, args) => {
21
45
  * Builds the recordMiddleware Spec for the route handler
22
46
  *
23
47
  * A spec is basically a specification -- or a list
24
- * of insrtuctions to the recordMiddleware function
48
+ * of instructions to the recordMiddleware function
25
49
  * that provide it with the information it needs to
26
50
  * do its job. You could also think of it as a
27
51
  * mini-DSL
52
+ *
53
+ * @param {WebFrameworkShim} shim
54
+ * @param {string} path URL route being executed
55
+ * @returns {object} spec for Fastify route handler
28
56
  */
29
57
  function buildMiddlewareSpecForRouteHandler(shim, path) {
30
58
  return {
31
- /**
32
- * The path to use for transaction naming
33
- */
34
- route: path,
35
-
36
- /**
37
- * A function the returns the NodeJS Request Object
38
- *
39
- * The job of the `req` callback is to return the current NodeJS
40
- * IncomingMessage object for this particular handler. Most NodeJS
41
- * frameworks will pass the request to each handler -- sometimes (as
42
- * is the case here) wrapped by another object.
43
- *
44
- * @param {any} shim the Webframework Shim
45
- * @param {any} fn the handler function passed to buildMiddlewareSpec
46
- * @param {any} fnName the handler function's name
47
- * @param {any} args the arguments passed to the handler function
48
- */
49
- req: getRawRequestFromFastifyRequest,
50
59
  /**
51
60
  * A function where we can wrap next, reply send, etc. methods
52
61
  *
@@ -66,6 +75,12 @@ function buildMiddlewareSpecForRouteHandler(shim, path) {
66
75
  * The isFinal param determines whether or not a path is appended for
67
76
  * this particular piece of middleware. (i.e. if this is the final handler
68
77
  * that is actually handling the request, the path is actually left on)
78
+ *
79
+ * @param shim
80
+ * @param fn
81
+ * @param fnName
82
+ * @param args
83
+ * @param bindSegment
69
84
  */
70
85
  next: function wrapNext(shim, fn, fnName, args, bindSegment) {
71
86
  const reply = args[1]
@@ -75,33 +90,28 @@ function buildMiddlewareSpecForRouteHandler(shim, path) {
75
90
  const isFinal = true
76
91
  bindSegment(reply, 'send', isFinal)
77
92
  },
78
-
79
- /**
80
- * A function that returns the request paramates
81
- *
82
- * @param {any} shim the Webframework Shim
83
- * @param {any} fn the handler function passed to buildMiddlewareSpec
84
- * @param {any} fnName the handler function's name
85
- * @param {any} args the arguments passed to the handler function
86
- */
87
- params: getParamsFromFastifyRequest
93
+ params: getParamsFromFastifyRequest,
94
+ req: getRequestFromFastify,
95
+ route: path
88
96
  }
89
97
  }
90
98
 
91
- function buildMiddlewareSpecForMiddlewareFunction() {
99
+ /**
100
+ * Spec for all Fastify middleware(excluding route handlers)
101
+ *
102
+ * @param {WebFrameworkShim} shim
103
+ * @param {string} name metric name for middleware being executed
104
+ * @param route
105
+ * @returns {object} spec for Fastify middleware
106
+ */
107
+ function buildMiddlewareSpecForMiddlewareFunction(shim, name, route) {
92
108
  return {
93
- req: getRawRequestFromFastifyRequest,
94
-
95
- next: function wrapNext(shim, fn, fnName, args, bindSegment) {
96
- const next = args[2]
97
- if (!shim.isFunction(next)) {
98
- return
99
- }
100
- const isFinal = false
101
- bindSegment(next, null, isFinal)
102
- },
103
-
104
- params: getParamsFromFastifyRequest
109
+ name,
110
+ route,
111
+ next: shim.LAST,
112
+ params: getParamsFromFastifyRequest,
113
+ req: getRequestFromFastify,
114
+ type: shim.MIDDLEWARE
105
115
  }
106
116
  }
107
117
 
@@ -4,11 +4,33 @@
4
4
  */
5
5
 
6
6
  'use strict'
7
+
7
8
  const semver = require('semver')
8
9
  const {
9
10
  buildMiddlewareSpecForRouteHandler,
10
11
  buildMiddlewareSpecForMiddlewareFunction
11
12
  } = require('./fastify/spec-builders')
13
+
14
+ /**
15
+ * These are the events that occur during a fastify
16
+ * request
17
+ * see: https://www.fastify.io/docs/latest/Lifecycle/
18
+ *
19
+ * Note: preSerialization and onSend happen after the route handler
20
+ * executes. `onResponse` does not execute until after the client
21
+ * sends the response so it'll never be in scope of the transaction
22
+ */
23
+ const REQUEST_HOOKS = [
24
+ 'onRequest',
25
+ 'preParsing',
26
+ 'preValidation',
27
+ 'preHandler',
28
+ 'preSerialization',
29
+ 'onSend',
30
+ 'onResponse',
31
+ 'onError'
32
+ ]
33
+
12
34
  /**
13
35
  * Sets up fastify route handler
14
36
  *
@@ -20,6 +42,9 @@ const {
20
42
  * those private implementations from access, and getting
21
43
  * at them would require a lot of gymnastics and hard to
22
44
  * maintain code
45
+ *
46
+ * @param shim
47
+ * @param fastify
23
48
  */
24
49
  const setupRouteHandler = (shim, fastify) => {
25
50
  fastify.addHook('onRoute', (routeOptions) => {
@@ -27,10 +52,10 @@ const setupRouteHandler = (shim, fastify) => {
27
52
  return
28
53
  }
29
54
  /**
30
- * recordMiddlware handler call
55
+ * recordMiddleware handler call
31
56
  *
32
57
  * The WebFramework shim treats the main route handler like any other
33
- * i.e. dont be confused by the call to recordMiddlware -- we don't
58
+ * i.e. dont be confused by the call to recordMiddleware -- we don't
34
59
  * have a recordRouteHandler, everything goes through recordMiddleware
35
60
  */
36
61
  const newRouteHandler = shim.recordMiddleware(
@@ -40,29 +65,25 @@ const setupRouteHandler = (shim, fastify) => {
40
65
 
41
66
  routeOptions.handler = newRouteHandler
42
67
  })
43
- }
44
68
 
45
- const setupMiddlewareHandlers = (shim, fastify) => {
46
- shim.wrap(fastify, 'use', function wrapFastifyUse(shim, fn) {
47
- return function wrappedFastifyUser() {
69
+ shim.wrap(fastify, 'addHook', function addWrapHook(shim, fn) {
70
+ return function wrappedAddHook() {
48
71
  const args = shim.argsToArray.apply(shim, arguments)
49
- const middlewareFunction = args[0]
50
- const newMiddlewareFunction = shim.recordMiddleware(
51
- middlewareFunction,
52
- buildMiddlewareSpecForMiddlewareFunction(shim)
53
- )
54
- // replace original function with our function
55
- args[0] = newMiddlewareFunction
72
+ const hookName = args[0]
73
+ if (REQUEST_HOOKS.includes(hookName)) {
74
+ const middlewareFunction = args[1]
75
+ const name = `${hookName}/${shim.getName(middlewareFunction)}`
76
+ const middlewareSpec = buildMiddlewareSpecForMiddlewareFunction(shim, name)
77
+ const newMiddlewareFunction = shim.recordMiddleware(middlewareFunction, middlewareSpec)
56
78
 
79
+ args[1] = newMiddlewareFunction
80
+ }
57
81
  return fn.apply(this, args)
58
82
  }
59
83
  })
60
84
  }
61
85
 
62
86
  module.exports = function initialize(agent, fastify, moduleName, shim) {
63
- if (!agent.config.feature_flag.fastify_instrumentation) {
64
- return
65
- }
66
87
  shim.setFramework(shim.FASTIFY)
67
88
 
68
89
  const fastifyVersion = shim.require('./package.json').version
@@ -73,18 +94,12 @@ module.exports = function initialize(agent, fastify, moduleName, shim) {
73
94
  */
74
95
  const wrappedExport = shim.wrapExport(fastify, function wrapFastifyModule(shim, fn) {
75
96
  return function wrappedFastifyModule() {
76
- // normalize arguments
77
- const args = shim.argsToArray.apply(shim, arguments)
78
-
79
97
  // call original function get get fastify object (which is singleton-ish)
80
- const fastifyForWrapping = fn.apply(this, args)
98
+ const fastifyForWrapping = fn.apply(this, arguments)
81
99
 
82
100
  setupRouteHandler(shim, fastifyForWrapping)
83
101
 
84
- // Don't wrap use() in fastify v3+
85
- if (!isv3Plus) {
86
- setupMiddlewareHandlers(shim, fastifyForWrapping)
87
- }
102
+ setupMiddlewareHandlers(shim, fastifyForWrapping, isv3Plus)
88
103
 
89
104
  return fastifyForWrapping
90
105
  }
@@ -95,10 +110,56 @@ module.exports = function initialize(agent, fastify, moduleName, shim) {
95
110
  }
96
111
  }
97
112
 
113
+ function setupMiddlewareHandlers(shim, fastify, isv3Plus) {
114
+ const mounterSpec = {
115
+ route: shim.FIRST,
116
+ wrapper: wrapMiddleware
117
+ }
118
+
119
+ if (isv3Plus) {
120
+ // Fastify v3+ does not ship with traditional Node.js middleware mounting.
121
+ // This style is accomplished leveraging decorators. Both middie (which was built-in in v2)
122
+ // and fastify-express mount a 'use' function for mounting middleware.
123
+ shim.wrap(fastify, 'decorate', function wrapDecorate(shim, fn) {
124
+ return function wrappedDecorate() {
125
+ const name = arguments[0]
126
+ if (name !== 'use') {
127
+ return fn.apply(this, arguments)
128
+ }
129
+
130
+ const args = shim.argsToArray.apply(shim, arguments)
131
+ args[1] = shim.wrapMiddlewareMounter(args[1], mounterSpec)
132
+
133
+ return fn.apply(this, args)
134
+ }
135
+ })
136
+ } else {
137
+ shim.wrapMiddlewareMounter(fastify, 'use', mounterSpec)
138
+ }
139
+ }
140
+
141
+ function wrapMiddleware(shim, middleware, name, route) {
142
+ if (shim.isWrapped(middleware)) {
143
+ return middleware
144
+ }
145
+
146
+ // prefixing the segment name for middleware execution
147
+ // with the Fastify lifecycle hook
148
+ const segmentName = `onRequest/${name}`
149
+ const spec = buildMiddlewareSpecForMiddlewareFunction(shim, segmentName, route)
150
+
151
+ const newMiddlewareFunction = shim.recordMiddleware(middleware, spec)
152
+
153
+ return newMiddlewareFunction
154
+ }
155
+
98
156
  /**
99
157
  * module.exports = fastify
100
158
  * module.exports.fastify = fastify
101
159
  * module.exports.default = fastify
160
+ *
161
+ * @param original
162
+ * @param wrappedExport
102
163
  */
103
164
  function setupExports(original, wrappedExport) {
104
165
  wrappedExport.fastify = original.fastify
@@ -0,0 +1,242 @@
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 cat = require('../util/cat')
9
+ const recordExternal = require('../metrics/recorders/http_external')
10
+ const logger = require('../logger').child({ component: 'undici' })
11
+ const NAMES = require('../metrics/names')
12
+ const NEWRELIC_SYNTHETICS_HEADER = 'x-newrelic-synthetics'
13
+ const SYMBOLS = {
14
+ SEGMENT: Symbol('__NR_segment'),
15
+ PARENT_SEGMENT: Symbol('__NR_parent_segment')
16
+ }
17
+ const { executionAsyncResource } = require('async_hooks')
18
+
19
+ let diagnosticsChannel = null
20
+ try {
21
+ diagnosticsChannel = require('diagnostics_channel')
22
+ } catch (e) {
23
+ // quick check to see if module exists
24
+ // module was not added until v15.x
25
+ }
26
+
27
+ module.exports = function addUndiciChannels(agent, undici, modName, shim) {
28
+ if (!diagnosticsChannel || !agent.config.feature_flag.undici_instrumentation) {
29
+ logger.warn(
30
+ 'diagnostics_channel or feature_flag.undici_instrumentation = false. Skipping undici instrumentation.'
31
+ )
32
+ return
33
+ }
34
+
35
+ /**
36
+ * Retrieves the current segment in transaction(parent in our context) from executionAsyncResource
37
+ * or from `shim.getSegment()` then adds to the executionAsyncResource for future
38
+ * undici requests within same async context.
39
+ *
40
+ * It was found that when running concurrent undici requests
41
+ * within a transaction that the parent segment would get out of sync
42
+ * depending on the async context of the transaction. By using
43
+ * `async_hooks.executionResource` it is more reliable.
44
+ *
45
+ * Note: However, if you have concurrent undici requests in a transaction
46
+ * and the request to the transaction is using a keep alive there is a chance the
47
+ * executionAsyncResource may be incorrect because of shared connections. To revert to a more
48
+ * naive tracking of parent set `config.feature_flag.undici_async_tracking: false` and
49
+ * it will just call `shim.getSegment()`
50
+ */
51
+ function getParentSegment() {
52
+ if (agent.config.feature_flag.undici_async_tracking) {
53
+ const resource = executionAsyncResource()
54
+
55
+ if (!resource[SYMBOLS.PARENT_SEGMENT]) {
56
+ const parent = shim.getSegment()
57
+ resource[SYMBOLS.PARENT_SEGMENT] = parent
58
+ }
59
+ return resource[SYMBOLS.PARENT_SEGMENT]
60
+ }
61
+ return shim.getSegment()
62
+ }
63
+
64
+ /**
65
+ * This event occurs after the Undici Request is created
66
+ * We will check current segment for opaque and also attach
67
+ * relevant headers to outgoing http request
68
+ *
69
+ * @param {object} params
70
+ * @param {object} params.request undici request object
71
+ */
72
+ diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => {
73
+ const parent = getParentSegment()
74
+ request[SYMBOLS.PARENT_SEGMENT] = parent
75
+ if (!parent || (parent && parent.opaque)) {
76
+ logger.trace(
77
+ 'Not capturing data for outbound request (%s) because parent segment opaque (%s)',
78
+ request.path,
79
+ parent && parent.name
80
+ )
81
+
82
+ return
83
+ }
84
+
85
+ const transaction = parent.transaction
86
+ const outboundHeaders = Object.create(null)
87
+ if (agent.config.encoding_key && transaction.syntheticsHeader) {
88
+ outboundHeaders[NEWRELIC_SYNTHETICS_HEADER] = transaction.syntheticsHeader
89
+ }
90
+
91
+ if (agent.config.distributed_tracing.enabled) {
92
+ transaction.insertDistributedTraceHeaders(outboundHeaders)
93
+ } else if (agent.config.cross_application_tracer.enabled) {
94
+ cat.addCatHeaders(agent.config, transaction, outboundHeaders)
95
+ } else {
96
+ logger.trace('Both DT and CAT are disabled, not adding headers!')
97
+ }
98
+
99
+ // eslint-disable-next-line guard-for-in
100
+ for (const key in outboundHeaders) {
101
+ request.addHeader(key, outboundHeaders[key])
102
+ }
103
+ })
104
+
105
+ /**
106
+ * This event occurs right before the data is written to the socket.
107
+ * Undici has some abstracted headers that are only created at this time, one
108
+ * is the `host` header which we need to name the Undici segment. So in this
109
+ * handler we create, start and set the segment active, name it, and
110
+ * attach the url/procedure/request.parameters
111
+ *
112
+ * @param {object} params
113
+ * @param {object} params.request undicie request object
114
+ * @param {TLSSocket | net.Socket} socket active socket connection
115
+ */
116
+ diagnosticsChannel.channel('undici:client:sendHeaders').subscribe(({ request, socket }) => {
117
+ const parentSegment = request[SYMBOLS.PARENT_SEGMENT]
118
+ if (!parentSegment || (parentSegment && parentSegment.opaque)) {
119
+ return
120
+ }
121
+
122
+ const port = socket.remotePort
123
+ const isHttps = socket.servername
124
+ let urlString
125
+ if (isHttps) {
126
+ urlString = `https://${socket.servername}`
127
+ urlString += port === 443 ? request.path : `:${port}${request.path}`
128
+ } else {
129
+ urlString = `http://${socket._host}`
130
+ urlString += port === 80 ? request.path : `:${port}${request.path}`
131
+ }
132
+
133
+ const url = new URL(urlString)
134
+
135
+ const name = NAMES.EXTERNAL.PREFIX + url.host + url.pathname
136
+ const segment = shim.createSegment(name, recordExternal(url.host, 'undici'), parentSegment)
137
+ if (segment) {
138
+ segment.start()
139
+ shim.setActiveSegment(segment)
140
+ segment.addAttribute('url', `${url.protocol}//${url.host}${url.pathname}`)
141
+
142
+ url.searchParams.forEach((value, key) => {
143
+ segment.addSpanAttribute(`request.parameters.${key}`, value)
144
+ })
145
+ segment.addAttribute('procedure', request.method || 'GET')
146
+ request[SYMBOLS.SEGMENT] = segment
147
+ }
148
+ })
149
+
150
+ /**
151
+ * This event occurs after the response headers have been received.
152
+ * We will add the relevant http response attributes to active segment.
153
+ * Also add CAT specific keys to active segment.
154
+ *
155
+ * @param {object} params
156
+ * @param {object} params.request undici request object
157
+ * @param {object} params.response { statusCode, headers, statusText }
158
+ */
159
+ diagnosticsChannel.channel('undici:request:headers').subscribe(({ request, response }) => {
160
+ const activeSegment = request[SYMBOLS.SEGMENT]
161
+ if (!activeSegment) {
162
+ return
163
+ }
164
+
165
+ activeSegment.addSpanAttribute('http.statusCode', response.statusCode)
166
+ activeSegment.addSpanAttribute('http.statusText', response.statusText)
167
+
168
+ if (
169
+ agent.config.cross_application_tracer.enabled &&
170
+ !agent.config.distributed_tracing.enabled
171
+ ) {
172
+ try {
173
+ const { appData } = cat.extractCatHeaders(response.headers)
174
+ const decodedAppData = cat.parseAppData(agent.config, appData)
175
+ const attrs = activeSegment.getAttributes()
176
+ const url = new URL(attrs.url)
177
+ cat.assignCatToSegment(decodedAppData, activeSegment, url.host)
178
+ } catch (err) {
179
+ logger.warn(err, 'Cannot add CAT data to segment')
180
+ }
181
+ }
182
+ })
183
+
184
+ /**
185
+ * This event occurs after the response body has been received.
186
+ * We will end the active segment and set the active back to parent before request
187
+ *
188
+ * @param {object} params.request undici request object
189
+ */
190
+ diagnosticsChannel.channel('undici:request:trailers').subscribe(({ request }) => {
191
+ endAndRestoreSegment(request)
192
+ })
193
+
194
+ /**
195
+ * This event occurs right before the request emits an error.
196
+ * We will end the active segment and set the active back to parent before request.
197
+ * We will also log errors to NR
198
+ *
199
+ * Note: This event occurs before the error handler so we will always log it for now.
200
+ */
201
+ diagnosticsChannel.channel('undici:request:error').subscribe(({ request, error }) => {
202
+ endAndRestoreSegment(request, error)
203
+ })
204
+
205
+ /**
206
+ * Gets the active and parent from given ctx(request, client connector)
207
+ * and ends active and restores parent to active. If an error exists
208
+ * it will add the error to the transaction
209
+ *
210
+ * @param {object} ctx request or client connector
211
+ * @param {Error} error
212
+ */
213
+ function endAndRestoreSegment(ctx, error) {
214
+ const activeSegment = ctx[SYMBOLS.SEGMENT]
215
+ const parentSegment = ctx[SYMBOLS.PARENT_SEGMENT]
216
+ if (activeSegment) {
217
+ activeSegment.end()
218
+
219
+ if (error) {
220
+ handleError(activeSegment, error)
221
+ }
222
+
223
+ if (parentSegment) {
224
+ shim.setActiveSegment(parentSegment)
225
+ }
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Adds the error to the active transaction
231
+ *
232
+ * @param {TraceSegment} activeSegment
233
+ * @param {Error} error
234
+ */
235
+ function handleError(activeSegment, error) {
236
+ logger.trace(error, 'Captured outbound error on behalf of the user.')
237
+ const tx = activeSegment.transaction
238
+ shim.agent.errors.add(tx, error)
239
+ }
240
+ }
241
+
242
+ module.exports.SYMBOLS = SYMBOLS
@@ -31,6 +31,7 @@ module.exports = function instrumentations() {
31
31
  'redis': { type: MODULE_TYPE.DATASTORE },
32
32
  'restify': { type: MODULE_TYPE.WEB_FRAMEWORK },
33
33
  'superagent': { module: '@newrelic/superagent' },
34
+ 'undici': { type: MODULE_TYPE.TRANSACTION },
34
35
  'oracle': { type: null },
35
36
  'vision': { type: MODULE_TYPE.WEB_FRAMEWORK },
36
37
  'when': { type: null }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "8.4.0",
3
+ "version": "8.5.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [
@@ -134,8 +134,8 @@
134
134
  "docs": "npm ci && jsdoc -c ./jsdoc-conf.json --private -r .",
135
135
  "integration": "npm run prepare-test && npm run sub-install && time tap test/integration/**/**/*.tap.js --timeout=180 --no-coverage --reporter classic",
136
136
  "prepare-test": "npm run ca-gen && npm run ssl && npm run docker-env",
137
- "lint": "eslint ./*.js lib test bin",
138
- "lint:fix": "eslint --fix, ./*.js lib test bin",
137
+ "lint": "eslint ./*.js lib test bin examples",
138
+ "lint:fix": "eslint --fix, ./*.js lib test bin examples",
139
139
  "public-docs": "npm ci && jsdoc -c ./jsdoc-conf.json --tutorials examples/shim api.js lib/shim/ lib/transaction/handle.js && cp examples/shim/*.png out/",
140
140
  "publish-docs": "./bin/publish-docs.sh",
141
141
  "services": "./bin/docker-services.sh",
@@ -187,14 +187,15 @@
187
187
  "commander": "^7.0.0",
188
188
  "eslint": "^7.32.0",
189
189
  "eslint-config-prettier": "^8.3.0",
190
+ "eslint-plugin-disable": "^2.0.1",
190
191
  "eslint-plugin-header": "^3.1.1",
192
+ "eslint-plugin-jsdoc": "^36.1.0",
191
193
  "eslint-plugin-node": "^11.1.0",
192
194
  "eslint-plugin-prettier": "^3.4.0",
193
195
  "express": "*",
194
196
  "generic-pool": "^3.6.1",
195
197
  "glob": "^7.1.2",
196
198
  "got": "^8.0.1",
197
- "http-errors": "^1.7.3",
198
199
  "husky": "^6.0.0",
199
200
  "jsdoc": "^3.6.3",
200
201
  "lint-staged": "^11.0.0",