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,344 @@
1
+ 'use strict'
2
+
3
+ const protoLoader = require('@grpc/proto-loader')
4
+ const grpc = require('../proxy/grpc')
5
+ const logger = require('../logger').child({component: 'grpc_connection'})
6
+ const EventEmitter = require('events')
7
+ const NAMES = require('../metrics/names')
8
+ const util = require('util')
9
+
10
+ const connectionStates = require('./connection/states')
11
+
12
+ const protoOptions = {keepCase: true,
13
+ longs: String,
14
+ enums: String,
15
+ defaults: true,
16
+ oneofs: true
17
+ }
18
+
19
+ const pathProtoDefinition = __dirname +
20
+ '../../../lib/grpc/endpoints/infinite-tracing/v1.proto'
21
+
22
+ const defaultBackoffOpts = {
23
+ initialSeconds: 0,
24
+ seconds:15
25
+ }
26
+
27
+ /**
28
+ * Class for managing the GRPC connection
29
+ *
30
+ * Both @grpc/grpc-js and grpc will manage the http2 connections
31
+ * for us -- this class manages the _stream_ connection logic.
32
+ *
33
+ * Will emit events based on the connectionStates (see above
34
+ */
35
+ class GrpcConnection extends EventEmitter {
36
+ /**
37
+ * GrpcConnection constructor
38
+ *
39
+ * Standard property setting/initilization, and sets an initial
40
+ * connection state of disconnected
41
+ *
42
+ * @param {Object} traceObserverConfig config item config.infinite_tracing.trace_observer
43
+ * @param {MetricAggregator} metrics metric aggregator, for supportability metrics
44
+ * @param {Object} backoffOpts allows injecting specific backoff times
45
+ */
46
+ constructor(traceObserverConfig, metrics, backoffOpts = defaultBackoffOpts) {
47
+ super()
48
+
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
+ }
59
+
60
+ // initial "stream connection" has a 0 second backoff, unless
61
+ // different values are injected via _backoffOpts
62
+ this._streamBackoffSeconds = this._backoffOpts.initialSeconds
63
+ this._metrics = metrics
64
+ this._setState(connectionStates.disconnected)
65
+
66
+ this._endpoint = this.getTraceObserverEndpoint(traceObserverConfig)
67
+ this._licensekey = null
68
+ this._runId = null
69
+
70
+ this.stream = null
71
+ }
72
+
73
+ /**
74
+ * Sets connection details
75
+ *
76
+ * Allows setting of connection details _after_ object constructions
77
+ * but before the actual connection.
78
+ *
79
+ * @param {string} endpoint the GRPC server's endpoint
80
+ * @param {string} license_key the agent license key
81
+ * @param {string} run_id the current agent run id (also called agent run token)
82
+ * @param {string} [rootCerts] string of root (ca) certificates to attach to the connection.
83
+ */
84
+ setConnectionDetails(license_key, run_id, rootCerts) {
85
+ this._licenseKey = license_key
86
+ this._runId = run_id
87
+ this._rootCerts = rootCerts
88
+
89
+ return this
90
+ }
91
+
92
+ getTraceObserverEndpoint(traceObserverConfig) {
93
+ return `${traceObserverConfig.host}:${traceObserverConfig.port}`
94
+ }
95
+
96
+ /**
97
+ * Sets the connection state
98
+ *
99
+ * Used to indicate a transition from one connection state to
100
+ * the next. Also responsible for emitting the connect state event
101
+ *
102
+ * @param {int} state The connection state (See connectionStates above)
103
+ * @param {ClientDuplexStreamImpl} state The GRPC stream, when defined
104
+ */
105
+ _setState(state, stream = null) {
106
+ this._state = state
107
+ this.emit(connectionStates[state], stream)
108
+ }
109
+
110
+ /**
111
+ * Start the Connection
112
+ *
113
+ * Public Entry point -- initiates a connection
114
+ */
115
+ connectSpans() {
116
+ if (this._state !== connectionStates.disconnected) {
117
+ return
118
+ }
119
+
120
+ 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)
126
+
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)
130
+
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
150
+ }
151
+
152
+ /**
153
+ * End the current stream and set state to disconnected.
154
+ *
155
+ * No more data can be sent until connected again.
156
+ */
157
+ disconnect() {
158
+ if (this._state === connectionStates.disconnected) {
159
+ return
160
+ }
161
+
162
+ this._disconnectWithoutReconnect()
163
+ }
164
+
165
+ /**
166
+ * Calculates backoff seconds
167
+ */
168
+ _getBackoffSeconds() {
169
+ return this._streamBackoffSeconds
170
+ }
171
+
172
+ /**
173
+ * Method returns GRPC metadata for initial connection
174
+ *
175
+ * @param {string} license_key
176
+ * @param {string} run_id
177
+ */
178
+ _getMetadata(license_key, run_id, env) {
179
+ const metadata = new grpc.Metadata()
180
+ metadata.add('license_key', license_key)
181
+ metadata.add('agent_run_token', run_id)
182
+
183
+ // check environment variables for testing parameters and
184
+ // pass to server via meta-data.
185
+ const flaky = parseInt(env.NEWRELIC_GRPCCONNECTION_METADATA_FLAKY, 10)
186
+ const delay = parseInt(env.NEWRELIC_GRPCCONNECTION_METADATA_DELAY, 10)
187
+ if (flaky) {
188
+ logger.trace('Adding flaky metadata: %s', flaky)
189
+ metadata.add('flaky', flaky)
190
+ }
191
+ if (delay) {
192
+ logger.trace('Adding delay metadata: %s', delay)
193
+ metadata.add('delay',delay)
194
+ }
195
+
196
+ return metadata
197
+ }
198
+
199
+ /**
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.
205
+ */
206
+ _reconnect() {
207
+ this._disconnect()
208
+ this.connectSpans()
209
+ }
210
+
211
+ _disconnect() {
212
+ if (this.stream) {
213
+ this.stream.removeAllListeners()
214
+
215
+ // Indicates to server we are done.
216
+ // Server officially closes the stream.
217
+ this.stream.end()
218
+ this.stream = null
219
+ }
220
+
221
+ this._setState(connectionStates.disconnected)
222
+ }
223
+
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
+ /**
236
+ * Central location to setup stream observers
237
+ *
238
+ * Events from the GRPC stream (a ClientDuplexStreamImpl) are the main way
239
+ * we communicate with the GRPC server.
240
+ *
241
+ * @param {ClientDuplexStreamImpl} stream
242
+ */
243
+ _setupSpanStreamObservers(stream) {
244
+ // listen for responses from server and log
245
+ if (logger.traceEnabled()) {
246
+ stream.on('data', function data(response) {
247
+ logger.trace("grpc span response stream: %s", JSON.stringify(response))
248
+ })
249
+ }
250
+
251
+ // listen for status that indicate stream has ended,
252
+ // and we need to disconnect
253
+ stream.on('status', (grpcStatus) => {
254
+ logger.trace('GRPC Status Received [%s]: %s', grpcStatus.code, grpcStatus.details)
255
+ const grpcStatusName =
256
+ grpc.status[grpcStatus.code] ? grpc.status[grpcStatus.code] : 'UNKNOWN'
257
+
258
+ if (grpc.status[grpc.status.UNIMPLEMENTED] === grpcStatusName) {
259
+ this._metrics.getOrCreateMetric(
260
+ NAMES.INFINITE_TRACING.SPAN_RESPONSE_GRPC_UNIMPLEMENTED
261
+ ).incrementCallCount()
262
+
263
+ // per the spec, An UNIMPLEMENTED status code from gRPC indicates
264
+ // that the versioned Trace Observer is no longer available. Agents
265
+ // MUST NOT attempt to reconnect in this case
266
+ this._disconnectWithoutReconnect()
267
+ } 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
+ }
277
+
278
+ this._reconnect()
279
+ }
280
+ })
281
+
282
+ // if we don't listen for the errors they'll bubble
283
+ // up and crash the application
284
+ stream.on('error', (err) => {
285
+ this._metrics.getOrCreateMetric(NAMES.INFINITE_TRACING.SPAN_RESPONSE_ERROR)
286
+ .incrementCallCount()
287
+
288
+ logger.trace('span stream error. Code: [%s]: %s',err.code, err.details)
289
+ })
290
+ }
291
+
292
+ /**
293
+ * Creates the GRPC credentials needed
294
+ */
295
+ _generateCredentials(grpcApi) {
296
+ let certBuffer = null
297
+
298
+ // Current settable value for testing. If allowed to be overriden via
299
+ // configuration, this should be removed in place of setting
300
+ // this._rootCerts from config via normal configuration precedence.
301
+ const envTestCerts = process.env.NEWRELIC_GRPCCONNECTION_CA
302
+ const rootCerts = this._rootCerts || envTestCerts
303
+ if (rootCerts) {
304
+ logger.debug('Infinite tracing root certificates found to attach to requests.')
305
+ try {
306
+ certBuffer = Buffer.from(rootCerts, 'utf-8')
307
+ } catch (err) {
308
+ logger.warn('Failed to create buffer from rootCerts, proceeding without.', err)
309
+ }
310
+ }
311
+
312
+ // null/undefined ca treated same as calling createSsl()
313
+ return grpcApi.credentials.createSsl(certBuffer)
314
+ }
315
+
316
+ /**
317
+ * Internal/private method for connection
318
+ *
319
+ * Contains the actual logic that connects to the GRPC service.
320
+ * "Connection" can be a somewhat misleading term here. This method
321
+ * invokes the "recordSpan" remote proceduce call. Behind the scenes
322
+ * this makes an http2 request with the metadata, and then returns
323
+ * a stream for further writing.
324
+ */
325
+ _connectSpans(endpoint, license_key, run_id) {
326
+ const packageDefinition = protoLoader.loadSync(pathProtoDefinition, protoOptions)
327
+
328
+ const serviceDefinition = grpc.loadPackageDefinition(
329
+ packageDefinition
330
+ ).com.newrelic.trace.v1
331
+
332
+ const credentials = this._generateCredentials(grpc)
333
+ const client = new serviceDefinition.IngestService(
334
+ endpoint,
335
+ credentials
336
+ )
337
+ const metadata = this._getMetadata(license_key, run_id, process.env)
338
+ const stream = client.recordSpan(metadata)
339
+ this._setupSpanStreamObservers(stream)
340
+
341
+ return stream
342
+ }
343
+ }
344
+ module.exports = GrpcConnection
@@ -0,0 +1,29 @@
1
+ syntax = "proto3";
2
+
3
+ package com.newrelic.trace.v1;
4
+
5
+ service IngestService {
6
+ // Accepts a stream of Span messages, and returns an irregular stream of
7
+ // RecordStatus messages.
8
+ rpc RecordSpan(stream Span) returns (stream RecordStatus) {}
9
+ }
10
+
11
+ message Span {
12
+ string trace_id = 1;
13
+ map<string, AttributeValue> intrinsics = 2;
14
+ map<string, AttributeValue> user_attributes = 3;
15
+ map<string, AttributeValue> agent_attributes = 4;
16
+ }
17
+
18
+ message AttributeValue {
19
+ oneof value {
20
+ string string_value = 1;
21
+ bool bool_value = 2;
22
+ int64 int_value = 3;
23
+ double double_value = 4;
24
+ }
25
+ }
26
+
27
+ message RecordStatus {
28
+ uint64 messages_seen = 1;
29
+ }
@@ -16,7 +16,8 @@ const SUPPORTABILITY = {
16
16
  DEPENDENCIES: 'Supportability/InstalledDependencies',
17
17
  NODEJS: 'Supportability/Nodejs',
18
18
  REGISTRATION: 'Supportability/Registration',
19
- EVENT_HARVEST: 'Supportability/EventHarvest'
19
+ EVENT_HARVEST: 'Supportability/EventHarvest',
20
+ INFINITE_TRACING: 'Supportability/InfiniteTracing'
20
21
  }
