newrelic 6.6.0 → 6.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,84 @@
1
+ ### 6.9.0 (2020-06-08):
2
+
3
+ * Added AWS API Gateway V2 Support to lambda instrumentation.
4
+
5
+ * Added 'transaction.name' intrinsic to active span at time transaction name is finalized.
6
+
7
+ This enables finding transaction name for traces that may not have a matching transaction event.
8
+
9
+ * Added 'error.expected' attribute to span active at time expected error was noticed.
10
+
11
+ * Dropped errors earlier during collection when error collection is disabled.
12
+
13
+ Error attributes will no longer show up on spans when error collection has been disabled. Other unnecessary work will also be avoided.
14
+
15
+ * Removed allocation of logging-only objects used by transaction naming when those log levels are disabled.
16
+
17
+ * Upgraded escodegen from 1.12.0 to 1.14.1.
18
+
19
+ * Upgraded readable-stream from 3.4.0 to 3.6.0.
20
+
21
+ * Upgraded @grpc/proto-loader from 0.5.3 to 0.5.4.
22
+
23
+ * Converted facts unit test to use tap API.
24
+
25
+ * Converted transaction 'finalizeName...' unit tests to use tap API.
26
+
27
+ * Added several items to .npmignore to prevent accidental publishing.
28
+
29
+ * Fixed Redis client w/o callback versioned test flicker.
30
+
31
+ Doesn't end transaction until error encountered. Increases wait time for first operation which has to complete for the second operation to be successful.
32
+
33
+ ### 6.8.0 (2020-05-21):
34
+
35
+ * Bumped @newrelic/native-metrics to ^5.1.0.
36
+
37
+ Upgraded nan to ^2.14.1 to resolve 'GetContents' deprecation warning with Node 14. This version of the native metrics module is tested against Node 14 and includes a pre-built binary download backup for Node 14.
38
+
39
+ * Added whitespace trimming of license key configuration values.
40
+
41
+ Previously, when a license key was entered with leading or trailing whitespace, it would be used as-is and result in a validation failure. This most commonly occurred with environment variable based configuration.
42
+
43
+ * Moved to GitHub actions for CI.
44
+
45
+ * Updated PR template and added initial issue templates.
46
+
47
+ * Converted most of the collector API unit tests to use the tap API. Split larger test groupings into their own test files.
48
+
49
+ ### 6.7.1 (2020-05-14):
50
+
51
+ * Added synthetics headers to transaction event intrinsics for DT
52
+
53
+ * Fixed stale comment documentation with regards to segment recording
54
+
55
+ ### 6.7.0 (2020-05-06):
56
+
57
+ * Added a configurable-length span queue to Infinite Tracing:
58
+ infinite_tracing.span_events.queue_size.
59
+
60
+ The queue length can be modified to balance the needs of keeping full traces
61
+ against trade-off of memory usage and CPU overhead in a high-throughput
62
+ application.
63
+
64
+ * Fixed issue where API.instrumentLoadedModule could throw an exception when it
65
+ failed.
66
+
67
+ Error details will now be caught and logged.
68
+
69
+ * Resolved non-proxy minimist security warnings by bumping dependencies.
70
+
71
+ These updates only impact development dependencies. Thank you to @devfreddy for
72
+ the contribution.
73
+
74
+ * Updated minimist sub-deps to resolve most related security warnings.
75
+ * Updated tap to resolve remaining tap security warnings.
76
+ * Updated @newrelic/proxy.
77
+
78
+ * Updated remaining /api unit tests to use tap API.
79
+
80
+ * Updated @grpc/grpc-js to v1.0.3.
81
+
1
82
  ### 6.6.0 (2020-04-20):
2
83
 
