newrelic 6.9.0 → 6.10.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
+ ### 6.10.0 (2020-06-22):
2
+
3
+ * Additional Transaction Information applied to Span Events
4
+ * When Distributed Tracing and/or Infinite Tracing are enabled, the Agent will now incorporate additional information from the Transaction Event on to the currently available Span Event of the transaction.
5
+ * The following items are affected:
6
+ * `aws-lambda` related attributes
7
+ * `error.message`
8
+ * `error.class`
9
+ * `error.expected`
10
+ * `http.statusCode`
11
+ * `http.statusText`
12
+ * `message.*`
13
+ * `parent.type`
14
+ * `parent.app`
15
+ * `parent.account`
16
+ * `parent.transportType`
17
+ * `parent.transportDuration`
18
+ * Request Parameters `request.parameters.*`
19
+ * `request.header.*`
20
+ * `request.method`
21
+ * `request.uri`
22
+ * Custom Attributes
23
+ * Custom transaction attributes added via `API.addCustomAttribute` or `API.addCustomAttributes` will now be propagated to the currently active span, if available.
24
+ * **Security Recommendation:**
25
+ * Review your Transaction Event attributes configuration. Any attribute include or exclude setting specific to Transaction Events should be applied to your Span Attributes configuration or global attributes configuration. Please see [Node.js agent attributes](https://docs.newrelic.com/docs/agents/nodejs-agent/attributes/nodejs-agent-attributes#configure-attributes) for more on how to configure.
26
+ * Upgraded @grpc/grpc-js from 1.0.3 to 1.0.4
27
+ * Modified redis callback-less versioned test to use `commandQueueLength` as indicator redis command has completed and test can continue. This is in effort to further reduce these test flickers. Additionally, added wait for client 'ready' before moving on to tests.
28
+ * Updated force secret test runs to run on branch pushes to the main repository.
29
+
1
30
  ### 6.9.0 (2020-06-08):
2
31
 
3
32
  * Added AWS API Gateway V2 Support to lambda instrumentation.
@@ -18,6 +18,8 @@ can be found at https://github.com/newrelic/node-newrelic.
18
18
  * [json-stringify-safe](#json-stringify-safe)
19
19
  * [readable-stream](#readable-stream)
20
20
  * [semver](#semver)
21
+ * [@grpc/grpc-js](#@grpc/grpc-js)
22
+ * [@grpc/proto-loader](#@grpc/proto-loader)
21
23
 
22
24
  ## async
23
25
 
package/api.js CHANGED
@@ -272,7 +272,7 @@ API.prototype.setControllerName = function setControllerName(name, action) {
272
272
  * @param {string} value The value you want displayed. Must be serializable.
273
273
  */
274
274
  API.prototype.addCustomAttribute = function addCustomAttribute(key, value) {
275
- var metric = this.agent.metrics.getOrCreateMetric(
275
+ const metric = this.agent.metrics.getOrCreateMetric(
276
276
  NAMES.SUPPORTABILITY.API + '/addCustomAttribute'
277
277
  )
278
278
  metric.incrementCallCount()
@@ -291,24 +291,36 @@ API.prototype.addCustomAttribute = function addCustomAttribute(key, value) {
291
291
  return false
292
292
  }
293
293
 
294
- var transaction = this.agent.tracer.getTransaction()
294
+ const transaction = this.agent.tracer.getTransaction()
295
295
  if (!transaction) {
296
- return logger.warn('No transaction found for custom attributes.')
296
+ logger.warn('No transaction found for custom attributes.')
297
+ return false
297
298
  }
298
299
 
299
- var trace = transaction.trace
300
+ const trace = transaction.trace
300
301
  if (!trace.custom) {
301
- return logger.warn(
302
+ logger.warn(
302
303
  'Could not add attribute %s to nonexistent custom attributes.',
303
304
  key
304
305
  )
306
+ return false
305
307
  }
306
308
 
307
309
  if (CUSTOM_DENYLIST.has(key)) {
308
- return logger.warn('Not overwriting value of NR-only attribute %s.', key)
310
+ logger.warn('Not overwriting value of NR-only attribute %s.', key)
311
+ return false
309
312
  }
310
313
 
311
314
  trace.addCustomAttribute(key, value)
315
+
316
+ const spanContext = this.agent.tracer.getSpanContext()
317
+ if (!spanContext) {
318
+ logger.debug('No span found for custom attributes.')
319
+ // success/failure is ambiguous here. since at least 1 attempt tried, not returning false
320
+ return
321
+ }
322
+
323
+ spanContext.addCustomAttribute(key, value, spanContext.ATTRIBUTE_PRIORITY.LOW)
312
324
  }
313
325
 
314
326
  /**
@@ -395,20 +407,22 @@ API.prototype.addCustomSpanAttribute = function addCustomSpanAttribute(key, valu
395
407
  return false
396
408
  }
397
409
 
398
- const segment = this.agent.tracer.getSegment()
410
+ const spanContext = this.agent.tracer.getSpanContext()
399
411
 
400
- if (!segment) {
401
- return logger.debug(
402
- 'Could not add attribute %s. No available span/segment.',
412
+ if (!spanContext) {
413
+ logger.debug(
414
+ 'Could not add attribute %s. No available span.',
403
415
  key
404
416
  )
417
+ return false
405
418
  }
406
419
 
407
420
  if (CUSTOM_DENYLIST.has(key)) {
408
- return logger.warn('Not overwriting value of NR-only attribute %s.', key)
421
+ logger.warn('Not overwriting value of NR-only attribute %s.', key)
422
+ return false
409
423
  }
410
424
 
411
- segment.addCustomSpanAttribute(key, value)
425
+ spanContext.addCustomAttribute(key, value)
412
426
  }
413
427
 
414
428
  API.prototype.setIgnoreTransaction = util.deprecate(
@@ -281,6 +281,11 @@ Config.prototype._fromServer = function _fromServer(params, key) {
281
281
  this.run_id = params.agent_run_id
282
282
  break
283
283
 
284
+ // if it's undefined or null, so be it
285
+ case 'request_headers_map':
286
+ this.request_headers_map = params.request_headers_map
287
+ break
288
+
284
289
  // handled by config.onConnect
285
290
  case 'high_security':
286
291
  break
@@ -19,10 +19,7 @@ const protoOptions = {keepCase: true,
19
19
  const pathProtoDefinition = __dirname +
20
20
  '../../../lib/grpc/endpoints/infinite-tracing/v1.proto'
21
21
 
22
- const defaultBackoffOpts = {
23
- initialSeconds: 0,
24
- seconds:15
25
- }
22
+ const DEFAULT_RECONNECT_DELAY_MS = 15 * 1000
26
23
 
27
24
  /**
28
25
  * Class for managing the GRPC connection
@@ -41,31 +38,21 @@ class GrpcConnection extends EventEmitter {
41
38
  *
42
39
  * @param {Object} traceObserverConfig config item config.infinite_tracing.trace_observer
43
40
  * @param {MetricAggregator} metrics metric aggregator, for supportability metrics
44
- * @param {Object} backoffOpts allows injecting specific backoff times
41
+ * @param {Number} [reconnectDelayMs=15000] number of milliseconds to wait before reconnecting
42
+ * for error states that require a reconnect delay.
45
43
  */
46
- constructor(traceObserverConfig, metrics, backoffOpts = defaultBackoffOpts) {
44
+ constructor(traceObserverConfig, metrics, reconnectDelayMs) {
47
45
  super()
48
46
 
49
- // only set opts if the object is valid
50
- if ( (backoffOpts.initialSeconds || backoffOpts.initialSeconds === 0) &&
51
- (backoffOpts.seconds || backoffOpts.seconds === 0)) {
52
- this._backoffOpts = backoffOpts
53
- } else {
54
- this._backoffOpts = defaultBackoffOpts
55
- logger.trace(
56
- 'invalid backoff information passed to constructor, using default values'
57
- )
58
- }
47
+ this._reconnectDelayMs = reconnectDelayMs || DEFAULT_RECONNECT_DELAY_MS
59
48
 
60
- // initial "stream connection" has a 0 second backoff, unless
61
- // different values are injected via _backoffOpts
62
- this._streamBackoffSeconds = this._backoffOpts.initialSeconds
63
49
  this._metrics = metrics
64
50
  this._setState(connectionStates.disconnected)
65
51
 
66
52
  this._endpoint = this.getTraceObserverEndpoint(traceObserverConfig)
67
53
  this._licensekey = null
68
54
  this._runId = null
55
+ this._requestHeadersMap = null
69
56
 
70
57
  this.stream = null
71
58
  }
@@ -79,11 +66,13 @@ class GrpcConnection extends EventEmitter {
79
66
  * @param {string} endpoint the GRPC server's endpoint
80
67
  * @param {string} license_key the agent license key
81
68
  * @param {string} run_id the current agent run id (also called agent run token)
69
+ * @param {object} requestHeadersMap request headers map received from server connect.
82
70
  * @param {string} [rootCerts] string of root (ca) certificates to attach to the connection.
83
71
  */
84
- setConnectionDetails(license_key, run_id, rootCerts) {
72
+ setConnectionDetails(license_key, run_id, requestHeadersMap, rootCerts) {
85
73
  this._licenseKey = license_key
86
74
  this._runId = run_id
75
+ this._requestHeadersMap = requestHeadersMap
87
76
  this._rootCerts = rootCerts
88
77
 
89
78
  return this
@@ -118,35 +107,21 @@ class GrpcConnection extends EventEmitter {
118
107
  }
119
108
 
120
109
  this._setState(connectionStates.connecting)
121
- logger.trace('connecting to grpc endpoint in [%s] seconds', this._getBackoffSeconds())
122
-
123
- setTimeout(() => {
124
- try {
125
- this.stream = this._connectSpans(this._endpoint, this._licenseKey, this._runId)
110
+ logger.trace('Connecting to gRPC endpoint.')
126
111
 
127
- // May not actually be "connected" at this point but we can write to the stream
128
- // immediately.
129
- this._setState(connectionStates.connected, this.stream)
112
+ try {
113
+ this.stream = this._connectSpans()
130
114
 
131
- // any future stream connect/disconnect after initial connection
132
- // should have a 15 second backoff
133
- this._setStreamBackoffAfterInitialStreamSetup()
134
- } catch (err) {
135
- logger.trace(
136
- err,
137
- 'Unexpected error establishing GRPC stream, will not attempt reconnect.'
138
- )
139
- this._disconnectWithoutReconnect()
140
- }
141
- }, this._getBackoffSeconds() * 1000)
142
- }
143
-
144
- _setStreamBackoffAfterInitialStreamSetup() {
145
- this._streamBackoffSeconds = this._backoffOpts.seconds
146
- }
147
-
148
- _setStreamBackoffToInitialValue() {
149
- this._streamBackoffSeconds = this._backoffOpts.initialSeconds
115
+ // May not actually be "connected" at this point but we can write to the stream
116
+ // immediately.
117
+ this._setState(connectionStates.connected, this.stream)
118
+ } catch (err) {
119
+ logger.trace(
120
+ err,
121
+ 'Unexpected error establishing gRPC stream, will not attempt reconnect.'
122
+ )
123
+ this._disconnect()
124
+ }
150
125
  }
151
126
 
152
127
  /**
@@ -159,14 +134,7 @@ class GrpcConnection extends EventEmitter {
159
134
  return
160
135
  }
161
136
 
162
- this._disconnectWithoutReconnect()
163
- }
164
-
165
- /**
166
- * Calculates backoff seconds
167
- */
168
- _getBackoffSeconds() {
169
- return this._streamBackoffSeconds
137
+ this._disconnect()
170
138
  }
171
139
 
172
140
  /**
@@ -175,11 +143,19 @@ class GrpcConnection extends EventEmitter {
175
143
  * @param {string} license_key
176
144
  * @param {string} run_id
177
145
  */
178
- _getMetadata(license_key, run_id, env) {
146
+ _getMetadata(license_key, run_id, requestHeadersMap, env) {
179
147
  const metadata = new grpc.Metadata()
180
148
  metadata.add('license_key', license_key)
181
149
  metadata.add('agent_run_token', run_id)
182
150
 
151
+ // p17 spec: If request_headers_map is empty or absent,
152
+ // the agent SHOULD NOT apply anything to its requests.
153
+ if (requestHeadersMap) {
154
+ for (const [key, value] of Object.entries(requestHeadersMap)) {
155
+ metadata.add(key.toLowerCase(), value) // keys MUST be lowercase for Infinite Tracing
156
+ }
157
+ }
158
+
183
159
  // check environment variables for testing parameters and
184
160
  // pass to server via meta-data.
185
161
  const flaky = parseInt(env.NEWRELIC_GRPCCONNECTION_METADATA_FLAKY, 10)
@@ -197,18 +173,23 @@ class GrpcConnection extends EventEmitter {
197
173
  }
198
174
 
199
175
  /**
200
- * Sets internal object state to disconnected initiates a new connection
201
- *
202
- * Called when we receive a stream status that indicates a potential
203
- * problem with the stream. We set the state (which will emit an event),
204
- * increment tries to honor the backoff, and then reconnect.
176
+ * Disconnects from gRPC endpoint and schedules establishing a new connection.
177
+ * @param {number} reconnectDelayMs number of milliseconds to wait before reconnecting.
205
178
  */
206
- _reconnect() {
179
+ _reconnect(reconnectDelayMs = 0) {
207
180
  this._disconnect()
208
- this.connectSpans()
181
+
182
+ logger.trace('Reconnecting to gRPC endpoint in [%s] seconds', reconnectDelayMs)
183
+
184
+ setTimeout(
185
+ this.connectSpans.bind(this),
186
+ reconnectDelayMs
187
+ )
209
188
  }
210
189
 
211
190
  _disconnect() {
191
+ logger.trace('Disconnecting from gRPC endpoint.')
192
+
212
193
  if (this.stream) {
213
194
  this.stream.removeAllListeners()
214
195
 
@@ -221,17 +202,6 @@ class GrpcConnection extends EventEmitter {
221
202
  this._setState(connectionStates.disconnected)
222
203
  }
223
204
 
224
- /**
225
- * Disconnects Without Reconnect
226
- *
227
- * Certain GRPC statuses require us to disconnect and _not_
228
- * attempt a stream reconnect.
229
- */
230
- _disconnectWithoutReconnect() {
231
- this._disconnect()
232
- this._setStreamBackoffToInitialValue()
233
- }
234
-
235
205
  /**
236
206
  * Central location to setup stream observers
237
207
  *
@@ -244,14 +214,14 @@ class GrpcConnection extends EventEmitter {
244
214
  // listen for responses from server and log
245
215
  if (logger.traceEnabled()) {
246
216
  stream.on('data', function data(response) {
247
- logger.trace("grpc span response stream: %s", JSON.stringify(response))
217
+ logger.trace("gRPC span response stream: %s", JSON.stringify(response))
248
218
  })
249
219
  }
250
220
 
251
221
  // listen for status that indicate stream has ended,
252
222
  // and we need to disconnect
253
223
  stream.on('status', (grpcStatus) => {
254
- logger.trace('GRPC Status Received [%s]: %s', grpcStatus.code, grpcStatus.details)
224
+ logger.trace('gRPC Status Received [%s]: %s', grpcStatus.code, grpcStatus.details)
255
225
  const grpcStatusName =
256
226
  grpc.status[grpcStatus.code] ? grpc.status[grpcStatus.code] : 'UNKNOWN'
257
227
 
@@ -263,19 +233,15 @@ class GrpcConnection extends EventEmitter {
263
233
  // per the spec, An UNIMPLEMENTED status code from gRPC indicates
264
234
  // that the versioned Trace Observer is no longer available. Agents
265
235
  // MUST NOT attempt to reconnect in this case
266
- this._disconnectWithoutReconnect()
236
+ this._disconnect()
237
+ } else if (grpc.status[grpc.status.OK] === grpcStatusName) {
238
+ this._reconnect()
267
239
  } else {
268
- // all statuses are treated as "bad stuff" happened, and
269
- // as a signal we need to reconnect (except UNIMPLEMENTED).
270
- // Even an "OK status" indicates the call completed successfully,
271
- // which indicates the stream is over.
272
- if (grpc.status[grpc.status.OK] !== grpcStatusName) {
273
- this._metrics.getOrCreateMetric(
274
- util.format(NAMES.INFINITE_TRACING.SPAN_RESPONSE_GRPC_STATUS, grpcStatusName)
275
- ).incrementCallCount()
276
- }
240
+ this._metrics.getOrCreateMetric(
241
+ util.format(NAMES.INFINITE_TRACING.SPAN_RESPONSE_GRPC_STATUS, grpcStatusName)
242
+ ).incrementCallCount()
277
243
 
278
- this._reconnect()
244
+ this._reconnect(this._reconnectDelayMs)
279
245
  }
280
246
  })
281
247
 
@@ -322,7 +288,7 @@ class GrpcConnection extends EventEmitter {
322
288
  * this makes an http2 request with the metadata, and then returns
323
289
  * a stream for further writing.
324
290
  */
325
- _connectSpans(endpoint, license_key, run_id) {
291
+ _connectSpans() {
326
292
  const packageDefinition = protoLoader.loadSync(pathProtoDefinition, protoOptions)
327
293
 
328
294
  const serviceDefinition = grpc.loadPackageDefinition(
@@ -331,14 +297,18 @@ class GrpcConnection extends EventEmitter {
331
297
 
332
298
  const credentials = this._generateCredentials(grpc)
333
299
  const client = new serviceDefinition.IngestService(
334
- endpoint,
300
+ this._endpoint,
335
301
  credentials
336
302
  )
337
- const metadata = this._getMetadata(license_key, run_id, process.env)
303
+
304
+ const metadata =
305
+ this._getMetadata(this._licenseKey, this._runId, this._requestHeadersMap, process.env)
306
+
338
307
  const stream = client.recordSpan(metadata)
339
308
  this._setupSpanStreamObservers(stream)
340
309
 
341
310
  return stream
342
311
  }
343
312
  }
313
+
344
314
  module.exports = GrpcConnection
@@ -110,6 +110,8 @@ function _collectHeaders(headers, nameMap, prefix, transaction) {
110
110
  ? COLLECTED_REQUEST_HEADERS
111
111
  : Object.keys(headers)
112
112
 
113
+ const segment = transaction.agent.tracer.getSegment()
114
+
113
115
  for (var i = 0; i < headerKeys.length; i++) {
114
116
  var headerKey = headerKeys[i]
115
117
  var header = headers[headerKey]
@@ -128,6 +130,8 @@ function _collectHeaders(headers, nameMap, prefix, transaction) {
128
130
  attributeName,
129
131
  header
130
132
  )
133
+
134
+ segment.addSpanAttribute(attributeName, header)
131
135
  }
132
136
  }
133
137
  }
@@ -133,7 +133,7 @@ module.exports = function instrumentOutbound(agent, opts, makeRequest) {
133
133
  if (parsed.parameters) {
134
134
  // Scrub and parse returns on object with a null prototype.
135
135
  for (let key in parsed.parameters) { // eslint-disable-line guard-for-in
136
- segment.addAttribute(`request.parameters.${key}`, parsed.parameters[key])
136
+ segment.addSpanAttribute(`request.parameters.${key}`, parsed.parameters[key])
137
137
  }
138
138
  }
139
139
  segment.addAttribute('url', `${proto}//${hostname}${parsed.path}`)
@@ -196,8 +196,8 @@ function handleError(segment, req, error) {
196
196
  */
197
197
  function handleResponse(segment, hostname, req, res) {
198
198
  // Add response attributes for spans
199
- segment.addAttribute('http.statusCode', res.statusCode)
200
- segment.addAttribute('http.statusText', res.statusMessage)
199
+ segment.addSpanAttribute('http.statusCode', res.statusCode)
200
+ segment.addSpanAttribute('http.statusText', res.statusMessage)
201
201
 
202
202
  // If CAT is enabled, grab those headers!
203
203
  const agent = segment.transaction.agent
@@ -71,6 +71,13 @@ function wrapEmitWithTransaction(agent, emit, isHTTPS) {
71
71
  const segment = tracer.createSegment(request.url, recordWeb)
72
72
  segment.start()
73
73
 
74
+ if (request.method != null) {
75
+ segment.addSpanAttribute(
76
+ 'request.method',
77
+ request.method
78
+ )
79
+ }
80
+
74
81
  if (txInfo) {
75
82
  // Seed segment stack to enable parenting logic leveraged by
76
83
  // web framework instrumentations.
@@ -98,6 +105,11 @@ function wrapEmitWithTransaction(agent, emit, isHTTPS) {
98
105
  transaction.url
99
106
  )
100
107
 
108
+ segment.addSpanAttribute(
109
+ 'request.uri',
110
+ transaction.url
111
+ )
112
+
101
113
  // store the port on which this transaction runs
102
114
  if (this.address instanceof Function) {
103
115
  var address = this.address()
@@ -172,6 +184,8 @@ function wrapEmitWithTransaction(agent, emit, isHTTPS) {
172
184
  'http.statusCode',
173
185
  responseCode
174
186
  )
187
+
188
+ segment.addSpanAttribute('http.statusCode', responseCode)
175
189
 
176
190
  /*
177
191
  TODO: remove with next major release
@@ -192,6 +206,8 @@ function wrapEmitWithTransaction(agent, emit, isHTTPS) {
192
206
  response.statusMessage
193
207
  )
194
208
 
209
+ segment.addSpanAttribute('http.statusText', response.statusMessage)
210
+
195
211
  /*
196
212
  TODO: remove with next major release
197
213
  httpResponseMessage attribute is deprecated
@@ -0,0 +1,196 @@
1
+ 'use strict'
2
+
3
+ const Config = require('./config')
4
+ const logger = require('./logger').child({component: 'attributes'})
5
+ const isValidType = require('./util/attribute-types')
6
+ const byteUtils = require('./util/byte-limit')
7
+ const properties = require('./util/properties')
8
+
9
+ const ATTRIBUTE_PRIORITY = {
10
+ HIGH: Infinity,
11
+ LOW: -Infinity
12
+ }
13
+
14
+ class PrioritizedAttributes {
15
+ constructor(scope, limit = Infinity) {
16
+ this.filter = makeFilter(scope)
17
+ this.limit = limit
18
+
19
+ this.attributes = new Map()
20
+ }
21
+
22
+
23
+ isValidLength(str) {
24
+ return typeof str === 'number' || byteUtils.isValidLength(str, 255)
25
+ }
26
+
27
+ _set(destinations, key, value, truncateExempt, priority) {
28
+ this.attributes.set(key, {value, destinations, truncateExempt, priority})
29
+ }
30
+
31
+ get(dest) {
32
+ const attrs = Object.create(null)
33
+
34
+ for (let [key, attr] of this.attributes) {
35
+ if (!(attr.destinations & dest)) {
36
+ continue
37
+ }
38
+
39
+ attrs[key] = typeof attr.value === 'string' && !attr.truncateExempt
40
+ ? byteUtils.truncate(attr.value, 255)
41
+ : attr.value
42
+ }
43
+
44
+ return attrs
45
+ }
46
+
47
+ has(key) {
48
+ this.attributes.has(key)
49
+ }
50
+
51
+ reset() {
52
+ this.attributes = new Map()
53
+ }
54
+
55
+ addAttribute(
56
+ destinations,
57
+ key,
58
+ value,
59
+ truncateExempt = false,
60
+ priority = ATTRIBUTE_PRIORITY.HIGH
61
+ ) {
62
+ const existingAttribute = this.attributes.get(key)
63
+
64
+ let droppableAttributeKey = null
65
+ if (!existingAttribute && this.attributes.size === this.limit) {
66
+ droppableAttributeKey = this._getDroppableAttributeKey(priority)
67
+
68
+ if (!droppableAttributeKey) {
69
+ logger.debug(
70
+ `Maximum number of custom attributes have been added.
71
+ Dropping attribute ${key} with ${value} type.`
72
+ )
73
+
74
+ return
75
+ }
76
+ }
77
+
78
+ if (existingAttribute && priority < existingAttribute.priority) {
79
+ logger.debug('incoming priority for \'%s\' is lower than existing, not updating.', key)
80
+ logger.trace(
81
+ '%s attribute retained value: %s, ignored value: %s',
82
+ key,
83
+ existingAttribute.value,
84
+ value
85
+ )
86
+ return
87
+ }
88
+
89
+ if (!isValidType(value)) {
90
+ logger.debug(
91
+ 'Not adding attribute %s with %s value type. This is expected for undefined' +
92
+ 'attributes and only an issue if an attribute is not expected to be undefined' +
93
+ 'or not of the type expected.',
94
+ key,
95
+ typeof value
96
+ )
97
+ return
98
+ }
99
+
100
+ if (!this.isValidLength(key)) {
101
+ logger.warn(
102
+ 'Length limit exceeded for attribute name, not adding: %s',
103
+ key
104
+ )
105
+ return
106
+ }
107
+
108
+
109
+ // Only set the attribute if at least one destination passed
110
+ const validDestinations = this.filter(destinations, key)
111
+ if (!validDestinations) {
112
+ return
113
+ }
114
+
115
+ if (droppableAttributeKey) {
116
+ logger.trace(
117
+ 'dropping existing lower priority attribute %s ' +
118
+ 'to add higher priority attribute %s',
119
+ droppableAttributeKey,
120
+ key
121
+ )
122
+
123
+ this.attributes.delete(droppableAttributeKey)
124
+ }
125
+
126
+ this._set(validDestinations, key, value, truncateExempt, priority)
127
+ }
128
+
129
+ addAttributes(destinations, attrs) {
130
+ for (let key in attrs) {
131
+ if (properties.hasOwn(attrs, key)) {
132
+ this.addAttribute(destinations, key, attrs[key])
133
+ }
134
+ }
135
+ }
136
+
137
+ _getDroppableAttributeKey(incomingPriority) {
138
+ // There will never be anything lower priority to drop
139
+ if (incomingPriority === ATTRIBUTE_PRIORITY.LOW) {
140
+ return null
141
+ }
142
+
143
+ this.lastFoundIndexCache = this.lastFoundIndexCache || Object.create(null)
144
+ const lastFoundIndex = this.lastFoundIndexCache[incomingPriority]
145
+
146
+ // We've already dropped all items lower than incomingPriority.
147
+ // We can honor the cache because at the point by which we've dropped
148
+ // all lower priority items, due to being at max capacity, there will never be another
149
+ // lower-priority item added. Lower priority items are unable to drop higher priority items.
150
+ if (lastFoundIndex === -1) {
151
+ return null
152
+ }
153
+
154
+ // We can't reverse iterate w/o creating an array that will iterate,
155
+ // so we just iterate forward stopping once we've checked the last cached index.
156
+ let lowerPriorityAttributeName = null
157
+ let foundIndex = -1
158
+
159
+ let index = 0
160
+ for (const [key, attribute] of this.attributes) {
161
+ // Don't search past last found lower priority item.
162
+ // At the point of dropping items for this priority,
163
+ // lower priority items will never be added.
164
+ if (lastFoundIndex && index > lastFoundIndex) {
165
+ break
166
+ }
167
+
168
+ if (attribute.priority < incomingPriority) {
169
+ lowerPriorityAttributeName = key
170
+ foundIndex = index
171
+ }
172
+
173
+ index++
174
+ }
175
+
176
+ // Item may not get dropped, so we simply store the index as
177
+ // an upper maximum and allow a future pass to clear out.
178
+ this.lastFoundIndexCache[incomingPriority] = foundIndex
179
+
180
+ return lowerPriorityAttributeName
181
+ }
182
+ }
183
+
184
+ function makeFilter(scope) {
185
+ const {attributeFilter} = Config.getInstance()
186
+ if (scope === 'transaction') {
187
+ return (d, k) => attributeFilter.filterTransaction(d, k)
188
+ } else if (scope === 'segment') {
189
+ return (d, k) => attributeFilter.filterSegment(d, k)
190
+ }
191
+ }
192
+
193
+ module.exports = {
194
+ PrioritizedAttributes: PrioritizedAttributes,
195
+ ATTRIBUTE_PRIORITY: ATTRIBUTE_PRIORITY
196
+ }
@@ -121,7 +121,10 @@ class AwsLambda {
121
121
  transaction.setPartialName(transactionName)
122
122
 
123
123
  const isApiGatewayLambdaProxy = apiGateway.isLambdaProxyEvent(event)
124
-
124
+ const segmentRecorder = isApiGatewayLambdaProxy ? recordWeb : recordBackground
125
+ const segment = shim.createSegment(functionName, segmentRecorder)
126
+ transaction.baseSegment = segment
127
+
125
128
  // resultProcessor is used to execute additional logic based on the
126
129
  // payload supplied to the callback.
127
130
  let resultProcessor
@@ -131,9 +134,6 @@ class AwsLambda {
131
134
  resultProcessor = getApiGatewayLambdaProxyResultProcessor(transaction)
132
135
  }
133
136
 
134
- const segmentRecorder = isApiGatewayLambdaProxy ? recordWeb : recordBackground
135
- const segment = shim.createSegment(functionName, segmentRecorder)
136
- transaction.baseSegment = segment
137
137
 
138
138
  const cbIndex = args.length - 1
139
139
 
@@ -175,6 +175,8 @@ class AwsLambda {
175
175
  shim.agent.setLambdaArn(context.invokedFunctionArn)
176
176
 
177
177
  shim.agent.setLambdaFunctionVersion(context.functionVersion)
178
+
179
+ segment.addSpanAttributes(awsAttributes)
178
180
 
179
181
  segment.start()
180
182
 
@@ -301,6 +303,8 @@ function getApiGatewayLambdaProxyResultProcessor(transaction) {
301
303
  function setWebRequest(shim, transaction, request) {
302
304
  transaction.type = shim.WEB
303
305
 
306
+ const segment = transaction.baseSegment
307
+
304
308
  transaction.url = urltils.scrub(request.url.path)
305
309
  transaction.verb = request.method
306
310
  transaction.trace.attributes.addAttribute(
@@ -308,6 +312,12 @@ function setWebRequest(shim, transaction, request) {
308
312
  'request.method',
309
313
  request.method
310
314
  )
315
+
316
+ segment.addSpanAttribute(
317
+ 'request.method',
318
+ request.method
319
+ )
320
+
311
321
  transaction.port = request.url.port
312
322
 
313
323
  transaction.addRequestParameters(request.url.requestParameters)
@@ -319,6 +329,11 @@ function setWebRequest(shim, transaction, request) {
319
329
  request.url.path
320
330
  )
321
331
 
332
+ segment.addSpanAttribute(
333
+ 'request.uri',
334
+ request.url.path
335
+ )
336
+
322
337
  headerAttributes.collectRequestHeaders(request.headers, transaction)
323
338
 
324
339
  if (shim.agent.config.distributed_tracing.enabled) {
@@ -369,6 +384,10 @@ function setWebResponse(transaction, response) {
369
384
  transaction.statusCode = response.statusCode
370
385
 
371
386
  const responseCode = String(response.statusCode)
387
+ /*
388
+ TODO: remove with next major release
389
+ httpResponseCode attribute is deprecated
390
+ */
372
391
  transaction.trace.attributes.addAttribute(
373
392
  ATTR_DEST.TRANS_COMMON,
374
393
  'httpResponseCode',
@@ -376,10 +395,24 @@ function setWebResponse(transaction, response) {
376
395
  )
377
396
 
378
397
  if (/^\d+$/.test(responseCode)) {
398
+ /*
399
+ TODO: remove with next major release
400
+ response.status attribute is deprecated
401
+ */
379
402
  transaction.trace.attributes.addAttribute(
380
403
  ATTR_DEST.TRANS_COMMON,
381
404
  'response.status',
382
405
  responseCode)
406
+
407
+ transaction.trace.attributes.addAttribute(
408
+ ATTR_DEST.TRANS_COMMON,
409
+ 'http.statusCode',
410
+ responseCode
411
+ )
412
+
413
+ const segment = transaction.agent.tracer.getSegment()
414
+
415
+ segment.addSpanAttribute('http.statusCode', responseCode)
383
416
  }
384
417
 
385
418
  headerAttributes.collectResponseHeaders(response.headers, transaction)
@@ -750,6 +750,12 @@ function recordSubscribedConsume(nodule, properties, spec) {
750
750
  ATTR_DESTS.NONE,
751
751
  'message.parameters.' + key,
752
752
  msgDesc.parameters[key])
753
+
754
+ tx.baseSegment.attributes.addAttribute(
755
+ ATTR_DESTS.NONE,
756
+ 'message.parameters.' + key,
757
+ msgDesc.parameters[key]
758
+ )
753
759
  }
754
760
  }
755
761
 
@@ -762,6 +768,11 @@ function recordSubscribedConsume(nodule, properties, spec) {
762
768
  'message.routingKey',
763
769
  msgDesc.routingKey
764
770
  )
771
+
772
+ tx.baseSegment.addSpanAttribute(
773
+ 'message.routingKey',
774
+ msgDesc.routingKey
775
+ )
765
776
  }
766
777
  if (shim.isString(msgDesc.queue)) {
767
778
  tx.trace.attributes.addAttribute(
@@ -769,6 +780,11 @@ function recordSubscribedConsume(nodule, properties, spec) {
769
780
  'message.queueName',
770
781
  msgDesc.queue
771
782
  )
783
+
784
+ tx.baseSegment.addSpanAttribute(
785
+ 'message.queueName',
786
+ msgDesc.queue
787
+ )
772
788
  }
773
789
  }
774
790
  if (msgDesc.headers) {
@@ -1,13 +1,36 @@
1
1
  'use strict'
2
2
 
3
+ const {MAXIMUM_CUSTOM_ATTRIBUTES} = require('../attributes')
4
+ const {PrioritizedAttributes, ATTRIBUTE_PRIORITY} = require('../prioritized-attributes')
5
+ const {DESTINATIONS} = require('../config/attribute-filter')
6
+
7
+ // Scoping impacts memoization. We could decide to add a scope instead of including
8
+ // spans in segment scope in the future.
9
+ const ATTRIBUTE_SCOPE = 'segment'
10
+
3
11
  class SpanContext {
4
- constructor(intrinsicAttributes) {
12
+ constructor(intrinsicAttributes, customAttributes) {
5
13
  this.intrinsicAttributes = intrinsicAttributes || Object.create(null)
14
+
15
+ this.customAttributes =
16
+ customAttributes || new PrioritizedAttributes(ATTRIBUTE_SCOPE, MAXIMUM_CUSTOM_ATTRIBUTES)
17
+
18
+ this.ATTRIBUTE_PRIORITY = ATTRIBUTE_PRIORITY
6
19
  }
7
20
 
8
21
  addIntrinsicAttribute(key, value) {
9
22
  this.intrinsicAttributes[key] = value
10
23
  }
24
+
25
+ addCustomAttribute(key, value, priority) {
26
+ this.customAttributes.addAttribute(
27
+ DESTINATIONS.SPAN_EVENT,
28
+ key,
29
+ value,
30
+ false,
31
+ priority
32
+ )
33
+ }
11
34
  }
12
35
 
13
36
  module.exports = SpanContext
@@ -80,7 +80,9 @@ class SpanEvent {
80
80
  */
81
81
  static fromSegment(segment, parentId = null, isRoot = false) {
82
82
  const attributes = segment.attributes.get(DESTINATIONS.SPAN_EVENT)
83
- const customAttributes = segment.customAttributes.get(DESTINATIONS.SPAN_EVENT)
83
+
84
+ const spanContext = segment.getSpanContext()
85
+ const customAttributes = spanContext.customAttributes.get(DESTINATIONS.SPAN_EVENT)
84
86
 
85
87
  let span = null
86
88
  if (HttpSpanEvent.testSegment(segment)) {
@@ -91,7 +93,7 @@ class SpanEvent {
91
93
  span = new SpanEvent(attributes, customAttributes)
92
94
  }
93
95
 
94
- const spanContext = segment.getSpanContext()
96
+
95
97
  for (const [key, value] of Object.entries(spanContext.intrinsicAttributes)) {
96
98
  span.intrinsics[key] = value
97
99
  }
@@ -126,10 +126,11 @@ class SpanStreamer {
126
126
  }
127
127
  }
128
128
 
129
- connect(agent_run_id) {
129
+ connect(agent_run_id, requestHeadersMap) {
130
130
  this.connection.setConnectionDetails(
131
131
  this.license_key,
132
- agent_run_id
132
+ agent_run_id,
133
+ requestHeadersMap
133
134
  )
134
135
 
135
136
  this.connection.connectSpans()
@@ -31,8 +31,10 @@ class StreamingSpanEventAggregator extends Aggregator {
31
31
  }
32
32
 
33
33
  logger.trace('StreamingSpanEventAggregator starting up')
34
- this.stream.connect(this.runId)
34
+ this.stream.connect(this.runId, this.requestHeadersMap)
35
35
  this.started = true
36
+
37
+ this.emit('started')
36
38
  }
37
39
 
38
40
  stop() {
@@ -43,6 +45,8 @@ class StreamingSpanEventAggregator extends Aggregator {
43
45
  logger.trace('StreamingSpanEventAggregator stopping')
44
46
  this.stream.disconnect()
45
47
  this.started = false
48
+
49
+ this.emit('stopped')
46
50
  }
47
51
 
48
52
  send() {
@@ -83,6 +87,12 @@ class StreamingSpanEventAggregator extends Aggregator {
83
87
  const span = StreamingSpanEvent.fromSegment(segment, parentId, isRoot)
84
88
  this.stream.write(span)
85
89
  }
90
+
91
+ reconfigure(config) {
92
+ super.reconfigure(config)
93
+
94
+ this.requestHeadersMap = config.request_headers_map
95
+ }
86
96
  }
87
97
 
88
98
  module.exports = StreamingSpanEventAggregator
@@ -98,7 +98,9 @@ class StreamingSpanEvent {
98
98
 
99
99
  static fromSegment(segment, parentId = null, isRoot = false) {
100
100
  const agentAttributes = segment.attributes.get(DESTINATIONS.SPAN_EVENT)
101
- const customAttributes = segment.customAttributes.get(DESTINATIONS.SPAN_EVENT)
101
+
102
+ const spanContext = segment.getSpanContext()
103
+ const customAttributes = spanContext.customAttributes.get(DESTINATIONS.SPAN_EVENT)
102
104
 
103
105
  const transaction = segment.transaction
104
106
  const traceId = transaction.traceId
@@ -112,7 +114,6 @@ class StreamingSpanEvent {
112
114
  span = new StreamingSpanEvent(traceId, agentAttributes, customAttributes)
113
115
  }
114
116
 
115
- const spanContext = segment.getSpanContext()
116
117
  for (const [key, value] of Object.entries(spanContext.intrinsicAttributes)) {
117
118
  span.addIntrinsicAttribute(key, value)
118
119
  }
@@ -406,6 +406,20 @@ function finalizeNameFromUri(requestURL, statusCode) {
406
406
  'request.parameters.' + key,
407
407
  params[key]
408
408
  )
409
+
410
+ const segment = this.agent.tracer.getSegment()
411
+
412
+ if (!segment) {
413
+ logger
414
+ .trace('Active segment not available, not adding request.parameters attribute for %s',
415
+ key)
416
+ } else {
417
+ segment.attributes.addAttribute(
418
+ DESTS.NONE,
419
+ 'request.parameters.' + key,
420
+ params[key]
421
+ )
422
+ }
409
423
  }
410
424
  }
411
425
  }, this)
@@ -466,6 +480,21 @@ Transaction.prototype._markAsWeb = function _markAsWeb(rawURL) {
466
480
  'request.parameters.' + key,
467
481
  params[key]
468
482
  )
483
+
484
+ const segment = this.agent.tracer.getSegment()
485
+
486
+ if (!segment) {
487
+ logger
488
+ .trace(
489
+ 'Active segment not available, not adding request.parameters span attribute for %s',
490
+ key)
491
+ } else {
492
+ segment.attributes.addAttribute(
493
+ DESTS.NONE,
494
+ 'request.parameters.' + key,
495
+ params[key]
496
+ )
497
+ }
469
498
  }
470
499
  }
471
500
  this.baseSegment.markAsWeb()
@@ -717,15 +746,15 @@ function _linkExceptionToSegment(exception) {
717
746
 
718
747
  // Add error attributes to the span
719
748
  const details = exception.getErrorDetails(config)
720
- segment.addAttribute('error.message', details.message)
721
- segment.addAttribute('error.class', details.type)
749
+ segment.addSpanAttribute('error.message', details.message)
750
+ segment.addSpanAttribute('error.class', details.type)
722
751
 
723
752
  const isExpected =
724
753
  errorHelper.isExpectedErrorClass(config, details.type) ||
725
754
  errorHelper.isExpectedErrorMessage(config, details.type, details.message)
726
755
 
727
756
  if (isExpected) {
728
- segment.addAttribute('error.expected', isExpected)
757
+ segment.addSpanAttribute('error.expected', isExpected)
729
758
  }
730
759
 
731
760
  // Add the span/segment ID to the exception as agent attributes
@@ -1264,6 +1293,14 @@ function addRequestParameters(requestParameters) {
1264
1293
  'request.parameters.' + key,
1265
1294
  requestParameters[key]
1266
1295
  )
1296
+
1297
+ const segment = this.baseSegment
1298
+
1299
+ segment.attributes.addAttribute(
1300
+ DESTS.NONE,
1301
+ 'request.parameters.' + key,
1302
+ requestParameters[key]
1303
+ )
1267
1304
  }
1268
1305
  }
1269
1306
  }
@@ -41,6 +41,8 @@ function Trace(transaction) {
41
41
  'host.displayName',
42
42
  displayName
43
43
  )
44
+
45
+ this.displayName = displayName
44
46
  }
45
47
  this.domain = null
46
48
  }
@@ -82,6 +84,26 @@ Trace.prototype.generateSpanEvents = function generateSpanEvents() {
82
84
  const spanAggregator = this.transaction.agent.spanEventAggregator
83
85
 
84
86
  const children = this.root.getChildren()
87
+
88
+ if (children.length > 0) {
89
+ // At the point where these attributes are available, we only have a
90
+ // root span. Adding attributes to first non-root span here.
91
+ const attributeMap = {
92
+ 'host.displayName': this.displayName,
93
+ 'parent.type': this.transaction.parentType,
94
+ 'parent.app': this.transaction.parentApp,
95
+ 'parent.account': this.transaction.parentAcct,
96
+ 'parent.transportType': this.transaction.parentTransportType,
97
+ 'parent.transportDuration': this.transaction.parentTransportDuration
98
+ }
99
+
100
+ for (const [key, value] of Object.entries(attributeMap)) {
101
+ if (value !== null) {
102
+ children[0].addSpanAttribute(key, value)
103
+ }
104
+ }
105
+ }
106
+
85
107
  for (let i = 0; i < children.length; ++i) {
86
108
  toProcess.push(new DTTraceNode(children[i], this.transaction.parentSpanId, true))
87
109
  }
@@ -6,7 +6,7 @@ const Timer = require('../../timer')
6
6
  const urltils = require('../../util/urltils')
7
7
  const hashes = require('../../util/hashes')
8
8
 
9
- const {Attributes, MAXIMUM_CUSTOM_ATTRIBUTES} = require('../../attributes')
9
+ const {Attributes} = require('../../attributes')
10
10
  const ExclusiveCalculator = require('./exclusive-time-calculator')
11
11
  const SpanContext = require('../../spans/span-context')
12
12
 
@@ -53,7 +53,6 @@ function TraceSegment(transaction, name, recorder) {
53
53
  }
54
54
 
55
55
  this.attributes = new Attributes(ATTRIBUTE_SCOPE)
56
- this.customAttributes = new Attributes(ATTRIBUTE_SCOPE, MAXIMUM_CUSTOM_ATTRIBUTES)
57
56
 
58
57
  this.children = []
59
58
 
@@ -99,13 +98,9 @@ function addAttribute(key, value, truncateExempt = false) {
99
98
  )
100
99
  }
101
100
 
102
- TraceSegment.prototype.getAttributes = function getAttributes() {
103
- return this.attributes.get(DESTINATIONS.TRANS_SEGMENT)
104
- }
105
-
106
- TraceSegment.prototype.addCustomSpanAttribute =
107
- function addCustomSpanAttribute(key, value, truncateExempt = false) {
108
- this.customAttributes.addAttribute(
101
+ TraceSegment.prototype.addSpanAttribute =
102
+ function addSpanAttribute(key, value, truncateExempt = false) {
103
+ this.attributes.addAttribute(
109
104
  DESTINATIONS.SPAN_EVENT,
110
105
  key,
111
106
  value,
@@ -113,6 +108,18 @@ function addCustomSpanAttribute(key, value, truncateExempt = false) {
113
108
  )
114
109
  }
115
110
 
111
+ TraceSegment.prototype.addSpanAttributes =
112
+ function addSpanAttributes(attributes) {
113
+ this.attributes.addAttributes(
114
+ DESTINATIONS.SPAN_EVENT,
115
+ attributes
116
+ )
117
+ }
118
+
119
+ TraceSegment.prototype.getAttributes = function getAttributes() {
120
+ return this.attributes.get(DESTINATIONS.TRANS_SEGMENT)
121
+ }
122
+
116
123
  TraceSegment.prototype.getSpanId = function getSpanId() {
117
124
  const conf = this.transaction.agent.config
118
125
  const enabled = conf.span_events.enabled && conf.distributed_tracing.enabled
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "6.9.0",
3
+ "version": "6.10.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "contributors": [
@@ -142,7 +142,7 @@
142
142
  "newrelic-naming-rules": "./bin/test-naming-rules.js"
143
143
  },
144
144
  "dependencies": {
145
- "@grpc/grpc-js": "1.0.3",
145
+ "@grpc/grpc-js": "1.0.4",
146
146
  "@grpc/proto-loader": "^0.5.4",
147
147
  "@newrelic/aws-sdk": "^1.1.1",
148
148
  "@newrelic/koa": "^3.0.0",