newrelic 9.8.1 → 9.9.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,32 @@
1
+ ### v9.9.0 (2023-02-06)
2
+ * Added support for url obfuscation using regex (Thanks for your contribution @matewilk)
3
+ * For Distributed Tracing, this means that:
4
+ * Incoming and outgoing requests' `path` will be obfuscated
5
+ * Transaction's `request.url` attribute will be obfuscated
6
+ * Span's `http.uri` path will be obfuscated
7
+ * For transactions, this means that:
8
+ * Transaction trace details `url` will be obfuscated
9
+ * With the following example configuration, url obfuscation will turn `/api/v1/users/12345456/edit` to `/api/v1/users/**/edit`.
10
+ ```js
11
+ url_obfuscation: {
12
+ enabled: true,
13
+ regex: {
14
+ pattern: /(\/api\/v1\/users\/)([\d]+)(\/.*$)/,
15
+ flags: "i",
16
+ replacement: '$1**$3'
17
+ }
18
+ }
19
+ ```
20
+ * You can also use environment variables to configure url obfuscation:
21
+ ```yml
22
+ NEW_RELIC_URL_OBFUSCATION_ENABLED: "true",
23
+ NEW_RELIC_URL_OBFUSCATION_REGEX_PATTERN: '/(\/api\/v1\/users\/)([\d]+)(\/.*$)/',
24
+ NEW_RELIC_URL_OBFUSCATION_REGEX_FLAGS: 'i',
25
+ NEW_RELIC_URL_OBFUSCATION_REGEX_REPLACEMENT: '$1**$3'
26
+ ```
27
+
28
+ * Add a new tracking type of instrumentation. This will be responsible for logging `Supportability/Features/Instrumentation/OnResolved/<pkg>` and `Supportability/Features/Instrumentation/OnResolved/<pkg>/Version/<version>` metrics when packages are required.
29
+
1
30
  ### v9.8.1 (2023-01-25)
2
31
 
3
32
  * Changed GCP metadata parsing to use `json-bigint` to avoid loss of precision from numerical instance ID.
@@ -2680,7 +2680,7 @@ SOFTWARE.
2680
2680
 
2681
2681
  ### jsdoc
2682
2682
 