3
84
  * Added support for [Infinite Tracing on New Relic
package/README.md CHANGED
@@ -742,3 +742,5 @@ license and the licenses of its dependencies.
742
742
  [6]: https://github.com/newrelic/node-newrelic/blob/master/lib/config/default.js
743
743
  [7]: https://docs.newrelic.com/docs/agents/nodejs-agent/installation-configuration/nodejs-agent-configuration
744
744
 
745
+
746
+
package/api.js CHANGED
@@ -342,13 +342,13 @@ API.prototype.addCustomAttributes = function addCustomAttributes(atts) {
342
342
 
343
343
  /**
344
344
  * Add custom span attributes in an object to the current segment/span.
345
- *
345
+ *
346
346
  * See documentation for newrelic.addCustomSpanAttribute for more information.
347
- *
347
+ *
348
348
  * An example of setting a custom span attribute:
349
- *
349
+ *
350
350
  * newrelic.addCustomSpanAttribute({test: 'value', test2: 'value2'})
351
- *
351
+ *
352
352
  * @param {object} [atts]
353
353
  * @param {string} [atts.KEY] The name you want displayed in the RPM UI.API.
354
354
  * @param {string} [atts.KEY.VALUE] The value you want displayed. Must be serializable.
@@ -396,7 +396,7 @@ API.prototype.addCustomSpanAttribute = function addCustomSpanAttribute(key, valu
396
396
  }
397
397
 
398
398
  const segment = this.agent.tracer.getSegment()
399
-
399
+
400
400
  if (!segment) {
401
401
  return logger.debug(
402
402
  'Could not add attribute %s. No available span/segment.',
@@ -1344,31 +1344,37 @@ function instrumentLoadedModule(moduleName, module) {
1344
1344
  NAMES.SUPPORTABILITY.API + '/instrumentLoadedModule'
1345
1345
  )
1346
1346
  metric.incrementCallCount()
1347
+ try {
1348
+ const instrumentationName = shimmer.getInstrumentationNameFromModuleName(moduleName)
1349
+ if (!shimmer.registeredInstrumentations[instrumentationName]) {
1350
+ logger.warn("No instrumentation registered for '%s'.", instrumentationName)
1351
+ return false
1352
+ }
1347
1353
 
1348
- const instrumentationName = shimmer.getInstrumentationNameFromModuleName(moduleName)
1349
- if (!shimmer.registeredInstrumentations[instrumentationName]) {
1350
- logger.warn("No instrumentation registered for '%s'.", instrumentationName)
1351
- return false
1352
- }
1353
-
1354
- const instrumentation = shimmer.registeredInstrumentations[instrumentationName]
1355
- if (!instrumentation.onRequire) {
1356
- logger.warn("No onRequire function registered for '%s'.", instrumentationName)
1357
- return false
1358
- }
1354
+ const instrumentation = shimmer.registeredInstrumentations[instrumentationName]
1355
+ if (!instrumentation.onRequire) {
1356
+ logger.warn("No onRequire function registered for '%s'.", instrumentationName)
1357
+ return false
1358
+ }
1359
1359
 
1360
- const resolvedName = require.resolve(moduleName)
1360
+ const resolvedName = require.resolve(moduleName)
1361
1361
 
1362
- const shim = shims.createShimFromType(
1363
- instrumentation.type,
1364
- this.agent,
1365
- moduleName,
1366
- resolvedName
1367
- )
1362
+ const shim = shims.createShimFromType(
1363
+ instrumentation.type,
1364
+ this.agent,
1365
+ moduleName,
1366
+ resolvedName
1367
+ )
1368
1368
 
1369
- instrumentation.onRequire(shim, module, moduleName)
1369
+ instrumentation.onRequire(shim, module, moduleName)
1370
1370
 
1371
- return true
1371
+ return true
1372
+ } catch (error) {
1373
+ logger.error(
1374
+ 'instrumentLoadedModule encountered an error, module not instrumentend: %s',
1375
+ error
1376
+ )
1377
+ }
1372
1378
  }
1373
1379
 
1374
1380
  /**
@@ -21,5 +21,14 @@ fi
21
21
 
22
22
  export AGENT_PATH=`pwd`
23
23
 
24
+ # @koa/router 8.03+ breaks segment naming for nested routes
25
+ skip="koa"
26
+
27
+ # Don't run the aws-sdk tests if we don't have the keys set
28
+ if [[ -z "$AWS_ACCESS_KEY_ID" ]]; then
29
+ skip="${skip},aws-sdk"
30
+ fi
31
+
32
+
24
33
  # This is meant to be temporary. Remove once new major version with fixes rolled into agent.
25
- time ./node_modules/.bin/versioned-tests $VERSIONED_MODE -i 2 -s koa ${directories[@]}
34
+ time ./node_modules/.bin/versioned-tests $VERSIONED_MODE -i 2 -s $skip ${directories[@]}
package/bin/ssl.sh CHANGED
@@ -8,7 +8,7 @@ set -x # be chatty and show the lines we're running
8
8
  # that are mysterious and not understood, so let
9
9
  # bail early if we detect that's the case
10
10
  ENGINE_OPENSSL=`openssl version | awk '{print $(1)}'`
11
- if [ "$ENGINE_OPENSSL" == "LibreSSL" ]
11
+ if [ "$ENGINE_OPENSSL" = "LibreSSL" ]
12
12
  then
13
13
  echo "LibreSSL is not supported, please install a stock openssl and \n"
14
14
  echo "make sure that openssl binary is in your PATH"
@@ -25,7 +25,7 @@ CERTIFICATE="test/lib/self-signed-test-certificate.crt"
25
25
 
26
26
  # USAGE: ./bin/ssl.sh clear
27
27
  # a sub command to remove all the generated files and start over
28
- if [ "$1" == "clear" ]
28
+ if [ "$1" = "clear" ]
29
29
  then
30
30
  rm $SSLKEY
31
31
  rm $CACERT
@@ -37,7 +37,7 @@ fi
37
37
 
38
38
  # if there's already a certificate, then exit, but
39
39
  # exit with a success code so build continue
40
- if [ -a $CERTIFICATE ]; then
40
+ if [ -e $CERTIFICATE ]; then
41
41
  exit 0;
42
42
  fi
43
43
 
@@ -891,16 +891,24 @@ exports.config = () => ({
891
891
  trace_observer: {
892
892
  /**
893
893
  * The URI HOST of the observer. Setting this enables infinite tracing.
894
- *
894
+ *
895
895
  * @env NEW_RELIC_INFINITE_TRACING_TRACE_OBSERVER_HOST
896
896
  */
897
897
  host: '',
898
898
  /**
899
899
  * The URI PORT of the observer.
900
- *
900
+ *
901
901
  * @env NEW_RELIC_INFINITE_TRACING_TRACE_OBSERVER_PORT
902
902
  */
903
903
  port: '443'
904
+ },
905
+ span_events: {
906
+ /**
907
+ * The amount of spans to hold onto before dropping them
908
+ *
909
+ * @env NEW_RELIC_INFINITE_TRACING_SPAN_EVENTS_QUEUE_SIZE
910
+ */
911
+ queue_size: 10000
904
912
  }
905
913
  },
906
914
 
package/lib/config/env.js CHANGED
@@ -157,7 +157,8 @@ const ENV_MAPPING = {
157
157
  trace_observer: {
158
158
  host: 'NEW_RELIC_INFINITE_TRACING_TRACE_OBSERVER_HOST',
159
159
  port: 'NEW_RELIC_INFINITE_TRACING_TRACE_OBSERVER_PORT'
160
- }
160
+ },
161
+ queue_size: 'NEW_RELIC_INFINITE_TRACING_QUEUE_SIZE'
161
162
  }
162
163
  }
163
164
 