21
22
 
22
23
  const ERRORS = {
@@ -231,6 +232,15 @@ const SPAN_EVENTS = {
231
232
  DROPPED: SUPPORTABILITY.PREFIX + 'SpanEvent/Discarded'
232
233
  }
233
234
 
235
+ const INFINITE_TRACING = {
236
+ SEEN: SUPPORTABILITY.INFINITE_TRACING + '/Span/Seen',
237
+ SENT: SUPPORTABILITY.INFINITE_TRACING + '/Span/Sent',
238
+ SPAN_RESPONSE_ERROR: SUPPORTABILITY.INFINITE_TRACING + '/Span/Response/Error',
239
+ SPAN_RESPONSE_GRPC_UNIMPLEMENTED: SUPPORTABILITY.INFINITE_TRACING + '/Span/gRPC/UNIMPLEMENTED',
240
+ SPAN_RESPONSE_GRPC_STATUS: SUPPORTABILITY.INFINITE_TRACING + '/Span/gRPC/%s',
241
+ DRAIN_DURATION: SUPPORTABILITY.INFINITE_TRACING + '/Drain/Duration'
242
+ }
243
+
234
244
  module.exports = {
235
245
  ACTION_DELIMITER: '/',
236
246
  ALL: ALL,
@@ -253,6 +263,7 @@ module.exports = {
253
263
  GC: GC,
254
264
  HAPI: HAPI,
255
265
  HTTP: 'HttpDispatcher',
266
+ INFINITE_TRACING: INFINITE_TRACING,
256
267
  LOOP: LOOP,
257
268
  MEMCACHE: MEMCACHE,
258
269
  MEMORY: MEMORY,
@@ -0,0 +1,15 @@
1
+ 'use strict'
2
+ class ProxyGrpc {
3
+ constructor(grpcLibrary = '@grpc/grpc-js') {
4
+ this.library = require(grpcLibrary)
5
+
6
+ // add methods or objets from base grpc class that we need as needed
7
+ this.credentials = this.library.credentials
8
+ this.Metadata = this.library.Metadata
9
+ this.loadPackageDefinition = this.library.loadPackageDefinition
10
+ this.status = this.library.status
11
+ this.Server = this.library.Server
12
+ this.ServerCredentials = this.library.ServerCredentials
13
+ }
14
+ }
15
+ module.exports = (new ProxyGrpc)
@@ -174,6 +174,8 @@ class AwsLambda {
174
174
 
175
175
  shim.agent.setLambdaArn(context.invokedFunctionArn)
176
176
 
177
+ shim.agent.setLambdaFunctionVersion(context.functionVersion)
178
+
177
179
  segment.start()
178
180
 
179
181
  let res
@@ -0,0 +1,112 @@
1
+ 'use strict'
2
+
3
+ const psemver = require('../util/process-version')
4
+ const logger = require('../logger')
5
+ const SpanEventAggregator = require('./span-event-aggregator')
6
+ const StreamingSpanEventAggregator = require('./streaming-span-event-aggregator')
7
+
8
+ function createSpanEventAggregator(config, collector, metrics) {
9
+ let shouldCreateStreaming = false
10
+ if (config.infinite_tracing.trace_observer.host) {
11
+ // TODO: ideally this validation and configuration clearing would happen
12
+ // in the config. Since we don't currently have a way to generate
13
+ // support metrics in the config, keeping this related logic together here.
14
+ // If logic happened prior, could merely check for existance of trace_observer.host.
15
+ shouldCreateStreaming = validateInfiniteTracing(config.infinite_tracing.trace_observer)
16
+
17
+ if (!shouldCreateStreaming) {
18
+ // Explicitly disable for any downstream consumers
19
+ config.infinite_tracing.trace_observer.host = ''
20
+ config.infinite_tracing.trace_observer.port = ''
21
+ }
22
+ }
23
+
24
+ if (shouldCreateStreaming) {
25
+ return createStreamingAggregator(config, collector, metrics)
26
+ }
27
+
28
+ return createStandardAggregator(config, collector, metrics)
29
+ }
30
+
31
+ function createStreamingAggregator(config, collector, metrics) {
32
+ logger.trace('Creating streaming span event aggregator for infinite tracing.')
33
+
34
+ // loading the class here to ensure its behind a feature flag
35
+ // and won't trigger a grpc load in node 8
36
+ const GrpcConnection = require('../grpc/connection')
37
+ const connection = new GrpcConnection(config.infinite_tracing.trace_observer, metrics)
38
+ const SpanStreamer = require('./span-streamer')
39
+ const spanStreamer = new SpanStreamer(
40
+ config.license_key,
41
+ connection,
42
+ metrics
43
+ )
44
+
45
+ const opts = {
46
+ periodMs: 1000,
47
+ limit: 50000,
48
+ span_streamer: spanStreamer
49
+ }
50
+
51
+ const aggregator = new StreamingSpanEventAggregator(opts, collector, metrics)
52
+
53
+ return aggregator
54
+ }
55
+
56
+ function createStandardAggregator(config, collector, metrics) {
57
+ logger.trace('Creating standard span event aggregator.')
58
+
59
+ const opts = {
60
+ periodMs: config.event_harvest_config.report_period_ms,
61
+ limit: config.event_harvest_config.harvest_limits.span_event_data
62
+ }
63
+
64
+ const aggregator = new SpanEventAggregator(opts, collector, metrics)
65
+ return aggregator
66
+ }
67
+
68
+ function validateInfiniteTracing(trace_observer) {
69
+ if (!psemver.satisfies('>=10.10.0')) {
70
+ logger.warn(
71
+ 'Infinite tracing disabled: this version of Node is not supported (must be >=10.10.0)'
72
+ )
73
+ return false
74
+ }
75
+
76
+ trace_observer.host = trace_observer.host.trim()
77
+
78
+ if (!validateHostName(trace_observer.host)) {
79
+ logger.warn('Infinite tracing disabled: invalid infinite_tracing.trace_observer.host value')
80
+
81
+ return false
82
+ }
83
+
84
+ if (typeof trace_observer.port !== 'string') {
85
+ trace_observer.port = String(trace_observer.port)
86
+ }
87
+
88
+ trace_observer.port = trace_observer.port.trim()
89
+
90
+ if (!validatePortValue(trace_observer.port)) {
91
+ logger.warn('Infinite tracing disabled: invalid infinite_tracing.trace_observer.port value')
92
+
93
+ return false
94
+ }
95
+
96
+ return true
97
+ }
98
+
99
+ function validateHostName(host) {
100
+ // Regular expression for validating a hostname
101
+ const hostReg = /(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)/
102
+
103
+ return hostReg.test(host)
104
+ }
105
+
106
+ function validatePortValue(port) {
107
+ if (port.length === 0) return false
108
+
109
+ return !isNaN(port)
110
+ }
111
+
112
+ module.exports = createSpanEventAggregator
@@ -0,0 +1,44 @@
1
+ 'use strict'
2
+
3
+ const STRING_TYPE = 'string_value'
4
+ const BOOL_TYPE = 'bool_value'
5
+ const INT_TYPE = 'int_value'
6
+ const DOUBLE_TYPE = 'double_value'
7
+
8
+ function mapToStreamingType(value) {
9
+ if (value === null || value === undefined) {
10
+ return
11
+ }
12
+
13
+ const valueType = typeof value
14
+
15
+ let protoTypeString = null
16
+ switch (valueType) {
17
+ case 'string': {
18
+ protoTypeString = STRING_TYPE
19
+ break
20
+ }
21
+ case 'boolean': {
22
+ protoTypeString = BOOL_TYPE
23
+ break
24
+ }
25
+ case 'number': {
26
+ const isInteger = Number.isInteger(value)
27
+ protoTypeString = isInteger ? INT_TYPE : DOUBLE_TYPE
28
+ break
29
+ }
30
+ default: {
31
+ protoTypeString = null
32
+ }
33
+ }
34
+
35
+ if (protoTypeString) {
36
+ return {
37
+ [protoTypeString]: value
38
+ }
39
+ }
40
+
41
+ return
42
+ }
43
+
44
+ module.exports = mapToStreamingType
@@ -35,6 +35,11 @@ class SpanEventAggregator extends EventAggregator {
35
35
  return [this.runId, metrics, eventData]
36
36
  }
37
37
 
38
+ start() {
39
+ logger.debug('starting SpanEventAggregator')
40
+ return super.start()
41
+ }
42
+
38
43
  send() {
39
44
  if (spanLogger.traceEnabled()) {
40
45
  spanLogger.trace({