2683
- This product includes source derived from [jsdoc](https://github.com/jsdoc/jsdoc) ([v3.6.11](https://github.com/jsdoc/jsdoc/tree/v3.6.11)), distributed under the [Apache-2.0 License](https://github.com/jsdoc/jsdoc/blob/v3.6.11/LICENSE.md):
2683
+ This product includes source derived from [jsdoc](https://github.com/jsdoc/jsdoc) ([v4.0.0](https://github.com/jsdoc/jsdoc/tree/v4.0.0)), distributed under the [Apache-2.0 License](https://github.com/jsdoc/jsdoc/blob/v4.0.0/LICENSE.md):
2684
2684
 
2685
2685
  ```
2686
2686
  # License
@@ -9355,12 +9355,12 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
9355
9355
 
9356
9356
  ### tap
9357
9357
 
9358
- This product includes source derived from [tap](https://github.com/tapjs/node-tap) ([v16.3.0](https://github.com/tapjs/node-tap/tree/v16.3.0)), distributed under the [ISC License](https://github.com/tapjs/node-tap/blob/v16.3.0/LICENSE):
9358
+ This product includes source derived from [tap](https://github.com/tapjs/node-tap) ([v16.3.4](https://github.com/tapjs/node-tap/tree/v16.3.4)), distributed under the [ISC License](https://github.com/tapjs/node-tap/blob/v16.3.4/LICENSE):
9359
9359
 
9360
9360
  ```
9361
9361
  The ISC License
9362
9362
 
9363
- Copyright (c) 2011-2022 Isaac Z. Schlueter and Contributors
9363
+ Copyright (c) 2011-2023 Isaac Z. Schlueter and Contributors
9364
9364
 
9365
9365
  Permission to use, copy, modify, and/or distribute this software for any
9366
9366
  purpose with or without fee is hereby granted, provided that the above
@@ -9,12 +9,6 @@ const fetchSystemInfo = require('../system-info')
9
9
  const logger = require('../logger').child({ component: 'facts' })
10
10
  const os = require('os')
11
11
  const parseLabels = require('../util/label-parser')
12
- const NAMES = require('../metrics/names')
13
-
14
- // For now static and tentative list of which logging libraries we
15
- // want to track. Later might come up with a better way of populating
16
- // this list.
17
- const LOG_LIBRARIES = ['winston', 'bunyan', 'pino', 'loglevel', 'npmlog', 'fancy-log']
18
12
 
19
13
  module.exports = facts
20
14
 
@@ -38,8 +32,6 @@ async function facts(agent, callback) {
38
32
  systemInfo = systemInfo || Object.create(null)
39
33
  environment = environment || []
40
34
 
41
- countSupportabilityForLogLibraries(environment, agent)
42
-
43
35
  const hostname = agent.config.getHostnameSafe()
44
36
  const results = {
45
37
  utilization: {
@@ -148,23 +140,3 @@ function getIdentifierOverride(appNames) {
148
140
  appNames.concat([]).sort().join(',')
149
141
  ].join(':')
150
142
  }
151
-
152
- /**
153
- * This function searches the computed environment for the presence of
154
- * some popular logging libraries. If present, it increments the
155
- * corresponding supportability metrics.
156
- *
157
- * @param {Array} environment the environment computed by
158
- * @param {object} agent the New Relic agent
159
- */
160
- function countSupportabilityForLogLibraries(environment, agent) {
161
- const packages = environment.find((elt) => elt[0] === 'Packages')[1]
162
- packages.forEach((pkg) => {
163
- pkg = JSON.parse(pkg)[0]
164
- if (LOG_LIBRARIES.includes(pkg)) {
165
- agent.metrics
166
- .getOrCreateMetric(`${NAMES.SUPPORTABILITY.NODEJS_DEPENDENCIES}/${pkg}`)
167
- .incrementCallCount()
168
- }
169
- })
170
- }
@@ -6,7 +6,7 @@
6
6
  'use strict'
7
7
 
8
8
  const defaultConfig = module.exports
9
- const { array, int, float, boolean, object, objectList, allowList } = require('./formatters')
9
+ const { array, int, float, boolean, object, objectList, allowList, regex } = require('./formatters')
10
10
 
11
11
  /**
12
12
  * A function that returns the definition of the agent configuration
@@ -52,6 +52,13 @@ defaultConfig.definition = () => ({
52
52
  * You shouldn't need to change this.
53
53
  */
54
54
  host: '',
55
+ /**
56
+ * Endpoint to send OpenTelemetry spans to.
57
+ *
58
+ * This should be automatically deduced from your region and other
59
+ * settings, but if desired, you can override it.
60
+ */
61
+ otlp_endpoint: '',
55
62
  /**
56
63
  * The port on which the collector proxy will be listening.
57
64
  *
@@ -1208,6 +1215,47 @@ defaultConfig.definition = () => ({
1208
1215
  formatter: boolean,
1209
1216
  default: false
1210
1217
  }
1218
+ },
1219
+
1220
+ /**
1221
+ * Obfuscates URL parameters
1222
+ * for outgoing and incoming requests
1223
+ * for distrubuted tracing attributes - both transaction and span attributes
1224
+ * for transaction trace transaction details
1225
+ */
1226
+ url_obfuscation: {
1227
+ /**
1228
+ * Toggles whether to obfuscate URL parameters
1229
+ */
1230
+ enabled: {
1231
+ formatter: boolean,
1232
+ default: false
1233
+ },
1234
+
1235
+ regex: {
1236
+ /**
1237
+ * A regular expression to match URL parameters to obfuscate
1238
+ */
1239
+ pattern: {
1240
+ formatter: regex,
1241
+ default: null
1242
+ },
1243
+
1244
+ /**
1245
+ * A string containing RegEx flags to use when matching URL parameters
1246
+ */
1247
+ flags: {
1248
+ default: ''
1249
+ },
1250
+
1251
+ /**
1252
+ * A string containing a replacement value for URL parameters
1253
+ * can contain refferences to capture groups in the pattern
1254
+ */
1255
+ replacement: {
1256
+ default: ''
1257
+ }
1258
+ }
1211
1259
  }
1212
1260
  })
1213
1261
 
@@ -106,3 +106,19 @@ formatters.objectList = function objectList(val, logger) {
106
106
  formatters.allowList = function allowList(list, val) {
107
107
  return list.includes(val) ? val : list[0]
108
108
  }
109
+
110
+ /**
111
+ * Parse a config setting as a regex
112
+ *
113
+ * @param {string} val valid regex
114
+ * @param {logger} logger agent logger instance
115
+ * @returns {RegExp} regex
116
+ */
117
+ formatters.regex = function regex(val, logger) {
118
+ try {
119
+ return new RegExp(val)
120
+ } catch (error) {
121
+ logger.error(`New Relic configurator could not validate regex: ${val}`)
122
+ logger.error(error.stack)
123
+ }
124
+ }
@@ -993,7 +993,7 @@ Config.prototype._fromPassed = function _fromPassed(external, internal, arbitrar
993
993
  return
994
994
  }
995
995
 
996
- if (typeof node === 'object' && !Array.isArray(node)) {
996
+ if (typeof node === 'object' && !Array.isArray(node) && !(node instanceof RegExp)) {
997
997
  // is top level and can have arbitrary keys
998
998
  const allowArbitrary = internal === this || HAS_ARBITRARY_KEYS.has(key)
999
999
  this._fromPassed(node, internal[key], allowArbitrary)
@@ -1095,7 +1095,13 @@ Config.prototype._fromEnvironment = function _fromEnvironment(
1095
1095
  setFromEnv({ config, key, envVar, paths })
1096
1096
  } else if (type === 'object') {
1097
1097
  if (value.hasOwnProperty('env')) {
1098
- setFromEnv({ config, key, envVar: value.env, paths, formatter: value.formatter })
1098
+ setFromEnv({
1099
+ config,
1100
+ key,
1101
+ envVar: value.env,
1102
+ paths,
1103
+ formatter: value.formatter
1104
+ })
1099
1105
  } else if (value.hasOwnProperty('default')) {
1100
1106
  const envVar = deriveEnvVar(key, paths)
1101
1107
  setFromEnv({ config, key, envVar, formatter: value.formatter, paths })
@@ -1298,14 +1304,21 @@ Config.prototype._canonicalize = function _canonicalize() {
1298
1304
  const level = this.logging.level
1299
1305
  this.logging.level = logAliases[level] || level
1300
1306
 
1307
+ const region = parseKey(this.license_key)
1301
1308
  if (this.host === '') {
1302
- const region = parseKey(this.license_key)
1303
1309
  if (region) {
1304
- this.host = 'collector.' + region + '.nr-data.net'
1310
+ this.host = `collector.${region}.nr-data.net`
1305
1311
  } else {
1306
1312
  this.host = 'collector.newrelic.com'
1307
1313
  }
1308
1314
  }
1315
+ if (this.otlp_endpoint === '') {
1316
+ if (region) {
1317
+ this.otlp_endpoint = `otlp.${region}.nr-data.net`
1318
+ } else {
1319
+ this.otlp_endpoint = 'otlp.nr-data.net'
1320
+ }
1321
+ }
1309
1322
 
1310
1323
  if (this.license_key) {
1311
1324
  this.license_key = this.license_key.trim()
@@ -215,6 +215,7 @@ function applySegment(opts, makeRequest, hostname, segment) {
215
215
  segment.start()
216
216
  const request = makeRequest(opts)
217
217
  const parsed = urltils.scrubAndParseParameters(request.path)
218
+ parsed.path = urltils.obfuscatePath(segment.transaction.agent.config, parsed.path)
218
219
  const proto = parsed.protocol || opts.protocol || 'http:'
219
220
  segment.name += parsed.path
220
221
  request[symbols.segment] = segment
@@ -86,7 +86,7 @@ function wrapEmitWithTransaction(agent, emit, isHTTPS) {
86
86
 
87
87
  // the error tracer needs a URL for tracing, even though naming overwrites
88
88
  transaction.parsedUrl = url.parse(request.url, true)
89
- transaction.url = transaction.parsedUrl.pathname
89
+ transaction.url = urltils.obfuscatePath(agent.config, transaction.parsedUrl.path)
90
90
  transaction.verb = request.method
91
91
 
92
92
  // URL is sent as an agent attribute with transaction events
@@ -38,6 +38,21 @@ module.exports = function instrumentations() {
38
38
  'undici': { type: MODULE_TYPE.TRANSACTION },
39
39
  '@hapi/vision': { type: MODULE_TYPE.WEB_FRAMEWORK },
40
40
  'when': { module: './instrumentation/when' },
41
- 'winston': { type: MODULE_TYPE.GENERIC }
41
+ 'winston': { type: MODULE_TYPE.GENERIC },
42
+ /**
43
+ * The modules below are listed here purely to take
44
+ * advantage of the Supportability/Features/onRequire/<module>
45
+ * metrics for libraries we want to track for some reason or another.
46
+ * The big uses cases are:
47
+ * Logging libraries we want to instrument in the future
48
+ * Libraries that have OpenTelemetry instrumentation we want to register
49
+ * or have already registered.
50
+ */
51
+ 'loglevel': { type: MODULE_TYPE.TRACKING },
52
+ 'npmlog': { type: MODULE_TYPE.TRACKING },
53
+ 'fancy-log': { type: MODULE_TYPE.TRACKING },
54
+ '@prisma/client': { type: MODULE_TYPE.TRACKING },
55
+ '@nestjs/core': { type: MODULE_TYPE.TRACKING },
56
+ 'knex': { type: MODULE_TYPE.TRACKING }
42
57
  }
43
58
  }
@@ -23,7 +23,6 @@ const SUPPORTABILITY = {
23
23
  TRANSACTION_API: 'Supportability/API/Transaction',
24
24
  UTILIZATION: 'Supportability/utilization',
25
25
  DEPENDENCIES: 'Supportability/InstalledDependencies',
26
- NODEJS_DEPENDENCIES: 'Supportability/InstalledDependencies/Nodejs',
27
26
  NODEJS: 'Supportability/Nodejs',
28
27
  REGISTRATION: 'Supportability/Registration',
29
28
  EVENT_HARVEST: 'Supportability/EventHarvest',
@@ -32,5 +32,11 @@ exports.MODULE_TYPE = {
32
32
  TRANSACTION: 'transaction',
33
33
 
34
34
  /** Web server framework module, such as Express or Restify. */
35
- WEB_FRAMEWORK: 'web-framework'
35
+ WEB_FRAMEWORK: 'web-framework',
36
+
37
+ /**
38
+ * Used to load supportability metrics on installed verisions of packages
39
+ * that the Node.js agent does not instrument(i.e. - otel instrumentation or top logging libraries)
40
+ */
41
+ TRACKING: 'tracking'
36
42
  }
package/lib/shimmer.js CHANGED
@@ -530,6 +530,13 @@ function instrumentPostLoad(agent, nodule, moduleName, resolvedName) {
530
530
  NAMES.FEATURES.INSTRUMENTATION.ON_REQUIRE
531
531
  )
532
532
 
533
+ // Tracking instrumentation is only used to add the supportability metrics
534
+ // that occur directly above this. No reason to attempt to load instrumentation
535
+ // as it does not exist.
536
+ if (instrumentation.type === MODULE_TYPE.TRACKING) {
537
+ return nodule
538
+ }
539
+
533
540
  try {
534
541
  if (instrumentation.onRequire(shim, nodule, moduleName) !== false) {
535
542
  nodule = shim.getExport(nodule)
@@ -580,7 +587,6 @@ function _firstPartyInstrumentation(agent, fileName, shim, nodule, moduleName) {
580
587
 
581
588
  function _postLoad(agent, nodule, name, resolvedName) {
582
589
  const instrumentation = shimmer.getInstrumentationNameFromModuleName(name)
583
-
584
590
  const registeredInstrumentation = shimmer.registeredInstrumentations[instrumentation]
585
591
  const hasPostLoadInstrumentation =
586
592
  registeredInstrumentation && registeredInstrumentation.onRequire
@@ -596,7 +602,6 @@ function _postLoad(agent, nodule, name, resolvedName) {
596
602
 
597
603
  function _onResolveFileName(agent, requiredNameOrPath, resolvedFilepath) {
598
604
  const instrumentation = shimmer.getInstrumentationNameFromModuleName(requiredNameOrPath)
599
-
600
605
  const registeredInstrumentation = shimmer.registeredInstrumentations[instrumentation]
601
606
  const hasResolvedFileInstrumentation =
602
607
  registeredInstrumentation && registeredInstrumentation.onResolved
@@ -8,6 +8,7 @@
8
8
  /* eslint sonarjs/cognitive-complexity: ["error", 19] -- TODO: https://issues.newrelic.com/browse/NEWRELIC-5252 */
9
9
 
10
10
  const codec = require('../../util/codec')
11
+ const urltils = require('../../util/urltils')
11
12
  const Segment = require('./segment')
12
13
  const { Attributes, MAXIMUM_CUSTOM_ATTRIBUTES } = require('../../attributes')
13
14
  const logger = require('../../logger').child({ component: 'trace' })
@@ -329,7 +330,9 @@ Trace.prototype._getRequestUri = function _getRequestUri() {
329
330
  const canAddUri = this.attributes.hasValidDestination(DESTINATIONS.TRANS_TRACE, REQUEST_URI_KEY)
330
331
  let requestUri = null // must be null if excluded
331
332
  if (canAddUri) {
332
- requestUri = this.transaction.url || UNKNOWN_URI_PLACEHOLDER
333
+ // obfuscate the path if config is set
334
+ const url = urltils.obfuscatePath(this.transaction.agent.config, this.transaction.url)
335
+ requestUri = url || UNKNOWN_URI_PLACEHOLDER
333
336
  }
334
337
 
335
338
  return requestUri
@@ -6,6 +6,7 @@
6
6
  'use strict'
7
7
 
8
8
  const url = require('url')
9
+ const logger = require('../logger').child({ component: 'urltils' })
9
10
 
10
11
  const LOCALHOST_NAMES = {
11
12
  'localhost': true,
@@ -162,6 +163,29 @@ module.exports = {
162
163
  }
163
164
  },
164
165
 
166
+ /**
167
+ * Obfuscates path parameters with regex from config
168
+ *
169
+ * @param {Config} config The configuration containing the regex
170
+ * @param {string} path The path to be obfuscated
171
+ * @returns {string} The obfuscated path or the original path
172
+ */
173
+ obfuscatePath: function obfuscatePath(config, path) {
174
+ const { enabled, regex } = config.url_obfuscation
175
+ if (typeof path !== 'string' || !enabled || !regex) {
176
+ return path
177
+ }
178
+
179
+ const { pattern, flags = '', replacement = '' } = regex
180
+ try {
181
+ const regexPattern = new RegExp(pattern, flags)
182
+ return path.replace(regexPattern, replacement)
183
+ } catch (e) {
184
+ logger.warn('Invalid regular expression for url_obfuscation.regex.pattern', pattern)
185
+ return path
186
+ }
187
+ },
188
+
165
189
  /**
166
190
  * Copy a set of request parameters from one object to another,
167
191
  * but do not overwrite any existing parameters in destination,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "9.8.1",
3
+ "version": "9.9.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [
@@ -221,7 +221,7 @@
221
221
  "glob": "^7.1.2",
222
222
  "got": "^11.8.5",
223
223
  "husky": "^6.0.0",
224
- "jsdoc": "^3.6.3",
224
+ "jsdoc": "^4.0.0",
225
225
  "lint-staged": "^11.0.0",
226
226
  "lockfile-lint": "^4.9.6",
227
227
  "memcached": ">=0.2.8",
@@ -234,7 +234,7 @@
234
234
  "rimraf": "^2.6.3",
235
235
  "should": "*",
236
236
  "sinon": "^4.5.0",
237
- "tap": "^16.0.1",
237
+ "tap": "^16.3.4",
238
238
  "temp": "^0.8.1",
239
239
  "testdouble": "^3.16.6",
240
240
  "when": "*"