@@ -245,7 +246,8 @@ const FLOAT_VARS = new Set([
245
246
 
246
247
  const INT_VARS = new Set([
247
248
  'NEW_RELIC_EXPLAIN_THRESHOLD',
248
- 'NEW_RELIC_MAX_SQL_SAMPLES'
249
+ 'NEW_RELIC_MAX_SQL_SAMPLES',
250
+ 'NEW_RELIC_INFINITE_TRACING_SPAN_EVENTS_QUEUE_SIZE'
249
251
  ])
250
252
 
251
253
  exports.ENV_MAPPING = ENV_MAPPING
@@ -1323,6 +1323,10 @@ Config.prototype._canonicalize = function _canonicalize() {
1323
1323
 
1324
1324
  this.serverless_mode.enabled = this.serverless_mode.enabled
1325
1325
  && this.feature_flag.serverless_mode
1326
+
1327
+ if (this.license_key) {
1328
+ this.license_key = this.license_key.trim()
1329
+ }
1326
1330
  }
1327
1331
 
1328
1332
  function _parseCodes(codes) {
@@ -193,6 +193,12 @@ class ErrorCollector {
193
193
  return
194
194
  }
195
195
 
196
+ const shouldCollectErrors = this._shouldCollectErrors()
197
+ if (!shouldCollectErrors) {
198
+ logger.trace('error_collector.enabled is false, dropping application error.')
199
+ return
200
+ }
201
+
196
202
  if (errorHelper.shouldIgnoreError(transaction, error, this.config)) {
197
203
  logger.trace('Ignoring error')
198
204
  return
@@ -223,7 +229,15 @@ class ErrorCollector {
223
229
  * @param {Exception} exception The Exception to be traced.
224
230
  */
225
231
  addUserError(transaction, error, customAttributes) {
226
- if (!error) return
232
+ if (!error) {
233
+ return
234
+ }
235
+
236
+ const shouldCollectErrors = this._shouldCollectErrors()
237
+ if (!shouldCollectErrors) {
238
+ logger.trace('error_collector.enabled is false, dropping user reported error.')
239
+ return
240
+ }
227
241
 
228
242
  const timestamp = Date.now()
229
243
  const exception = new Exception({error, timestamp, customAttributes})
@@ -270,18 +284,6 @@ class ErrorCollector {
270
284
  return false
271
285
  }
272
286
 
273
- // allow enabling & disabling the error tracer at runtime
274
- // TODO: it would be better to check config in the public add() to prevents collecting
275
- // errors on the transaction unnecessarily. Should we allow the work above or
276
- // short-circuit sooner?
277
- if (
278
- !this.config.collect_errors ||
279
- !this.config.error_collector ||
280
- !this.config.error_collector.enabled
281
- ) {
282
- return false
283
- }
284
-
285
287
  if (exception.error) {
286
288
  logger.trace(exception.error, 'Got exception to trace:')
287
289
  }
@@ -304,7 +306,10 @@ class ErrorCollector {
304
306
  }
305
307
  }
306
308
 
307
- this.traceAggregator.add(errorTrace)
309
+ // defaults true in config/index. can be modified server-side
310
+ if (this.config.collect_errors) {
311
+ this.traceAggregator.add(errorTrace)
312
+ }
308
313
 
309
314
  if (this.config.error_collector.capture_events === true) {
310
315
  const priority = transaction && transaction.priority || Math.random()
@@ -328,6 +333,17 @@ class ErrorCollector {
328
333
  this.seenObjectsByTransaction = Object.create(null)
329
334
  }
330
335
 
336
+ _shouldCollectErrors() {
337
+ const errorCollectorEnabled =
338
+ this.config.error_collector && this.config.error_collector.enabled
339
+
340
+ const shouldCaptureTraceOrEvent =
341
+ this.config.collect_errors || // are traces enabled
342
+ (this.config.error_collector && this.config.error_collector.capture_events)
343
+
344
+ return errorCollectorEnabled && shouldCaptureTraceOrEvent
345
+ }
346
+
331
347
  reconfigure(config) {
332
348
  this.config = config
333
349
 
@@ -117,28 +117,30 @@ function wrapEmitWithTransaction(agent, emit, isHTTPS) {
117
117
  transaction.queueTime = Date.now() - queueTimeStamp
118
118
  }
119
119
 
120
+ const synthHeader = request.headers[NEWRELIC_SYNTHETICS_HEADER]
121
+
122
+ if (synthHeader && agent.config.trusted_account_ids && agent.config.encoding_key) {
123
+ handleSyntheticsHeader(
124
+ synthHeader,
125
+ agent.config.encoding_key,
126
+ agent.config.trusted_account_ids,
127
+ transaction
128
+ )
129
+ }
130
+
120
131
  if (agent.config.distributed_tracing.enabled) {
121
132
  // Node http headers are automatically lowercase
122
133
  transaction.acceptDistributedTraceHeaders(transport, request.headers)
123
134
  } else if (agent.config.cross_application_tracer.enabled) {
124
- var encKey = agent.config.encoding_key
125
- var incomingCatId = request.headers[NEWRELIC_ID_HEADER]
126
- var obfTransaction = request.headers[NEWRELIC_TRANSACTION_HEADER]
127
- var synthHeader = request.headers[NEWRELIC_SYNTHETICS_HEADER]
128
- if (encKey) {
129
- cat.handleCatHeaders(incomingCatId, obfTransaction, encKey, transaction)
135
+ const incomingCatId = request.headers[NEWRELIC_ID_HEADER]
136
+ const obfTransaction = request.headers[NEWRELIC_TRANSACTION_HEADER]
137
+
138
+ if (agent.config.encoding_key) {
139
+ cat.handleCatHeaders(incomingCatId, obfTransaction, agent.config.encoding_key, transaction)
130
140
  if (transaction.incomingCatId) {
131
141
  logger.trace('Got inbound request CAT headers in transaction %s',
132
142
  transaction.id)
133
143
  }
134
- if (synthHeader && agent.config.trusted_account_ids) {
135
- handleSyntheticsHeader(
136
- synthHeader,
137
- encKey,
138
- agent.config.trusted_account_ids,
139
- transaction
140
- )
141
- }
142
144
  }
143
145
  }
144
146
 
@@ -4,7 +4,7 @@ var EventEmitter = require('events').EventEmitter
4
4
  var util = require('util')
5
5
  var logger = require('../logger').child({component: 'metric_normalizer'})
6
6
  var deepEqual = require('../util/deep-equal')
7
- var Rule = require('./normalizer/rule')
7
+ var NormalizerRule = require('./normalizer/rule')
8
8
  var NAMES = require('../metrics/names.js')
9
9
 
10
10
 
@@ -82,7 +82,7 @@ MetricNormalizer.prototype.load = function load(json) {
82
82
 
83
83
  json.forEach(function cb_forEach(ruleJSON) {
84
84
  // no need to add the same rule twice
85
- var rule = new Rule(ruleJSON)
85
+ var rule = new NormalizerRule(ruleJSON)
86
86
  if (!this.rules.find(deepEqual.bind(null, rule))) {
87
87
  this.rules.push(rule)
88
88
  logger.trace("Loaded %s normalization rule: %s", this.type, rule)
@@ -142,9 +142,9 @@ MetricNormalizer.prototype.loadFromConfig = function loadFromConfig() {
142
142
  : r.precedence > json.eval_order
143
143
  })
144
144
  if (insert === -1) {
145
- this.rules.push(new Rule(json))
145
+ this.rules.push(new NormalizerRule(json))
146
146
  } else {
147
- this.rules.splice(insert, 0, new Rule(json))
147
+ this.rules.splice(insert, 0, new NormalizerRule(json))
148
148
  }
149
149
  }, this)
150
150
  }
@@ -186,7 +186,7 @@ MetricNormalizer.prototype.addSimple = function addSimple(pattern, name) {
186
186
  json.ignore = true
187
187
  }
188
188
 
189
- this.rules.unshift(new Rule(json))
189
+ this.rules.unshift(new NormalizerRule(json))
190
190
  }
191
191
 
192
192
  /**
@@ -21,7 +21,7 @@
21
21
  "httpMethod",
22
22
  "path",
23
23
  "requestContext",
24
- "resource"
24
+ "requestContext.stage"
25
25
  ]
26
26
  },
27
27
  "cloudFront": {
package/lib/shim/shim.js CHANGED
@@ -989,9 +989,9 @@ function record(nodule, properties, recordNamer) {
989
989
  })
990
990
 
991
991
  // Apply the function, and (if it returned a stream) bind that too.
992
- // If you are looking here Geoff, the reason there is no check for
993
- // `segment` is because it should be guaranteed by the parent and active
994
- // transaction check at the beginning of this function. :)
992
+ // The reason there is no check for `segment` is because it should
993
+ // be guaranteed by the parent and active transaction check
994
+ // at the beginning of this function.
995
995
  var ret = _applyRecorderSegment(segment, this, args, segDesc)
996
996
  if (ret) {
997
997
  if (segDesc.stream) {
@@ -39,7 +39,8 @@ function createStreamingAggregator(config, collector, metrics) {
39
39
  const spanStreamer = new SpanStreamer(
40
40
  config.license_key,
41
41
  connection,
42
- metrics
42
+ metrics,
43
+ config.infinite_tracing.span_events.queue_size
43
44
  )
44
45
 
45
46
  const opts = {
@@ -0,0 +1,13 @@
1
+ 'use strict'
2
+
3
+ class SpanContext {
4
+ constructor(intrinsicAttributes) {
5
+ this.intrinsicAttributes = intrinsicAttributes || Object.create(null)
6
+ }
7
+
8
+ addIntrinsicAttribute(key, value) {
9
+ this.intrinsicAttributes[key] = value
10
+ }
11
+ }
12
+
13
+ module.exports = SpanContext
@@ -91,6 +91,11 @@ class SpanEvent {
91
91
  span = new SpanEvent(attributes, customAttributes)
92
92
  }
93
93
 
94
+ const spanContext = segment.getSpanContext()
95
+ for (const [key, value] of Object.entries(spanContext.intrinsicAttributes)) {
96
+ span.intrinsics[key] = value
97
+ }
98
+
94
99
  const tx = segment.transaction
95
100
 
96
101
  span.intrinsics.traceId = tx.traceId
@@ -1,94 +1,128 @@
1
1
  'use strict'
2
2
 
3
3
  const logger = require('../logger').child({component: 'span-streamer'})
4
+ const NAMES = require('../metrics/names').INFINITE_TRACING
4
5
 
5
- const NAMES = require('../metrics/names')
6
+ const SPAN_DROP_MSG =
7
+ 'Queue full, dropping spans.'
8
+ const SPAN_DROP_INTERVAL = 5
6
9
 
7
- const NO_STREAM_WARNING =
8
- 'Attempting to stream spans before connection created. ' +
9
- 'This warning will not appear again this agent run.'
10
-
11
- const BACK_PRESSURE_WARNING =
12
- 'Back pressure detected in SpanStreamer! Spans will be dropped until the current batch ' +
13
- 'has fully sent. Will not warn again for %s seconds.'
14
- const BACK_PRESSURE_WARNING_INTERVAL = 60 // in seconds
15
-
16
- const BACK_PRESSURE_STOP = 'Back pressure has ended, continuing to stream'
17
10
 
18
11
  class SpanStreamer {
19
- constructor(license_key, connection, metrics) {
12
+ constructor(license_key, connection, metrics, queue_size) {
20
13
  this.stream = null
21
14
  this.license_key = license_key
22
15
  this.connection = connection
16
+ this.queue_size = queue_size
17
+ this.spans = []
23
18
  this._metrics = metrics
24
19
  this._writable = false
25
20
 
26
21
  // 'connected' indicates a safely writeable stream.
27
22
  // May still be mid-connect to gRPC server.
28
23
  this.connection.on('connected', (stream) =>{
24
+ logger.info('Span streamer connected')
29
25
  this.stream = stream
30
26
  this._writable = true
27
+ this.sendQueue()
31
28
  })
32
29
 
33
30
  this.connection.on('disconnected', () =>{
31
+ logger.info('Span streamer disconnected')
34
32
  this.stream = null
35
33
  this._writable = false
36
34
  })
37
35
  }
38
36
 
37
+ /* Accepts a span and either writes it to the stream, queues it to be sent,
38
+ * or drops it depending on stream/queue state
39
+ */
39
40
  write(span) {
40
- if (!this.stream) {
41
- logger.warnOnce(NO_STREAM_WARNING)
42
- return false
43
- }
41
+ this._metrics.getOrCreateMetric(NAMES.SEEN).incrementCallCount()
44
42
 
43
+ // If not writeable (because of backpressure) queue the span
45
44
  if (!this._writable) {
46
- return false
45
+ if (this.spans.length <= this.queue_size) {
46
+ this.spans.push(span)
47
+ return
48
+ }
49
+
50
+ // If the queue is full drop the span
51
+ logger.infoOncePer(
52
+ // key for the OncePer
53
+ 'SPAN_DROP_MSG',
54
+ // interval in ms
55
+ SPAN_DROP_INTERVAL * 1000,
56
+ // message
57
+ SPAN_DROP_MSG + ' Will not warn again for %s seconds.',
58
+ // variables to put in log message
59
+ SPAN_DROP_INTERVAL
60
+ )
61
+
62
+ return
47
63
  }
48
64
 
49
65
  const formattedSpan = span.toStreamingFormat()
50
66
 
51
67
  try {
52
- // false indicates the stream has reached the highWaterMark
53
- // and future writes should be avoided until drained. written items,
54
- // including the one that returned false, will still be buffered.
55
- this._writable = this.stream.write(formattedSpan)
56
-
57
- if (!this._writable) {
58
- logger.infoOncePer(
59
- 'BACK_PRESSURE_START',
60
- BACK_PRESSURE_WARNING_INTERVAL * 1000,
61
- BACK_PRESSURE_WARNING,
62
- BACK_PRESSURE_WARNING_INTERVAL
63
- )
64
-
65
- const waitDrainStart = Date.now()
66
-
67
- this.stream.once('drain', () => {
68
- const drainCompleted = Date.now()
69
- const drainDurationMs = drainCompleted - waitDrainStart
70
-
71
- // Metric can be used to see how frequently completing drains
72
- // as well as average time to drain from when we first notice.
73
- this._metrics.getOrCreateMetric(NAMES.INFINITE_TRACING.DRAIN_DURATION)
74
- .recordValue(drainDurationMs / 1000)
75
-
76
- logger.trace(BACK_PRESSURE_STOP)
77
- this._writable = true
78
- })
79
- }
80
-
81
- // span was added to internal node stream buffer to be sent while draining
82
- // so we return true even when we should not write anymore afterwards
83
- return true
68
+ this.send(formattedSpan)
84
69
  } catch (err) {
85
- logger.trace('Could not stream span.', err)
70
+ logger.error(err)
86
71
  // TODO: something has gone horribly wrong.
87
72
  // We may want to log and turn off this aggregator
88
73
  // to prevent sending further spans. Maybe even "disable" their creation?
89
74
  // or is there a situation where we can recover?
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Sends the span over the stream. Spans are only sent here if the stream is
80
+ * in a writable state. If the stream becomes unwritable after sending the
81
+ * span, a drain event handler is setup to continue writing when possible.
82
+ */
83
+ send(span) {
84
+ // false indicates the stream has reached the highWaterMark
85
+ // and future writes should be avoided until drained. written items,
86
+ // including the one that returned false, will still be buffered.
87
+ this._writable = this.stream.write(span)
88
+ this._metrics.getOrCreateMetric(NAMES.SENT).incrementCallCount()
89
+
90
+ if (!this._writable) {
91
+ const waitDrainStart = Date.now()
92
+ const onDrain = this.drain.bind(this, waitDrainStart)
93
+ this.stream.once('drain', onDrain)
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Drains the span queue that built up when the connection was
99
+ * back-pressured or disconnected. `waitDrainStart` is when the stream
100
+ * initially blocked, used to time how long the stream was blocked. If this
101
+ * is not defined, it is assumed this is being called after a reconnect,
102
+ * and the metric is not used.
103
+ */
104
+ drain(waitDrainStart) {
105
+ // Metric can be used to see how frequently completing drains as well as
106
+ // average time to drain from when we first notice.
107
+ const drainCompleted = Date.now()
108
+ const drainDurationMs = drainCompleted - waitDrainStart
109
+ this._metrics.getOrCreateMetric(NAMES.DRAIN_DURATION)
110
+ .recordValue(drainDurationMs / 1000)
111
+
112
+ // Once the 'drain' event fires we can begin writing to the stream again
113
+ this._writable = true
114
+
115
+ this.sendQueue()
116
+ }
90
117
 
91
- return false
118
+ sendQueue() {
119
+ // Continue sending the spans that were in the queue. _writable is checked
120
+ // so that if a send fails while clearing the queue, this drain handler can
121
+ // finish, and the drain handler setup on the failed send will then attempt
122
+ // to clear the queue
123
+ while (this.spans.length > 0 && this._writable) {
124
+ const nextObject = this.spans.shift()
125
+ this.send(nextObject)
92
126
  }
93
127
  }
94
128
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  const Aggregator = require('../aggregators/base-aggregator')
4
4
  const StreamingSpanEvent = require('./streaming-span-event')
5
- const NAMES = require('../metrics/names')
6
5
  const logger = require('../logger').child({component: 'streaming-span-event-aggregator'})
7
6
 
8
7
  const SEND_WARNING =
@@ -20,7 +19,6 @@ class StreamingSpanEventAggregator extends Aggregator {
20
19
 
21
20
  super(opts, collector)
22
21
 
23
- this.metricNames = opts.metricNames || NAMES.INFINITE_TRACING
24
22
  this.stream = opts.span_streamer
25
23
  this.metrics = metrics
26
24
  this.started = false
@@ -82,16 +80,8 @@ class StreamingSpanEventAggregator extends Aggregator {
82
80
  return
83
81
  }
84
82
 
85
- // SEEN/SENT are to understand where we've had to drop spans due back-pressure, errors,
86
- // reconnects, etc. so moving after start check to avoid logging for a
87
- // currently unsupported case.
88
- this.metrics.getOrCreateMetric(this.metricNames.SEEN).incrementCallCount()
89
-
90
83
  const span = StreamingSpanEvent.fromSegment(segment, parentId, isRoot)
91
-
92
- if (this.stream.write(span)) {
93
- this.metrics.getOrCreateMetric(this.metricNames.SENT).incrementCallCount()
94
- }
84
+ this.stream.write(span)
95
85
  }
96
86
  }
97
87
 
@@ -112,6 +112,11 @@ class StreamingSpanEvent {
112
112
  span = new StreamingSpanEvent(traceId, agentAttributes, customAttributes)
113
113
  }
114
114
 
115
+ const spanContext = segment.getSpanContext()
116
+ for (const [key, value] of Object.entries(spanContext.intrinsicAttributes)) {
117
+ span.addIntrinsicAttribute(key, value)
118
+ }
119
+
115
120
  span.addIntrinsicAttribute('guid', segment.id)
116
121
  span.addIntrinsicAttribute('parentId', parentId)
117
122
  span.addIntrinsicAttribute('transactionId', transaction.id)
@@ -381,8 +381,10 @@ Transaction.prototype.isIgnored = function getIgnore() {
381
381
  */
382
382
  Transaction.prototype.finalizeNameFromUri = finalizeNameFromUri
383
383
  function finalizeNameFromUri(requestURL, statusCode) {
384
- logger.trace({requestURL: requestURL, statusCode: statusCode, transactionId: this.id,
385
- transactionName: this.name}, 'Setting transaction name')
384
+ if (logger.traceEnabled()) {
385
+ logger.trace({requestURL: requestURL, statusCode: statusCode, transactionId: this.id,
386
+ transactionName: this.name}, 'Setting transaction name')
387
+ }
386
388
 
387
389
  this.url = urltils.scrub(requestURL)
388
390
  this.statusCode = statusCode
@@ -426,11 +428,26 @@ function finalizeNameFromUri(requestURL, statusCode) {
426
428
  }
427
429
 
428
430
  this.baseSegment && this._markAsWeb(requestURL)
429
- logger.trace({
430
- transactionId: this.id,
431
- transactionName: this.name,
432
- ignore: this.ignore
433
- }, 'Finished setting transaction name from Uri')
431
+
432
+ this._copyNameToActiveSpan(this.name)
433
+
434
+ if (logger.traceEnabled()) {
435
+ logger.trace({
436
+ transactionId: this.id,
437
+ transactionName: this.name,
438
+ ignore: this.ignore
439
+ }, 'Finished setting transaction name from Uri')
440
+ }
441
+ }
442
+
443
+ Transaction.prototype._copyNameToActiveSpan = function _copyNameToActiveSpan(name) {
444
+ const spanContext = this.agent.tracer.getSpanContext()
445
+ if (!spanContext) {
446
+ logger.trace('Span context not available, not adding transaction.name attribute for %s', name)
447
+ return
448
+ }
449
+
450
+ spanContext.addIntrinsicAttribute('transaction.name', name)
434
451
  }
435
452
 
436
453
  /**
@@ -490,11 +507,15 @@ Transaction.prototype.finalizeName = function finalizeName(name) {
490
507
 
491
508
  this.baseSegment && this.baseSegment.setNameFromTransaction()
492
509
 
493
- logger.trace({
494
- transactionId: this.id,
495
- transactionName: this.name,
496
- ignore: this.ignore
497
- }, 'Finished setting transaction name from string')
510
+ this._copyNameToActiveSpan(this.name)
511
+
512
+ if (logger.traceEnabled()) {
513
+ logger.trace({
514
+ transactionId: this.id,
515
+ transactionName: this.name,
516
+ ignore: this.ignore
517
+ }, 'Finished setting transaction name from string')
518
+ }
498
519
  }
499
520
 
500
521
  /**
@@ -692,11 +713,21 @@ function _linkExceptionToSegment(exception) {
692
713
  return
693
714
  }
694
715
 
716
+ const config = this.agent.config
717
+
695
718
  // Add error attributes to the span
696
- const details = exception.getErrorDetails(this.agent.config)
719
+ const details = exception.getErrorDetails(config)
697
720
  segment.addAttribute('error.message', details.message)
698
721
  segment.addAttribute('error.class', details.type)
699
722
 
723
+ const isExpected =
724
+ errorHelper.isExpectedErrorClass(config, details.type) ||
725
+ errorHelper.isExpectedErrorMessage(config, details.type, details.message)
726
+
727
+ if (isExpected) {
728
+ segment.addAttribute('error.expected', isExpected)
729
+ }
730
+
700
731
  // Add the span/segment ID to the exception as agent attributes
701
732
  exception.agentAttributes.spanId = segment.id
702
733
  }
@@ -8,6 +8,7 @@ const hashes = require('../../util/hashes')
8
8
 
9
9
  const {Attributes, MAXIMUM_CUSTOM_ATTRIBUTES} = require('../../attributes')
10
10
  const ExclusiveCalculator = require('./exclusive-time-calculator')
11
+ const SpanContext = require('../../spans/span-context')
11
12
 
12
13
  const NAMES = require('../../metrics/names')
13
14
  const INSTANCE_UNKNOWN = 'unknown'
@@ -77,6 +78,17 @@ function TraceSegment(transaction, name, recorder) {
77
78
  this.probe('new TraceSegment')
78
79
  }
79
80
 
81
+ TraceSegment.prototype.getSpanContext = function getSpanContext() {
82
+ const config = this.transaction.agent.config
83
+ const spansEnabled = config.distributed_tracing.enabled && config.span_events.enabled
84
+
85
+ if (!this._spanContext && spansEnabled) {
86
+ this._spanContext = new SpanContext()
87
+ }
88
+
89
+ return this._spanContext
90
+ }
91
+
80
92
  TraceSegment.prototype.addAttribute =
81
93
  function addAttribute(key, value, truncateExempt = false) {
82
94
  this.attributes.addAttribute(
@@ -91,7 +103,7 @@ TraceSegment.prototype.getAttributes = function getAttributes() {
91
103
  return this.attributes.get(DESTINATIONS.TRANS_SEGMENT)
92
104
  }
93
105
 
94
- TraceSegment.prototype.addCustomSpanAttribute =
106
+ TraceSegment.prototype.addCustomSpanAttribute =
95
107
  function addCustomSpanAttribute(key, value, truncateExempt = false) {
96
108
  this.customAttributes.addAttribute(
97
109
  DESTINATIONS.SPAN_EVENT,
@@ -21,6 +21,7 @@ function Tracer(agent) {
21
21
 
22
22
  Tracer.prototype.getTransaction = getTransaction
23
23
  Tracer.prototype.getSegment = getSegment
24
+ Tracer.prototype.getSpanContext = getSpanContext
24
25
  Tracer.prototype.createSegment = createSegment
25
26
  Tracer.prototype.addSegment = addSegment
26
27
  Tracer.prototype.transactionProxy = transactionProxy
@@ -61,6 +62,10 @@ function getSegment() {
61
62
  return this.segment
62
63
  }
63
64
 
65
+ function getSpanContext() {
66
+ return this.segment && this.segment.getSpanContext()
67
+ }
68
+
64
69
  function createSegment(name, recorder, _parent) {
65
70
  var parent = _parent || this.segment
66
71
  if (!parent || !parent.transaction.isActive()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "6.6.0",
3
+ "version": "6.9.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "contributors": [
@@ -83,6 +83,21 @@
83
83
  "name": "Michael Goin",
84
84
  "email": "mgoin@newrelic.com",
85
85
  "web": "https://newrelic.com"
86
+ },
87
+ {
88
+ "name": "Alan Storm",
89
+ "email": "astorm@newrelic.com",
90
+ "web": "https://newrelic.com"
91
+ },
92
+ {
93
+ "name": "Carlo Pearson",
94
+ "email": "cpearson@newrelic.com",
95
+ "web": "https://newrelic.com"
96
+ },
97
+ {
98
+ "name": "Nick Tzaperas",
99
+ "email": "ntzaperas@newrelic.com",
100
+ "web": "https://newrelic.com"
86
101
  }
87
102
  ],
88
103
  "description": "New Relic agent",
@@ -108,7 +123,7 @@
108
123
  "clean": "./bin/clean.sh",
109
124
  "docker-env": "./bin/docker-env-vars.sh",
110
125
  "docs": "npm ci && jsdoc -c ./jsdoc-conf.json --private -r .",
111
- "integration": "npm run prepare-test && npm run sub-install && time tap --no-esm test/integration/**/**/*.tap.js --timeout=120 --no-coverage",
126
+ "integration": "npm run prepare-test && npm run sub-install && time tap --no-esm test/integration/**/**/*.tap.js --timeout=180 --no-coverage",
112
127
  "prepare-test": "npm ci && npm run ca-gen && npm run ssl && npm run docker-env",
113
128
  "lint": "eslint ./*.js lib test",
114
129
  "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/",
@@ -118,7 +133,7 @@
118
133
  "ssl": "./bin/ssl.sh",
119
134
  "sub-install": "node test/bin/install_sub_deps",
120
135
  "test": "npm run integration && npm run unit",
121
- "unit": "rm -f newrelic_agent.log && time tap --test-regex='(\\/|^test\\/unit\\/.*\\.test\\.js)$' --timeout=120 --no-coverage",
136
+ "unit": "rm -f newrelic_agent.log && time tap --test-regex='(\\/|^test\\/unit\\/.*\\.test\\.js)$' --timeout=180 --no-coverage",
122
137
  "update-cross-agent-tests": "./bin/update-cats.sh",
123
138
  "versioned-tests": "./bin/run-versioned-tests.sh",
124
139
  "versioned": "npm run prepare-test && time ./bin/run-versioned-tests.sh"
@@ -127,27 +142,27 @@
127
142
  "newrelic-naming-rules": "./bin/test-naming-rules.js"
128
143
  },
129
144
  "dependencies": {
130
- "@grpc/grpc-js": "^0.6.15",
131
- "@grpc/proto-loader": "^0.5.3",
145
+ "@grpc/grpc-js": "1.0.3",
146
+ "@grpc/proto-loader": "^0.5.4",
132
147
  "@newrelic/aws-sdk": "^1.1.1",
133
148
  "@newrelic/koa": "^3.0.0",
134
149
  "@newrelic/superagent": "^2.0.1",
135
150
  "@tyriar/fibonacci-heap": "^2.0.7",
136
151
  "async": "^2.1.4",
137
152
  "concat-stream": "^2.0.0",
138
- "escodegen": "^1.11.1",
153
+ "escodegen": "^1.14.1",
139
154
  "esprima": "^4.0.1",
140
155
  "https-proxy-agent": "^4.0.0",
141
156
  "json-stringify-safe": "^5.0.0",
142
- "readable-stream": "^3.1.1",
157
+ "readable-stream": "^3.6.0",
143
158
  "semver": "^5.3.0"
144
159
  },
145
160
  "optionalDependencies": {
146
- "@newrelic/native-metrics": "^5.0.0"
161
+ "@newrelic/native-metrics": "^5.1.0"
147
162
  },
148
163
  "devDependencies": {
149
- "@newrelic/proxy": "^1.0.0",
150
- "@newrelic/test-utilities": "^3.1.0",
164
+ "@newrelic/proxy": "^2.0.0",
165
+ "@newrelic/test-utilities": "^3.3.0",
151
166
  "JSV": "~4.0.2",
152
167
  "architect": "*",
153
168
  "benchmark": "^2.1.4",
@@ -173,7 +188,7 @@
173
188
  "rimraf": "^2.6.3",
174
189
  "should": "*",
175
190
  "sinon": "^4.5.0",
176
- "tap": "^14.10.6",
191
+ "tap": "^14.10.7",
177
192
  "temp": "^0.8.1",
178
193
  "through": "^2.3.6",
179
194
  "when": "*"
package/.eslintignore DELETED
@@ -1 +0,0 @@
1
- **/node_modules/**
package/.eslintrc.js DELETED
@@ -1,67 +0,0 @@
1
- module.exports = {
2
- "env": {
3
- "es6": true,
4
- "node": true,
5
- "browser": false
6
- },
7
- "parserOptions": {
8
- "ecmaVersion": 6
9
- },
10
- "ignorePatterns": ["invalid-json/"],
11
- "rules": {
12
- "indent": ["warn", 2, {"SwitchCase": 1}],
13
- "brace-style": "error",
14
- "comma-dangle": "off",
15
- "comma-style": ["error", "last"],
16
- "consistent-return": "off",
17
- "curly": "off",
18
- "eol-last": "error",
19
- "eqeqeq": ["error", "smart"],
20
- "camelcase": ["off", {"properties": "never"}],
21
- "dot-notation": "error",
22
- "func-names": "error",
23
- "guard-for-in": "error",
24
- "key-spacing": ["off", { "beforeColon": false }],
25
- "max-len": ["error", 100, { "ignoreUrls": true }],
26
- "max-nested-callbacks": ["error", 3],
27
- "max-params": ["error", 5],
28
- "new-cap": "error",
29
- "no-console": "warn",
30
- "no-debugger": "error",
31
- "no-else-return": "error",
32
- "no-floating-decimal": "error",
33
- "no-lonely-if": "error",
34
- "no-mixed-requires": "error",
35
- "no-multiple-empty-lines": "error",
36
- "no-multi-spaces": ["off", { "ignoreEOLComments": true }],
37
- "no-new": "error",
38
- "no-new-func": "warn",
39
- "no-shadow": ["warn", {"allow": ["shim"]}],
40
- "no-undef": "error",
41
- "no-unused-vars": "error",
42
- "no-use-before-define": ["off", {"functions": false}],
43
- "one-var": ["off", "never"],
44
- "padded-blocks": ["error", "never"],
45
- "radix": "error",
46
- "semi": ["error", "never"],
47
- "space-before-function-paren": ["error", "never"],
48
- "keyword-spacing": "error",
49
- "space-before-blocks": "error",
50
- "space-infix-ops": "error",
51
- "spaced-comment": "error",
52
- "space-unary-ops": "error",
53
- "strict": "error",
54
- "quote-props": [ "off", "consistent-as-needed" ],
55
- "quotes": ["off", "single"],
56
- "use-isnan": "error",
57
- "wrap-iife": "error"
58
- },
59
- "overrides": [
60
- {
61
- "files": ["test/integration/*.tap.js", "test/integration/*/*.tap.js", "test/integration/core/exec-me.js"],
62
- "rules": {
63
- "no-console": ["off"]
64
- }
65
- }
66
- ]
67
- }
@@ -1,5 +0,0 @@
1
- ## CHANGE LOG
2
-
3
- ## INTERNAL LINKS
4
-
5
- ## NOTES
package/.travis.yml DELETED
@@ -1,40 +0,0 @@
1
- language: generic
2
- env:
3
- global:
4
- - MONGODB=2.6.11
5
- matrix:
6
- # NODE 8
7
- - NR_NODE_VERSION=8 SUITE=smoke
8
- - NR_NODE_VERSION=8 SUITE=unit
9
- - NR_NODE_VERSION=8 SUITE=integration
10
- - NR_NODE_VERSION=8 SUITE=versioned
11
- # NODE 10
12
- - NR_NODE_VERSION=10 SUITE=smoke
13
- - NR_NODE_VERSION=10 SUITE=unit
14
- - NR_NODE_VERSION=10 SUITE=integration
15
- - NR_NODE_VERSION=10 SUITE=versioned
16
- # NODE 12 --
17
- - NR_NODE_VERSION=12 SUITE=smoke
18
- - NR_NODE_VERSION=12 SUITE=unit
19
- - NR_NODE_VERSION=12 SUITE=integration
20
- - NR_NODE_VERSION=12 SUITE=lint
21
- - NR_NODE_VERSION=12 SUITE=versioned
22
- services:
23
- - memcached
24
- - mysql
25
- - redis
26
- - postgresql
27
- - rabbitmq
28
- addons:
29
- apt:
30
- packages:
31
- - rabbitmq-server
32
- before_install:
33
- - source ./bin/travis-node.sh
34
- - ./bin/travis-setup.sh
35
- install: if [ "$SUITE" != "smoke" ]; then npm ci; fi
36
- cache:
37
- directories:
38
- - "$HOME/.npm"
39
- script: npm run $SUITE
40
- after_script: find /home/travis/.npm/_logs -type f -exec cat '{}' +