newrelic 5.13.0 → 5.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -469,7 +469,8 @@ CollectorAPI.prototype.restart = function restart(callback) {
469
469
  CollectorAPI.prototype._runLifecycle = function _runLifecycle(method, body, callback) {
470
470
  if (!this.isConnected()) {
471
471
  logger.warn('Not connected to New Relic. Not calling.', method.name)
472
- return callback(new Error('Not connected to collector.', null, null))
472
+ const error = new Error('Not connected to collector.', null, null)
473
+ return setImmediate(callback, error)
473
474
  }
474
475
 
475
476
  const api = this
@@ -179,6 +179,13 @@ exports.config = () => ({
179
179
  filter_cache_limit: 1000
180
180
  },
181
181
 
182
+ diagnostics: {
183
+ code_injector: {
184
+ enabled: false,
185
+ internal_file_pattern: /nodejs_agent\/(?:(?!test))|\/node_modules\/(?:@)?newrelic/
186
+ }
187
+ },
188
+
182
189
  logging: {
183
190
  /**
184
191
  * Verbosity of the module's logging. This module uses bunyan
package/lib/config/env.js CHANGED
@@ -51,6 +51,11 @@ const ENV_MAPPING = {
51
51
  include: 'NEW_RELIC_TRANSACTION_EVENTS_ATTRIBUTES_INCLUDE'
52
52
  }
53
53
  },
54
+ diagnostics: {
55
+ code_injector: {
56
+ enabled: 'NEW_RELIC_DIAGNOSTICS_CODE_INJECTOR_ENABLED'
57
+ }
58
+ },
54
59
  transaction_tracer: {
55
60
  attributes: {
56
61
  enabled: 'NEW_RELIC_TRANSACTION_TRACER_ATTRIBUTES_ENABLED',
@@ -186,6 +191,7 @@ const BOOLEAN_VARS = new Set([
186
191
  'NEW_RELIC_TRACER_ENABLED',
187
192
  'NEW_RELIC_TRANSACTION_TRACER_ATTRIBUTES_ENABLED',
188
193
  'NEW_RELIC_TRANSACTION_EVENTS_ATTRIBUTES_ENABLED',
194
+ 'NEW_RELIC_DIAGNOSTICS_CODE_INJECTOR_ENABLED',
189
195
  'NEW_RELIC_TRANSACTION_SEGMENTS_ATTRIBUTES_ENABLED',
190
196
  'NEW_RELIC_DEBUG_METRICS',
191
197
  'NEW_RELIC_DEBUG_TRACER',
@@ -135,9 +135,6 @@ function Config(config) {
135
135
  this.sampling_target_period_in_seconds = 60
136
136
  this.max_payload_size_in_bytes = DEFAULT_MAX_PAYLOAD_SIZE_IN_BYTES
137
137
 
138
- // how frequently harvester runs
139
- this.data_report_period = 60
140
-
141
138
  // this value is arbitrary
142
139
  this.max_trace_segments = 900
143
140
 
@@ -338,7 +335,6 @@ Config.prototype._fromServer = function _fromServer(params, key) {
338
335
 
339
336
  case 'apdex_t':
340
337
  case 'web_transactions_apdex':
341
- case 'data_report_period':
342
338
  this._updateIfChanged(params, key)
343
339
  break
344
340
  case 'event_harvest_config':
@@ -1029,7 +1025,7 @@ Config.prototype._fromPassed = function _fromPassed(external, internal, arbitrar
1029
1025
  return
1030
1026
  }
1031
1027
 
1032
- if (typeof node === 'object' && !Array.isArray(node)) {
1028
+ if (typeof node === 'object' && !(node instanceof RegExp) && !Array.isArray(node)) {
1033
1029
  // is top level and can have arbitrary keys
1034
1030
  var allowArbitrary = internal === this || HAS_ARBITRARY_KEYS.has(key)
1035
1031
  this._fromPassed(node, internal[key], allowArbitrary)
@@ -1207,6 +1203,10 @@ Config.prototype._enforceServerless = function _enforceServerless(inputConfig) {
1207
1203
  }
1208
1204
  // default trusted_account_key to account_id
1209
1205
  this.trusted_account_key = this.trusted_account_key || this.account_id
1206
+
1207
+ // Not required in serverless mode but must default to Unknown to function.
1208
+ this.primary_application_id = this.primary_application_id || 'Unknown'
1209
+
1210
1210
  return
1211
1211
  }
1212
1212
  const DT_KEYS = ['account_id', 'primary_application_id', 'trusted_account_key']
@@ -0,0 +1,161 @@
1
+ 'use strict'
2
+
3
+ // to avoid parsing the esprima code base we require it here
4
+ // TODO: excise this with better internal file filtering
5
+ const esprima = require('esprima')
6
+ const ecg = require('escodegen')
7
+ const fileNameToken = {
8
+ type: 'Literal',
9
+ value: null
10
+ }
11
+ const lineNumberTokenTemplate = {
12
+ type: 'Literal',
13
+ value: null
14
+ }
15
+
16
+ class Mutator {
17
+ constructor(appliesTo, mutate) {
18
+ this.appliesTo = appliesTo
19
+ this.mutate = mutate
20
+ this.tokens = []
21
+ }
22
+
23
+ add(token) {
24
+ this.tokens.push(token)
25
+ }
26
+
27
+ apply() {
28
+ this.tokens.forEach(this.mutate)
29
+ }
30
+ }
31
+
32
+ const functionTypes = new Set([
33
+ 'ArrowFunctionExpression',
34
+ 'FunctionExpression',
35
+ 'AsyncFunction'
36
+ ])
37
+
38
+ const equalityCheckOps = new Set([
39
+ '==',
40
+ '===',
41
+ '!=',
42
+ '!==',
43
+ 'instanceof'
44
+ ])
45
+
46
+ const variableTypes = new Set([
47
+ 'MemberExpression',
48
+ 'Identifier'
49
+ ])
50
+
51
+ const callTypes = new Set([
52
+ 'CallExpression',
53
+ 'NewExpression'
54
+ ])
55
+
56
+ const wrapTemplate = esprima.parse('__NR_wrap()').body[0].expression
57
+ const unwrapTemplate = esprima.parse('__NR_unwrap()').body[0].expression
58
+
59
+ const mutators = [
60
+ new Mutator(function wrapAssignmentPredicate(token) {
61
+ return token.type === 'AssignmentExpression' &&
62
+ token.operator === '=' &&
63
+ functionTypes.has(token.right.type)
64
+ }, function injectWrapAssignment(token) {
65
+ token.right = wrapToken(token.right)
66
+ }),
67
+ new Mutator(function wrapArgPredicate(token) {
68
+ return callTypes.has(token.type)
69
+ }, function injectWrapArg(token) {
70
+ token.arguments = token.arguments.map(wrapToken)
71
+ }),
72
+ new Mutator(function unwrapPredicate(token) {
73
+ return token.type === 'BinaryExpression' && equalityCheckOps.has(token.operator)
74
+ }, function injectUnwrap(token) {
75
+ token.left = unwrapToken(token.left)
76
+ token.right = unwrapToken(token.right)
77
+ })
78
+ ]
79
+
80
+ function wrapToken(argToken) {
81
+ const type = argToken.type
82
+ if (
83
+ !functionTypes.has(type) &&
84
+ !callTypes.has(type) &&
85
+ !variableTypes.has(type) ||
86
+ !argToken.loc
87
+ ) {
88
+ return argToken
89
+ }
90
+
91
+ const wrapped = Object.assign({}, wrapTemplate)
92
+ const lineNumberToken = Object.assign({}, lineNumberTokenTemplate)
93
+ lineNumberToken.value = argToken.loc.start.line
94
+ wrapped.arguments = [argToken, lineNumberToken, Object.assign({}, fileNameToken)]
95
+ return wrapped
96
+ }
97
+
98
+ function unwrapToken(argToken) {
99
+ if (!variableTypes.has(argToken.type)) {
100
+ return argToken
101
+ }
102
+
103
+ const wrapped = Object.assign({}, unwrapTemplate)
104
+ wrapped.arguments = [argToken]
105
+ return wrapped
106
+ }
107
+
108
+ function inject(sourceCode, file) {
109
+ // wrap the incoming file code to make it more palatable for esprima.
110
+ // node likewise wraps the contents of the file in a function, so this
111
+ // replicates the behavior (e.g. allows for global returns)
112
+ sourceCode = 'function main() {' + sourceCode + '}'
113
+ const sourceRootBody = esprima.parse(sourceCode, {loc: true}).body[0].body.body
114
+
115
+ const toRelax = [].concat(sourceRootBody)
116
+
117
+ while (toRelax.length) {
118
+ const currentToken = toRelax.pop()
119
+
120
+ mutators.forEach(m => {
121
+ if (m.appliesTo(currentToken)) {
122
+ m.add(currentToken)
123
+ }
124
+ })
125
+
126
+ for (let key of Object.keys(currentToken)) {
127
+ if (key === 'loc') continue
128
+ const value = currentToken[key]
129
+ if (value && value instanceof Object) {
130
+ if (Array.isArray(value)) {
131
+ for (let t of value) {
132
+ if (t) {
133
+ toRelax.push(t)
134
+ }
135
+ }
136
+ } else {
137
+ toRelax.push(value)
138
+ }
139
+ }
140
+ }
141
+ }
142
+
143
+ // TODO: make this less janky
144
+ fileNameToken.value = file
145
+ mutators.forEach(m => m.apply())
146
+ fileNameToken.value = null
147
+ // create a new base level token that contains all the statements we
148
+ // want to pass back to node
149
+ const printed = ecg.generate({
150
+ type: 'Program',
151
+ body: sourceRootBody,
152
+ sourceType: 'script'
153
+ }, {
154
+ format: {
155
+ semicolons: false
156
+ }
157
+ })
158
+ return printed
159
+ }
160
+
161
+ module.exports = { inject }
package/lib/shimmer.js CHANGED
@@ -6,6 +6,7 @@ var logger = require('./logger').child({component: 'shimmer'})
6
6
  var INSTRUMENTATIONS = require('./instrumentations')()
7
7
  var properties = require('./util/properties')
8
8
  var shims = require('./shim')
9
+ const { inject } = require('./injector')
9
10
 
10
11
  var MODULE_TYPE = shims.constants.MODULE_TYPE
11
12
 
@@ -342,6 +343,121 @@ var shimmer = module.exports = {
342
343
  var Module = require('module')
343
344
  var filepathMap = {}
344
345
 
346
+ // TODO: how do we unpatch this?
347
+ const diagConf = agent.config.diagnostics.code_injector
348
+ if (diagConf.enabled) {
349
+ // XXX: this is required to handle unwrapping event listener removal
350
+ const EEProto = require('events').EventEmitter.prototype
351
+ shimmer.wrapMethod(
352
+ EEProto,
353
+ 'EventEmitter.prototype',
354
+ ['addListener', 'on', 'once', 'off', 'prependListener', 'prependOnceListener'],
355
+ function wrapAddListener(addListener) {
356
+ return function wrappedAddListener(ev, listener) {
357
+ return addListener.call(this, ev, __NR_unwrap(listener))
358
+ }
359
+ }
360
+ )
361
+ shimmer.wrapMethod(
362
+ EEProto,
363
+ 'EventEmitter.prototype',
364
+ 'removeListener',
365
+ function wrapRemoveListener(removeListener) {
366
+ return function wrappedRemoveListener(ev, listener) {
367
+ return removeListener.call(this, ev, __NR_unwrap(listener))
368
+ }
369
+ }
370
+ )
371
+ const internalCodePattern = diagConf.internal_file_pattern
372
+ const proto = Module.prototype
373
+ shimmer.wrapMethod(
374
+ proto,
375
+ 'Module.prototype',
376
+ '_compile',
377
+ function wrapCompile(_compile) {
378
+ return function wrappedCompile(code, file) {
379
+ let injected
380
+ try {
381
+ injected = internalCodePattern.test(file) ? code : inject(code, file)
382
+ return _compile.call(this, injected, file)
383
+ } catch (e) {
384
+ logger.debug('Unable to parse file:', file, e)
385
+ return _compile.call(this, code, file)
386
+ }
387
+ }
388
+ }
389
+ )
390
+
391
+ // TODO: use shimmer methods
392
+ global.tracer = agent.tracer
393
+ global.__NR_wrap = __NR_wrap
394
+ function __NR_wrap(f, lineNum, fileName) {
395
+ const scheduledSegment = agent.tracer.getSegment()
396
+ if (
397
+ typeof f !== 'function' ||
398
+ !scheduledSegment ||
399
+ !scheduledSegment.transaction.isActive()
400
+ ) {
401
+ return f
402
+ }
403
+
404
+
405
+ const scheduledTransaction = scheduledSegment.transaction
406
+ return new Proxy(f, {
407
+ get: function getTrap(target, prop) {
408
+ // Allow for look up of the target
409
+ if (prop === '__NR_new_original') {
410
+ return target
411
+ }
412
+ return target[prop]
413
+ },
414
+ construct: function constructTrap(Target, proxyArgs) {
415
+ const currentSegment = agent.tracer.getSegment()
416
+ if (
417
+ scheduledTransaction.isActive() &&
418
+ (
419
+ !currentSegment ||
420
+ currentSegment.transaction.id !== scheduledTransaction.id
421
+ )
422
+ ) {
423
+ logger.info(
424
+ `lost state in ${fileName} (line ${lineNum});`,
425
+ `expected to be in transaction ${scheduledTransaction.id},`,
426
+ `instead landed in ${currentSegment && currentSegment.transaction.id}`
427
+ )
428
+ }
429
+ return new Target(...proxyArgs)
430
+ },
431
+ apply: function wrappedApply(target, thisArg, args) {
432
+ const currentSegment = agent.tracer.getSegment()
433
+ if (
434
+ scheduledTransaction.isActive() &&
435
+ (
436
+ !currentSegment ||
437
+ currentSegment.transaction.id !== scheduledTransaction.id
438
+ )
439
+ ) {
440
+ logger.info(
441
+ `lost state in ${fileName} (line ${lineNum});`,
442
+ `expected to be in transaction ${scheduledTransaction.id},`,
443
+ `instead landed in ${currentSegment && currentSegment.transaction.id}`
444
+ )
445
+ }
446
+ return target.apply(thisArg, args)
447
+ }
448
+ })
449
+ }
450
+
451
+ global.__NR_unwrap = __NR_unwrap
452
+ function __NR_unwrap(f) {
453
+ if (typeof f !== 'function' || !f || !f.__NR_new_original) {
454
+ return f
455
+ }
456
+
457
+ return f.__NR_new_original
458
+ }
459
+ }
460
+
345
461
  shimmer.wrapMethod(Module, 'Module', '_resolveFilename', function wrapRes(resolve) {
346
462
  return function wrappedResolveFilename(file) {
347
463
  // This is triggered by the load call, so record the path that has been seen so
@@ -40,7 +40,7 @@ class SpanEventAggregator extends EventAggregator {
40
40
  spanLogger.trace({
41
41
  spansCollected: this.length,
42
42
  spansSeen: this.seen
43
- }, 'Entity stats on harvest')
43
+ }, 'Entity stats on span harvest')
44
44
  }
45
45
  super.send()
46
46
  }
@@ -72,7 +72,6 @@ function Transaction(agent) {
72
72
  )
73
73
 
74
74
  ++agent.activeTransactions
75
- ++agent.transactionCreatedInHarvest
76
75
 
77
76
  this.numSegments = 0
78
77
  this.id = hashes.makeId()
@@ -66,7 +66,8 @@ exports.request = function request(opts, agent, cb) {
66
66
  function respond(data) {
67
67
  agent.removeListener('errored', abortRequest)
68
68
  agent.removeListener('stopped', abortRequest)
69
-
69
+ agent.removeListener('disconnected', abortRequest)
70
+
70
71
  if (res.statusCode !== 200) {
71
72
  logger.debug(
72
73
  'Got %d %s from metadata request %j',
@@ -83,22 +84,28 @@ exports.request = function request(opts, agent, cb) {
83
84
  })
84
85
 
85
86
  req.setTimeout(1000, function requestTimeout() {
86
- logger.debug('Request for metadata %j timed out', opts)
87
- cb(new Error('Timeout'))
87
+ req.abort()
88
88
  })
89
+
89
90
  req.on('error', function requestError(err) {
91
+ if (err.code === 'ECONNRESET') {
92
+ logger.debug('Request for metadata %j timed out', opts)
93
+ return cb(err)
94
+ }
95
+
90
96
  logger.debug('Message for metadata %j: %s', opts, err.message)
91
97
  cb(err)
92
98
  })
93
-
94
99
  agent.once('errored', abortRequest)
95
100
  agent.once('stopped', abortRequest)
101
+ agent.once('disconnected', abortRequest)
96
102
 
97
103
  function abortRequest() {
98
104
  logger.debug('Aborting request for metadata at %j', opts)
99
105
  req.abort()
100
106
  agent.removeListener('errored', abortRequest)
101
107
  agent.removeListener('stopped', abortRequest)
108
+ agent.removeListener('disconnected', abortRequest)
102
109
  }
103
110
  }
104
111
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "5.13.0",
3
+ "version": "5.13.1",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "contributors": [
@@ -108,7 +108,7 @@
108
108
  "clean": "./bin/clean.sh",
109
109
  "docker-env": "./bin/docker-env-vars.sh",
110
110
  "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 test/integration/*/*.tap.js test/integration/*/*/*.tap.js --timeout=120",
111
+ "integration": "npm run prepare-test && npm run sub-install && time tap --no-esm test/integration/**/**/*.tap.js --timeout=120 --no-coverage",
112
112
  "prepare-test": "npm ci && npm run ca-gen && npm run ssl && npm run docker-env",
113
113
  "lint": "eslint ./*.js lib test",
114
114
  "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/",
@@ -119,7 +119,7 @@
119
119
  "ssl": "./bin/ssl.sh",
120
120
  "sub-install": "node test/bin/install_sub_deps",
121
121
  "test": "npm run integration && npm run unit",
122
- "unit": "rm -f newrelic_agent.log && mocha -r nock -c test/unit --recursive",
122
+ "unit": "rm -f newrelic_agent.log && mocha -r nock -c test/unit --recursive --exit",
123
123
  "update-cross-agent-tests": "./bin/update-cats.sh",
124
124
  "versioned-tests": "./bin/run-versioned-tests.sh",
125
125
  "versioned": "npm run prepare-test && time ./bin/run-versioned-tests.sh"
@@ -133,7 +133,9 @@
133
133
  "@tyriar/fibonacci-heap": "^2.0.7",
134
134
  "async": "^2.1.4",
135
135
  "concat-stream": "^2.0.0",
136
- "https-proxy-agent": "^2.2.1",
136
+ "escodegen": "^1.11.1",
137
+ "esprima": "^4.0.1",
138
+ "https-proxy-agent": "^3.0.0",
137
139
  "json-stringify-safe": "^5.0.0",
138
140
  "readable-stream": "^3.1.1",
139
141
  "semver": "^5.3.0"
@@ -158,7 +160,7 @@
158
160
  "lodash": "^4.17.14",
159
161
  "memcached": ">=0.2.8",
160
162
  "minami": "^1.1.1",
161
- "mocha": "^5.2.0",
163
+ "mocha": "^6.2.1",
162
164
  "mongodb": "^2",
163
165
  "mysql": "*",
164
166
  "nock": "9.1.9",
@@ -170,7 +172,7 @@
170
172
  "rimraf": "^2.6.3",
171
173
  "should": "*",
172
174
  "sinon": "^4.5.0",
173
- "tap": "^12.7.0",
175
+ "tap": "^14.6.9",
174
176
  "temp": "^0.8.1",
175
177
  "through": "^2.3.6",
176
178
  "when": "*"
@@ -1,94 +0,0 @@
1
- 'use strict'
2
-
3
- const logger = require('./logger').child({component: 'event_aggregator'})
4
- const PriorityQueue = require('./priority-queue')
5
-
6
- /**
7
- * Aggregates events up to a certain limit.
8
- *
9
- * @private
10
- * @class
11
- */
12
- class EventAggregator {
13
- constructor(limit) {
14
- this._events = new PriorityQueue(limit)
15
- }
16
-
17
- get limit() {
18
- return this._events.limit
19
- }
20
-
21
- set limit(limit) {
22
- this._events.setLimit(limit)
23
- }
24
-
25
- get seen() {
26
- return this._events.seen
27
- }
28
-
29
- get length() {
30
- return this._events.length
31
- }
32
-
33
- get overflow() {
34
- return this._events.overflow()
35
- }
36
-
37
- /**
38
- *
39
- */
40
- getQueue() {
41
- return this._events
42
- }
43
-
44
- /**
45
- * Fetches all the span events aggregated.
46
- *
47
- * @return {array.<Event>} An array of span events from the aggregator.
48
- */
49
- getEvents() {
50
- return this._events.toArray()
51
- }
52
-
53
- /**
54
- * Resets the contents of the aggregator and all counters.
55
- *
56
- * @return {PriorityQueue} The old collection of aggregated events.
57
- */
58
- clearEvents() {
59
- const oldEvents = this._events
60
- this._events = new PriorityQueue(this._events.limit)
61
- return oldEvents
62
- }
63
-
64
- addEvent(event, priority) {
65
- return this._events.add(event, priority)
66
- }
67
-
68
- /**
69
- * Merges a set of events back into the aggregator.
70
- *
71
- * This should only be used after a failed harvest with the `PriorityQueue`
72
- * returned from `EventAggregator#clearEvents`.
73
- *
74
- * @param {?PriorityQueue} events - The collection of events to re-merge.
75
- */
76
- mergeEvents(events) {
77
- if (!events) {
78
- return
79
- }
80
-
81
- // We calculate the number that will be merged for the log, but we try to
82
- // add every event because we want the ones with the highest priority, not
83
- // the first `n` events.
84
- const toMerge = Math.min(events.length, this.limit - this.length)
85
- logger.warn(
86
- 'Merging %d of %d events into %s for next harvest',
87
- toMerge, events.length, this.constructor.name
88
- )
89
-
90
- this._events.merge(events)
91
- }
92
- }
93
-
94
- module.exports = EventAggregator