newrelic 6.5.0 → 6.6.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.
@@ -0,0 +1,109 @@
1
+ 'use strict'
2
+
3
+ const logger = require('../logger').child({component: 'span-streamer'})
4
+
5
+ const NAMES = require('../metrics/names')
6
+
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
+
18
+ class SpanStreamer {
19
+ constructor(license_key, connection, metrics) {
20
+ this.stream = null
21
+ this.license_key = license_key
22
+ this.connection = connection
23
+ this._metrics = metrics
24
+ this._writable = false
25
+
26
+ // 'connected' indicates a safely writeable stream.
27
+ // May still be mid-connect to gRPC server.
28
+ this.connection.on('connected', (stream) =>{
29
+ this.stream = stream
30
+ this._writable = true
31
+ })
32
+
33
+ this.connection.on('disconnected', () =>{
34
+ this.stream = null
35
+ this._writable = false
36
+ })
37
+ }
38
+
39
+ write(span) {
40
+ if (!this.stream) {
41
+ logger.warnOnce(NO_STREAM_WARNING)
42
+ return false
43
+ }
44
+
45
+ if (!this._writable) {
46
+ return false
47
+ }
48
+
49
+ const formattedSpan = span.toStreamingFormat()
50
+
51
+ 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
84
+ } catch (err) {
85
+ logger.trace('Could not stream span.', err)
86
+ // TODO: something has gone horribly wrong.
87
+ // We may want to log and turn off this aggregator
88
+ // to prevent sending further spans. Maybe even "disable" their creation?
89
+ // or is there a situation where we can recover?
90
+
91
+ return false
92
+ }
93
+ }
94
+
95
+ connect(agent_run_id) {
96
+ this.connection.setConnectionDetails(
97
+ this.license_key,
98
+ agent_run_id
99
+ )
100
+
101
+ this.connection.connectSpans()
102
+ }
103
+
104
+ disconnect() {
105
+ this.connection.disconnect()
106
+ }
107
+ }
108
+
109
+ module.exports = SpanStreamer
@@ -0,0 +1,59 @@
1
+ 'use strict'
2
+
3
+ const mapToStreamingType = require('./map-to-streaming-type')
4
+
5
+ /**
6
+ * Specialized attribute collection class for use with infinite streaming.
7
+ * Currently designed to be sent over grpc via the v1.proto definition.
8
+ *
9
+ * @private
10
+ * @class
11
+ */
12
+ class StreamingSpanAttributes {
13
+ constructor(attributes) {
14
+ if (attributes) {
15
+ this.addAttributes(attributes)
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Add a key/value pair to the attribute collection.
21
+ * null/undefined values will be dropped.
22
+ *
23
+ * Does not apply filtering/truncation.
24
+ *
25
+ * @param {string} key Name of the attribute to be stored.
26
+ * @param {string|boolean|number} value Value of the attribute to be stored.
27
+ */
28
+ addAttribute(key, value) {
29
+ const streamingValue = mapToStreamingType(value)
30
+ if (streamingValue) {
31
+ this[key] = streamingValue
32
+ return true
33
+ }
34
+
35
+ return false
36
+ }
37
+
38
+ /**
39
+ * Adds all attributes in an object to the attribute collection.
40
+ * null/undefined values will be dropped.
41
+ *
42
+ * Does not apply filtering/truncation.
43
+ *
44
+ * @param {object} [attributes]
45
+ * @param {string} [attributes.key] Name of the attribute to be stored.
46
+ * @param {string|boolean|number} [attributes.value] Value of the attribute to be stored.
47
+ */
48
+ addAttributes(attributes) {
49
+ if (!attributes) {
50
+ return
51
+ }
52
+
53
+ for (let [key, value] of Object.entries(attributes)) {
54
+ this.addAttribute(key, value)
55
+ }
56
+ }
57
+ }
58
+
59
+ module.exports = StreamingSpanAttributes
@@ -0,0 +1,98 @@
1
+ 'use strict'
2
+
3
+ const Aggregator = require('../aggregators/base-aggregator')
4
+ const StreamingSpanEvent = require('./streaming-span-event')
5
+ const NAMES = require('../metrics/names')
6
+ const logger = require('../logger').child({component: 'streaming-span-event-aggregator'})
7
+
8
+ const SEND_WARNING =
9
+ 'send() is not currently supported on streaming span event aggregator. ' +
10
+ 'This warning will not appear again this agent run.'
11
+
12
+ // TODO: this doesn't "aggregate". Perhaps we need a different terminology
13
+ // for the base-class and then this implementation can avoid the misleading language.
14
+ class StreamingSpanEventAggregator extends Aggregator {
15
+ constructor(opts, collector, metrics) {
16
+ opts = opts || {}
17
+ opts.periodMs = opts.periodMs ? opts.periodMs : 1000
18
+ opts.limit = opts.limit ? opts.limit : 10000
19
+ opts.method = opts.method || 'span_event_data'
20
+
21
+ super(opts, collector)
22
+
23
+ this.metricNames = opts.metricNames || NAMES.INFINITE_TRACING
24
+ this.stream = opts.span_streamer
25
+ this.metrics = metrics
26
+ this.started = false
27
+ this.isStream = true
28
+ }
29
+
30
+ start() {
31
+ if (this.started) {
32
+ return
33
+ }
34
+
35
+ logger.trace('StreamingSpanEventAggregator starting up')
36
+ this.stream.connect(this.runId)
37
+ this.started = true
38
+ }
39
+
40
+ stop() {
41
+ if (!this.started) {
42
+ return
43
+ }
44
+
45
+ logger.trace('StreamingSpanEventAggregator stopping')
46
+ this.stream.disconnect()
47
+ this.started = false
48
+ }
49
+
50
+ send() {
51
+ // Only log once started. This will get invoked on initial harvest
52
+ // prior to start which we'll just ignore.
53
+ if (this.started) {
54
+ logger.warnOnce(SEND_WARNING)
55
+ }
56
+
57
+ this.emit(`finished ${this.method} data send.`)
58
+
59
+ return
60
+ }
61
+
62
+ /**
63
+ * Not a payload based aggregator
64
+ *
65
+ * This is here to implement the implicit interface
66
+ */
67
+ _toPayloadSync() {
68
+ return
69
+ }
70
+
71
+ /**
72
+ * Attempts to add the given segment to the collection.
73
+ *
74
+ * @param {TraceSegment} segment - The segment to add.
75
+ * @param {string} [parentId=null] - The GUID of the parent span.
76
+ *
77
+ * @return {bool} True if the segment was added, or false if it was discarded.
78
+ */
79
+ addSegment(segment, parentId, isRoot) {
80
+ if (!this.started) {
81
+ logger.trace('Aggregator has not yet started, dropping span (%s).', segment.name)
82
+ return
83
+ }
84
+
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
+ const span = StreamingSpanEvent.fromSegment(segment, parentId, isRoot)
91
+
92
+ if (this.stream.write(span)) {
93
+ this.metrics.getOrCreateMetric(this.metricNames.SENT).incrementCallCount()
94
+ }
95
+ }
96
+ }
97
+
98
+ module.exports = StreamingSpanEventAggregator
@@ -0,0 +1,261 @@
1
+ 'use strict'
2
+
3
+ const StreamingSpanAttributes = require('./streaming-span-attributes')
4
+ const {truncate} = require('../util/byte-limit')
5
+ const Config = require('../config')
6
+
7
+ const {DESTINATIONS} = require('../config/attribute-filter')
8
+ const NAMES = require('../metrics/names')
9
+ const HTTP_LIBRARY = 'http'
10
+ const CLIENT_KIND = 'client'
11
+ const CATEGORIES = {
12
+ HTTP: 'http',
13
+ DATASTORE: 'datastore',
14
+ GENERIC: 'generic'
15
+ }
16
+
17
+ /**
18
+ * Specialized span event class for use with infinite streaming.
19
+ * Currently designed to be sent over grpc via the v1.proto definition.
20
+ *
21
+ * @private
22
+ * @class
23
+ */
24
+ class StreamingSpanEvent {
25
+ /**
26
+ * @param {*} traceId TraceId for the Span.
27
+ * @param {object} agentAttributes Initial set of agent attributes.
28
+ * Must be pre-filtered and truncated.
29
+ * @param {object} customAttributes Initial set of custom attributes.
30
+ * Must be pre-filtered and truncated.
31
+ */
32
+ constructor(traceId, agentAttributes, customAttributes) {
33
+ this._traceId = traceId
34
+
35
+ this._intrinsicAttributes = new StreamingSpanAttributes()
36
+ this._intrinsicAttributes.addAttribute('traceId', traceId)
37
+ this._intrinsicAttributes.addAttribute('type', 'Span')
38
+ this._intrinsicAttributes.addAttribute('category', CATEGORIES.GENERIC)
39
+
40
+ this._customAttributes = new StreamingSpanAttributes(customAttributes)
41
+ this._agentAttributes = new StreamingSpanAttributes(agentAttributes)
42
+ }
43
+
44
+ /**
45
+ * Add a key/value pair to the Span's instrinisics collection.
46
+ *
47
+ * @param {string} key Name of the attribute to be stored.
48
+ * @param {string|boolean|number} value Value of the attribute to be stored.
49
+ */
50
+ addIntrinsicAttribute(key, value) {
51
+ this._intrinsicAttributes.addAttribute(key, value)
52
+ }
53
+
54
+ /**
55
+ * Add a key/value pair to the Span's custom/user attributes collection.
56
+ * @param {string} key Name of the attribute to be stored.
57
+ * @param {string|boolean|number} value Value of the attribute to be stored.
58
+ * @param {boolean} [truncateExempt=false] Set to true if attribute should not be truncated.
59
+ */
60
+ addCustomAttribute(key, value, truncateExempt = false) {
61
+ const shouldKeep = this._checkFilter(key)
62
+ if (shouldKeep) {
63
+ const processedValue = truncateExempt ? value : _truncate(value)
64
+ this._customAttributes.addAttribute(key, processedValue)
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Add a key/value pair to the Span's agent attributes collection.
70
+ * @param {string} key Name of the attribute to be stored.
71
+ * @param {string|boolean|number} value Value of the attribute to be stored.
72
+ * @param {boolean} [truncateExempt=false] Set to true if attribute should not be truncated.
73
+ */
74
+ addAgentAttribute(key, value, truncateExempt = false) {
75
+ const shouldKeep = this._checkFilter(key)
76
+ if (shouldKeep) {
77
+ const processedValue = truncateExempt ? value : _truncate(value)
78
+ this._agentAttributes.addAttribute(key, processedValue)
79
+ }
80
+ }
81
+
82
+ _checkFilter(key) {
83
+ const {attributeFilter} = Config.getInstance()
84
+ const dest = attributeFilter.filterSegment(DESTINATIONS.SPAN_EVENT, key)
85
+ return dest & DESTINATIONS.SPAN_EVENT
86
+ }
87
+
88
+ toStreamingFormat() {
89
+ // Attributes are pre-formatted.
90
+ const formatted = {
91
+ trace_id: this._traceId,
92
+ intrinsics: this._intrinsicAttributes,
93
+ user_attributes: this._customAttributes,
94
+ agent_attributes: this._agentAttributes
95
+ }
96
+ return formatted
97
+ }
98
+
99
+ static fromSegment(segment, parentId = null, isRoot = false) {
100
+ const agentAttributes = segment.attributes.get(DESTINATIONS.SPAN_EVENT)
101
+ const customAttributes = segment.customAttributes.get(DESTINATIONS.SPAN_EVENT)
102
+
103
+ const transaction = segment.transaction
104
+ const traceId = transaction.traceId
105
+
106
+ let span = null
107
+ if (StreamingHttpSpanEvent.isHttpSegment(segment)) {
108
+ span = new StreamingHttpSpanEvent(traceId, agentAttributes, customAttributes)
109
+ } else if (StreamingDatastoreSpanEvent.isDatastoreSegment(segment)) {
110
+ span = new StreamingDatastoreSpanEvent(traceId, agentAttributes, customAttributes)
111
+ } else {
112
+ span = new StreamingSpanEvent(traceId, agentAttributes, customAttributes)
113
+ }
114
+
115
+ span.addIntrinsicAttribute('guid', segment.id)
116
+ span.addIntrinsicAttribute('parentId', parentId)
117
+ span.addIntrinsicAttribute('transactionId', transaction.id)
118
+ span.addIntrinsicAttribute('sampled', transaction.sampled)
119
+ span.addIntrinsicAttribute('priority', transaction.priority)
120
+ span.addIntrinsicAttribute('name', segment.name)
121
+
122
+ if (isRoot) {
123
+ span.addIntrinsicAttribute('trustedParentId', transaction.traceContext.trustedParentId)
124
+ if (transaction.traceContext.tracingVendors) {
125
+ span.addIntrinsicAttribute('tracingVendors', transaction.traceContext.tracingVendors)
126
+ }
127
+ }
128
+
129
+ // Only set this if it will be `true`. Must be `null` otherwise.
130
+ if (transaction.baseSegment === segment) {
131
+ span.addIntrinsicAttribute('nr.entryPoint', true)
132
+ }
133
+
134
+ // Timestamp in milliseconds, duration in seconds. Yay consistency!
135
+ span.addIntrinsicAttribute('timestamp', segment.timer.start)
136
+ span.addIntrinsicAttribute('duration', segment.timer.getDurationInMillis() / 1000)
137
+
138
+ return span
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Specialized span event class for external requests for use with infinite streaming.
144
+ * Currently designed to be sent over grpc via the v1.proto definition.
145
+ *
146
+ * @private
147
+ * @class
148
+ */
149
+ class StreamingHttpSpanEvent extends StreamingSpanEvent {
150
+ /**
151
+ * @param {*} traceId TraceId for the Span.
152
+ * @param {object} agentAttributes Initial set of agent attributes.
153
+ * Must be pre-filtered and truncated.
154
+ * @param {object} customAttributes Initial set of custom attributes.
155
+ * Must be pre-filtered and truncated.
156
+ */
157
+ constructor(traceId, agentAttributes, customAttributes) {
158
+ super(traceId, agentAttributes, customAttributes)
159
+
160
+ this.addIntrinsicAttribute('category', CATEGORIES.HTTP)
161
+ this.addIntrinsicAttribute('component', agentAttributes.library || HTTP_LIBRARY)
162
+ this.addIntrinsicAttribute('span.kind', CLIENT_KIND)
163
+
164
+ if (agentAttributes.library) {
165
+ agentAttributes.library = null
166
+ }
167
+
168
+ if (agentAttributes.url) {
169
+ this.addAgentAttribute('http.url', agentAttributes.url)
170
+ agentAttributes.url = null
171
+ }
172
+
173
+ if (agentAttributes.procedure) {
174
+ this.addAgentAttribute('http.method', agentAttributes.procedure)
175
+ agentAttributes.procedure = null
176
+ }
177
+ }
178
+
179
+ static isHttpSegment(segment) {
180
+ return segment.name.startsWith(NAMES.EXTERNAL.PREFIX)
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Specialized span event class for datastore operations and queries for use with
186
+ * infinite streaming.
187
+ * Currently designed to be sent over grpc via the v1.proto definition.
188
+ *
189
+ * @private
190
+ * @class.
191
+ */
192
+ class StreamingDatastoreSpanEvent extends StreamingSpanEvent {
193
+ /**
194
+ * @param {*} traceId TraceId for the Span.
195
+ * @param {object} agentAttributes Initial set of agent attributes.
196
+ * Must be pre-filtered and truncated.
197
+ * @param {object} customAttributes Initial set of custom attributes.
198
+ * Must be pre-filtered and truncated.
199
+ */
200
+ constructor(traceId, agentAttributes, customAttributes) {
201
+ super(traceId, agentAttributes, customAttributes)
202
+
203
+ this.addIntrinsicAttribute('category', CATEGORIES.DATASTORE)
204
+ this.addIntrinsicAttribute('span.kind', CLIENT_KIND)
205
+
206
+ if (agentAttributes.product) {
207
+ this.addIntrinsicAttribute('component', agentAttributes.product)
208
+ agentAttributes.product = null
209
+ }
210
+
211
+ if (agentAttributes.collection) {
212
+ this.addAgentAttribute('db.collection', agentAttributes.collection)
213
+ agentAttributes.collection = null
214
+ }
215
+
216
+ if (agentAttributes.sql || agentAttributes.sql_obfuscated) {
217
+ let sql = null
218
+ if (agentAttributes.sql_obfuscated) {
219
+ sql = _truncate(agentAttributes.sql_obfuscated)
220
+ agentAttributes.sql_obfuscated = null
221
+ } else if (agentAttributes.sql) {
222
+ sql = _truncate(agentAttributes.sql)
223
+ agentAttributes.sql = null
224
+ }
225
+
226
+ // Flag as exempt from normal attribute truncation
227
+ this.addAgentAttribute('db.statement', sql, true)
228
+ }
229
+
230
+ if (agentAttributes.database_name) {
231
+ this.addAgentAttribute('db.instance', agentAttributes.database_name)
232
+ agentAttributes.database_name = null
233
+ }
234
+
235
+ if (agentAttributes.host) {
236
+ this.addAgentAttribute('peer.hostname', agentAttributes.host)
237
+
238
+ if (agentAttributes.port_path_or_id) {
239
+ const address = `${agentAttributes.host}:${agentAttributes.port_path_or_id}`
240
+ this.addAgentAttribute('peer.address', address)
241
+ agentAttributes.port_path_or_id = null
242
+ }
243
+
244
+ agentAttributes.host = null
245
+ }
246
+ }
247
+
248
+ static isDatastoreSegment(segment) {
249
+ return segment.name.startsWith(NAMES.DB.PREFIX)
250
+ }
251
+ }
252
+
253
+ function _truncate(val) {
254
+ let truncated = truncate(val, 1997)
255
+ if (truncated !== val) {
256
+ truncated += '...'
257
+ }
258
+ return truncated
259
+ }
260
+
261
+ module.exports = StreamingSpanEvent
@@ -67,28 +67,31 @@ Trace.prototype.end = function end() {
67
67
  * Iterates over the trace tree and generates a span event for each segment.
68
68
  */
69
69
  Trace.prototype.generateSpanEvents = function generateSpanEvents() {
70
- var config = this.transaction.agent.config
70
+ const config = this.transaction.agent.config
71
+
72
+ const infiniteTracingConfigured = Boolean(config.infinite_tracing.trace_observer.host)
71
73
 
72
74
  if (
73
- this.transaction.sampled &&
75
+ (infiniteTracingConfigured || this.transaction.sampled) &&
74
76
  config.span_events.enabled &&
75
77
  config.distributed_tracing.enabled
76
78
  ) {
77
- var toProcess = []
79
+ let toProcess = []
80
+
78
81
  // Root segment does not become a span, so we need to process it separately.
79
82
  const spanAggregator = this.transaction.agent.spanEventAggregator
83
+
80
84
  const children = this.root.getChildren()
81
85
  for (let i = 0; i < children.length; ++i) {
82
86
  toProcess.push(new DTTraceNode(children[i], this.transaction.parentSpanId, true))
83
87
  }
84
88
 
85
89
  while (toProcess.length) {
86
- var segmentInfo = toProcess.pop()
87
- var segment = segmentInfo.segment
90
+ let segmentInfo = toProcess.pop()
91
+ let segment = segmentInfo.segment
88
92
 
89
- // Even though at some point we might want to stop adding events
90
- // because all the priorities should be the same, we need to count
91
- // the spans as seen.
93
+ // Even though at some point we might want to stop adding events because all the priorities
94
+ // should be the same, we need to count the spans as seen.
92
95
  spanAggregator.addSegment(segment, segmentInfo.parentId, segmentInfo.isRoot)
93
96
 
94
97
  const nodes = segment.getChildren()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "6.5.0",
3
+ "version": "6.6.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "contributors": [
@@ -127,7 +127,9 @@
127
127
  "newrelic-naming-rules": "./bin/test-naming-rules.js"
128
128
  },
129
129
  "dependencies": {
130
- "@newrelic/aws-sdk": "^1.1.2",
130
+ "@grpc/grpc-js": "^0.6.15",
131
+ "@grpc/proto-loader": "^0.5.3",
132
+ "@newrelic/aws-sdk": "^1.1.1",
131
133
  "@newrelic/koa": "^3.0.0",
132
134
  "@newrelic/superagent": "^2.0.1",
133
135
  "@tyriar/fibonacci-heap": "^2.0.7",
@@ -171,7 +173,7 @@
171
173
  "rimraf": "^2.6.3",
172
174
  "should": "*",
173
175
  "sinon": "^4.5.0",
174
- "tap": "^14.10.1",
176
+ "tap": "^14.10.6",
175
177
  "temp": "^0.8.1",
176
178
  "through": "^2.3.6",
177
179
  "when": "*"