newrelic 10.0.0 → 10.1.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,20 @@
1
+ ### v10.1.0 (2023-05-04)
2
+
3
+ * Added batching and compression to infinite tracing.
4
+ * These options are on by default.
5
+ * To restore uncompressed spans set `config.infinite_tracing.compression` to `false`.
6
+ * To send single spans vs batches set `config.infinite_tracing.batching` to `false`.
7
+ * The environment variables for these new configurations are: `NEW_RELIC_INFINITE_TRACING_BATCHING` and `NEW_RELIC_INFINITE_TRACING_COMPRESSION`.
8
+
9
+ * Added support to record Nest.js error stack traces.
10
+ * Nest.js is officially supported via underlying instrumentation of express or fastify.
11
+
12
+ * Added job to create a release notes PR in the post-release workflow.
13
+
14
+ * Removed request library and updated helper to use http/https to make requests in tests.
15
+
16
+ * Reduced cognitive complexity in lib/serverless/aws-lambda.js
17
+
1
18
  ### v10.0.0 (2023-04-19)
2
19
 
3
20
  * **BREAKING** - Updated the default of `config.transaction_tracer.record_sql` from `off` to `obfuscated`. This means that sql statements will be captured but obfuscated.
@@ -62,7 +62,6 @@ code, the source code can be found at [https://github.com/newrelic/node-newrelic
62
62
  * [prettier](#prettier)
63
63
  * [proxyquire](#proxyquire)
64
64
  * [q](#q)
65
- * [request](#request)
66
65
  * [rimraf](#rimraf)
67
66
  * [should](#should)
68
67
  * [sinon](#sinon)
@@ -9165,68 +9164,6 @@ IN THE SOFTWARE.
9165
9164
 
9166
9165
  ```
9167
9166
 
9168
- ### request
9169
-
9170
- This product includes source derived from [request](https://github.com/request/request) ([v2.88.2](https://github.com/request/request/tree/v2.88.2)), distributed under the [Apache-2.0 License](https://github.com/request/request/blob/v2.88.2/LICENSE):
9171
-
9172
- ```
9173
- Apache License
9174
-
9175
- Version 2.0, January 2004
9176
-
9177
- http://www.apache.org/licenses/
9178
-
9179
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
9180
-
9181
- 1. Definitions.
9182
-
9183
- "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
9184
-
9185
- "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
9186
-
9187
- "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
9188
-
9189
- "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
9190
-
9191
- "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
9192
-
9193
- "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
9194
-
9195
- "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
9196
-
9197
- "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
9198
-
9199
- "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
9200
-
9201
- "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
9202
-
9203
- 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
9204
-
9205
- 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
9206
-
9207
- 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
9208
-
9209
- You must give any other recipients of the Work or Derivative Works a copy of this License; and
9210
-
9211
- You must cause any modified files to carry prominent notices stating that You changed the files; and
9212
-
9213
- You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
9214
-
9215
- If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
9216
-
9217
- 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
9218
-
9219
- 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
9220
-
9221
- 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
9222
-
9223
- 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9224
-
9225
- 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
9226
-
9227
- END OF TERMS AND CONDITIONS
9228
- ```
9229
-
9230
9167
  ### rimraf
9231
9168
 
9232
9169
  This product includes source derived from [rimraf](https://github.com/isaacs/rimraf) ([v2.7.1](https://github.com/isaacs/rimraf/tree/v2.7.1)), distributed under the [ISC License](https://github.com/isaacs/rimraf/blob/v2.7.1/LICENSE):
@@ -23,10 +23,10 @@ class ServerlessCollector {
23
23
  * Constructs a new serverless collector instance with the give agent.
24
24
  *
25
25
  * @class
26
- * @param pipePath
27
26
  * @classdesc
28
27
  * A helper class for wrapping modules with segments
29
28
  * @param {Agent} agent - The agent this collector will use
29
+ * @param {string} pipePath path of the named pipe to the Lambda extension, if it's enabled
30
30
  */
31
31
  constructor(agent, pipePath) {
32
32
  this._agent = agent
@@ -45,7 +45,7 @@ class ServerlessCollector {
45
45
  /**
46
46
  * Sets the ARN to be sent up in the metadata.
47
47
  *
48
- * @param arn
48
+ * @param {string} arn Amazon Resource Name of the function
49
49
  */
50
50
  setLambdaArn(arn) {
51
51
  this.metadata.arn = arn
@@ -54,7 +54,7 @@ class ServerlessCollector {
54
54
  /**
55
55
  * Sets the function_version to be sent up in the metadata.
56
56
  *
57
- * @param function_version
57
+ * @param {string} function_version version indicator for Lambda function
58
58
  */
59
59
  setLambdaFunctionVersion(function_version) {
60
60
  this.metadata.function_version = function_version
@@ -84,7 +84,7 @@ class ServerlessCollector {
84
84
  /**
85
85
  * There is nothing to actually restart for serverless, so we do nothing.
86
86
  *
87
- * @param cb
87
+ * @param {Function} cb callback function, if any
88
88
  */
89
89
  restart(cb) {
90
90
  setImmediate(cb, null, CollectorResponse.success(null))
@@ -216,6 +216,7 @@ class ServerlessCollector {
216
216
  * Constructs, serializes, and prints the final consolidated payload to stdout.
217
217
  *
218
218
  * @param {Function} cb The callback to invoke when finished.
219
+ * @returns {boolean} indicating if callback was defined and successfully executed
219
220
  */
220
221
  flushPayload(cb) {
221
222
  if (!this.enabled) {
@@ -270,7 +271,8 @@ class ServerlessCollector {
270
271
  /**
271
272
  * Writes payload to pipe
272
273
  *
273
- * @param payload
274
+ * @param {string} payload serialized stringified-JSON payload to flush
275
+ * @returns {boolean} whether or not flush was successful
274
276
  */
275
277
  flushToPipeSync(payload) {
276
278
  try {
@@ -1122,6 +1122,14 @@ defaultConfig.definition = () => ({
1122
1122
  formatter: int,
1123
1123
  default: 10000
1124
1124
  }
1125
+ },
1126
+ batching: {
1127
+ formatter: boolean,
1128
+ default: true
1129
+ },
1130
+ compression: {
1131
+ formatter: boolean,
1132
+ default: true
1125
1133
  }
1126
1134
  },
1127
1135
  /**
@@ -41,15 +41,17 @@ class GrpcConnection extends EventEmitter {
41
41
  * Standard property setting/initialization, and sets an initial
42
42
  * connection state of disconnected
43
43
  *
44
- * @param {object} traceObserverConfig config item config.infinite_tracing.trace_observer
44
+ * @param {object} infiniteTracingConfig config item config.infinite_tracing
45
45
  * @param {MetricAggregator} metrics metric aggregator, for supportability metrics
46
46
  * @param {number} [reconnectDelayMs=15000] number of milliseconds to wait before reconnecting
47
47
  * for error states that require a reconnect delay.
48
48
  */
49
- constructor(traceObserverConfig, metrics, reconnectDelayMs) {
49
+ constructor(infiniteTracingConfig, metrics, reconnectDelayMs = DEFAULT_RECONNECT_DELAY_MS) {
50
50
  super()
51
+ this._compression = infiniteTracingConfig.compression
52
+ this._method = infiniteTracingConfig.batching ? 'recordSpanBatch' : 'recordSpan'
51
53
 
52
- this._reconnectDelayMs = reconnectDelayMs || DEFAULT_RECONNECT_DELAY_MS
54
+ this._reconnectDelayMs = reconnectDelayMs
53
55
 
54
56
  this._metrics = metrics
55
57
  this._setState(connectionStates.disconnected)
@@ -58,6 +60,7 @@ class GrpcConnection extends EventEmitter {
58
60
  this._runId = null
59
61
  this._requestHeadersMap = null
60
62
 
63
+ const traceObserverConfig = infiniteTracingConfig.trace_observer
61
64
  this._endpoint = this.getTraceObserverEndpoint(traceObserverConfig)
62
65
 
63
66
  this._client = null
@@ -70,18 +73,17 @@ class GrpcConnection extends EventEmitter {
70
73
  * Allows setting of connection details _after_ object constructions
71
74
  * but before the actual connection.
72
75
  *
73
- * @param {string} endpoint the GRPC server's endpoint
74
76
  * @param {string} licenseKey the agent license key
75
77
  * @param {string} runId the current agent run id (also called agent run token)
76
78
  * @param {object} requestHeadersMap request headers map received from server connect.
77
79
  * @param {string} [rootCerts] string of root (ca) certificates to attach to the connection.
80
+ * @returns {GrpcConnection} the instance of grpc connection
78
81
  */
79
82
  setConnectionDetails(licenseKey, runId, requestHeadersMap, rootCerts) {
80
83
  this._licenseKey = licenseKey
81
84
  this._runId = runId
82
85
  this._requestHeadersMap = requestHeadersMap
83
86
  this._rootCerts = rootCerts
84
-
85
87
  return this
86
88
  }
87
89
 
@@ -95,9 +97,8 @@ class GrpcConnection extends EventEmitter {
95
97
  * Used to indicate a transition from one connection state to
96
98
  * the next. Also responsible for emitting the connect state event
97
99
  *
98
- * @param {int} state The connection state (See connectionStates above)
99
- * @param {ClientDuplexStreamImpl} state The GRPC stream, when defined
100
- * @param stream
100
+ * @param {number} state The connection state (See connectionStates above)
101
+ * @param {object} stream duplex stream
101
102
  */
102
103
  _setState(state, stream = null) {
103
104
  this._state = state
@@ -145,10 +146,11 @@ class GrpcConnection extends EventEmitter {
145
146
  /**
146
147
  * Method returns GRPC metadata for initial connection
147
148
  *
148
- * @param {string} licenseKey
149
- * @param {string} runId
150
- * @param requestHeadersMap
151
- * @param env
149
+ * @param {string} licenseKey agent key
150
+ * @param {string} runId agent run id
151
+ * @param {object} requestHeadersMap map of request headers to include
152
+ * @param {object} env process.env
153
+ * @returns {object} grpc metadata
152
154
  */
153
155
  _getMetadata(licenseKey, runId, requestHeadersMap, env) {
154
156
  const metadata = new grpc.Metadata()
@@ -172,8 +174,8 @@ class GrpcConnection extends EventEmitter {
172
174
  * Adds test metadata used to simulate connectivity issues
173
175
  * when appropriate env vars are set
174
176
  *
175
- * @param {Metadata} metadata
176
- * @param env
177
+ * @param {object} metadata metadata to set
178
+ * @param {object} env process.env
177
179
  */
178
180
  _setTestMetadata(metadata, env) {
179
181
  for (const [key, envVar] of Object.entries(GRPC_TEST_META)) {
@@ -236,7 +238,7 @@ class GrpcConnection extends EventEmitter {
236
238
  * Events from the GRPC stream (a ClientDuplexStreamImpl) are the main way
237
239
  * we communicate with the GRPC server.
238
240
  *
239
- * @param {ClientDuplexStreamImpl} stream
241
+ * @param {object} stream duplex stream
240
242
  */
241
243
  _setupSpanStreamObservers(stream) {
242
244
  // Node streams require all data sent by the server to be read before the end
@@ -263,7 +265,7 @@ class GrpcConnection extends EventEmitter {
263
265
  // that the versioned Trace Observer is no longer available. Agents
264
266
  // MUST NOT attempt to reconnect in this case
265
267
  logger.info(
266
- '[UNIMPLEMENTED]: Trace Obserserver is no longer available. Shutting down connection.'
268
+ '[UNIMPLEMENTED]: Trace Observer is no longer available. Shutting down connection.'
267
269
  )
268
270
  this._disconnect()
269
271
  } else if (grpc.status[grpc.status.OK] === grpcStatusName) {
@@ -297,7 +299,8 @@ class GrpcConnection extends EventEmitter {
297
299
  /**
298
300
  * Creates the GRPC credentials needed
299
301
  *
300
- * @param grpcApi
302
+ * @param {object} grpcApi grpc lib
303
+ * @returns {object} ssl credentials
301
304
  */
302
305
  _generateCredentials(grpcApi) {
303
306
  let certBuffer = null
@@ -325,15 +328,17 @@ class GrpcConnection extends EventEmitter {
325
328
  *
326
329
  * Contains the actual logic that connects to the GRPC service.
327
330
  * "Connection" can be a somewhat misleading term here. This method
328
- * invokes the "recordSpan" remote proceduce call. Behind the scenes
331
+ * invokes the either `recordSpan` or `recordSpanBatch` remote procedure call. Behind the scenes
329
332
  * this makes an http2 request with the metadata, and then returns
330
333
  * a stream for further writing.
334
+ *
335
+ * @returns {object} stream duplex stream
331
336
  */
332
337
  _connectSpans() {
333
338
  if (!this._client) {
334
339
  // Only create once to avoid potential memory leak.
335
340
  // We create here (currently) for consistent error handling.
336
- this._client = this._createClient(this._endpoint)
341
+ this._client = this._createClient()
337
342
  }
338
343
 
339
344
  const metadata = this._getMetadata(
@@ -343,7 +348,7 @@ class GrpcConnection extends EventEmitter {
343
348
  process.env
344
349
  )
345
350
 
346
- const stream = this._client.recordSpan(metadata)
351
+ const stream = this._client[this._method](metadata)
347
352
  this._setupSpanStreamObservers(stream)
348
353
 
349
354
  return stream
@@ -357,9 +362,10 @@ class GrpcConnection extends EventEmitter {
357
362
  * the stream is closed and we do not have a handle to the client. Currently
358
363
  * impacting grpc-js@1.2.11 and several earlier versions.
359
364
  *
360
- * @param endpoint
365
+ * @returns {object} protobuf API for IngestService
361
366
  */
362
- _createClient(endpoint) {
367
+ _createClient() {
368
+ const endpoint = this._endpoint
363
369
  logger.trace('Creating gRPC client for: ', endpoint)
364
370
 
365
371
  const packageDefinition = protoLoader.loadSync(PROTO_DEFINITION_PATH, PROTO_OPTIONS)
@@ -369,7 +375,25 @@ class GrpcConnection extends EventEmitter {
369
375
  const traceApi = protoDescriptor.com.newrelic.trace.v1
370
376
 
371
377
  const credentials = this._generateCredentials(grpc)
372
- return new traceApi.IngestService(endpoint, credentials)
378
+
379
+ // If you want to use mock server use insecure creds
380
+ // const credentials = grpc.credentials.createInsecure()
381
+
382
+ const opts = {}
383
+ if (this._compression) {
384
+ // 2 = gzip compression
385
+ // see: https://github.com/grpc/grpc-node/blob/master/packages/grpc-js/src/compression-algorithms.ts#L21
386
+ opts['grpc.default_compression_algorithm'] = 2
387
+ this._metrics
388
+ .getOrCreateMetric(`${NAMES.INFINITE_TRACING.COMPRESSION}/enabled`)
389
+ .incrementCallCount()
390
+ } else {
391
+ this._metrics
392
+ .getOrCreateMetric(`${NAMES.INFINITE_TRACING.COMPRESSION}/disabled`)
393
+ .incrementCallCount()
394
+ }
395
+
396
+ return new traceApi.IngestService(endpoint, credentials, opts)
373
397
  }
374
398
  }
375
399
 
@@ -6,6 +6,15 @@ service IngestService {
6
6
  // Accepts a stream of Span messages, and returns an irregular stream of
7
7
  // RecordStatus messages.
8
8
  rpc RecordSpan(stream Span) returns (stream RecordStatus) {}
9
+
10
+ // Accepts a stream of SpanBatch messages, and returns an irregular
11
+ // stream of RecordStatus messages. This endpoint can be used to improve
12
+ // throughput when Span messages are small
13
+ rpc RecordSpanBatch(stream SpanBatch) returns (stream RecordStatus) {}
14
+ }
15
+
16
+ message SpanBatch {
17
+ repeated Span spans = 1;
9
18
  }
10
19
 
11
20
  message Span {
@@ -27,3 +36,4 @@ message AttributeValue {
27
36
  message RecordStatus {
28
37
  uint64 messages_seen = 1;
29
38
  }
39
+
@@ -0,0 +1,38 @@
1
+ /*
2
+ * Copyright 2023 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const logger = require('../../logger').child({ component: 'nestjs' })
8
+ const semver = require('semver')
9
+
10
+ module.exports = function initialize(agent, core, moduleName, shim) {
11
+ const nestJsVersion = shim.require('./package.json').version
12
+ shim.setFramework(shim.NEST)
13
+ // Earliest version that runs in the tests
14
+ if (semver.lt(nestJsVersion, '8.0.0')) {
15
+ logger.debug(
16
+ `Not instrumenting Nest.js version ${nestJsVersion}; minimum instrumentable version is 8.0.0`
17
+ )
18
+ return
19
+ }
20
+
21
+ shim.wrap(core.BaseExceptionFilter.prototype, 'handleUnknownError', (shim, original) => {
22
+ return function wrappedHandleUnknownError(exception) {
23
+ const segment = shim.getActiveSegment()
24
+ const transaction = segment?.transaction
25
+ if (transaction) {
26
+ shim.agent.errors.add(transaction, exception)
27
+ logger.trace(exception, 'Captured error handled by Nest.js exception filter.')
28
+ } else {
29
+ logger.trace(
30
+ exception,
31
+ 'Ignoring error handled by Nest.js exception filter: not in a transaction'
32
+ )
33
+ }
34
+
35
+ return original.apply(this, arguments)
36
+ }
37
+ })
38
+ }
@@ -27,6 +27,7 @@ module.exports = function instrumentations() {
27
27
  'memcached': { type: MODULE_TYPE.DATASTORE },
28
28
  'mongodb': { type: MODULE_TYPE.DATASTORE },
29
29
  'mysql': { module: './instrumentation/mysql' },
30
+ '@nestjs/core': { type: MODULE_TYPE.WEB_FRAMEWORK },
30
31
  'pino': { module: './instrumentation/pino' },
31
32
  'pg': { type: MODULE_TYPE.DATASTORE },
32
33
  'q': { type: null },
@@ -52,7 +53,6 @@ module.exports = function instrumentations() {
52
53
  'npmlog': { type: MODULE_TYPE.TRACKING },
53
54
  'fancy-log': { type: MODULE_TYPE.TRACKING },
54
55
  '@prisma/client': { type: MODULE_TYPE.DATASTORE },
55
- '@nestjs/core': { type: MODULE_TYPE.TRACKING },
56
56
  'knex': { type: MODULE_TYPE.TRACKING }
57
57
  }
58
58
  }
@@ -264,7 +264,9 @@ const INFINITE_TRACING = {
264
264
  SPAN_RESPONSE_GRPC_STATUS: SUPPORTABILITY.INFINITE_TRACING + '/Span/gRPC/%s',
265
265
  QUEUE_CAPACITY: SUPPORTABILITY.INFINITE_TRACING + '/Span/QueueCapacity',
266
266
  QUEUE_SIZE: SUPPORTABILITY.INFINITE_TRACING + '/Span/QueueSize',
267
- DRAIN_DURATION: SUPPORTABILITY.INFINITE_TRACING + '/Drain/Duration'
267
+ DRAIN_DURATION: SUPPORTABILITY.INFINITE_TRACING + '/Drain/Duration',
268
+ COMPRESSION: `${SUPPORTABILITY.INFINITE_TRACING}/gRPC/Compression`,
269
+ BATCHING: `${SUPPORTABILITY.INFINITE_TRACING}/gRPC/Batching`
268
270
  }
269
271
 
270
272
  const FEATURES = {
@@ -5,8 +5,6 @@
5
5
 
6
6
  'use strict'
7
7
 
8
- /* eslint sonarjs/cognitive-complexity: ["error", 29] -- TODO: https://issues.newrelic.com/browse/NEWRELIC-5252 */
9
-
10
8
  const apiGateway = require('./api-gateway')
11
9
  const headerAttributes = require('../header-attributes')
12
10
  const get = require('../util/get')
@@ -67,6 +65,45 @@ class AwsLambda {
67
65
  return null
68
66
  }
69
67
 
68
+ wrapEnders() {
69
+ const shim = this.shim
70
+ // There is no prependListener in node 4, so we wrap emit to look for 'beforeExit'
71
+ // NOTE: This may be converted to holding onto a single ender function if only
72
+ // one invocation is executing at a time.
73
+ shim.wrap(process, 'emit', function wrapEmit(shim, emit) {
74
+ return function wrappedEmit(ev, error) {
75
+ // need to add error as uncaughtException to be used
76
+ // later to add to transaction errors
77
+ if (ev === 'unhandledRejection') {
78
+ uncaughtException = error
79
+ }
80
+
81
+ if (['beforeExit', 'unhandledRejection'].includes(ev)) {
82
+ transactionEnders.forEach((ender) => {
83
+ ender()
84
+ })
85
+ transactionEnders = []
86
+ }
87
+ return emit.apply(process, arguments)
88
+ }
89
+ })
90
+ }
91
+
92
+ wrapFatal() {
93
+ const shim = this.shim
94
+ shim.wrap(process, '_fatalException', function wrapper(shim, original) {
95
+ return function wrappedFatalException(error) {
96
+ // logic placed before the _fatalException call, since it ends the invocation
97
+ uncaughtException = error
98
+ transactionEnders.forEach((ender) => {
99
+ ender()
100
+ })
101
+ transactionEnders = []
102
+ return original.apply(this, arguments)
103
+ }
104
+ })
105
+ }
106
+
70
107
  patchLambdaHandler(handler) {
71
108
  const awsLambda = this
72
109
  const shim = this.shim
@@ -80,38 +117,8 @@ class AwsLambda {
80
117
  // Only wrap emit on process the first time patch is called.
81
118
  patchCalled = true
82
119
 
83
- // There is no prependListener in node 4, so we wrap emit to look for 'beforeExit'
84
- // NOTE: This may be converted to holding onto a single ender function if only
85
- // one invocation is executing at a time.
86
- shim.wrap(process, 'emit', function wrapEmit(shim, emit) {
87
- return function wrappedEmit(ev, error) {
88
- // need to add error as uncaughtException to be used
89
- // later to add to transaction errors
90
- if (ev === 'unhandledRejection') {
91
- uncaughtException = error
92
- }
93
-
94
- if (['beforeExit', 'unhandledRejection'].includes(ev)) {
95
- transactionEnders.forEach((ender) => {
96
- ender()
97
- })
98
- transactionEnders = []
99
- }
100
- return emit.apply(process, arguments)
101
- }
102
- })
103
-
104
- shim.wrap(process, '_fatalException', function wrapper(shim, original) {
105
- return function wrappedFatalException(error) {
106
- // logic placed before the _fatalException call, since it ends the invocation
107
- uncaughtException = error
108
- transactionEnders.forEach((ender) => {
109
- ender()
110
- })
111
- transactionEnders = []
112
- return original.apply(this, arguments)
113
- }
114
- })
120
+ this.wrapEnders()
121
+ this.wrapFatal()
115
122
  }
116
123
 
117
124
  return shim.bindCreateTransaction(wrappedHandler, { type: shim.BG })
@@ -155,7 +162,7 @@ class AwsLambda {
155
162
 
156
163
  transactionEnders.push(txnEnder)
157
164
 
158
- args[cbIndex] = wrapCallbackAndCaptureError(
165
+ args[cbIndex] = awsLambda.wrapCallbackAndCaptureError(
159
166
  transaction,
160
167
  txnEnder,
161
168
  args[cbIndex],
@@ -164,8 +171,8 @@ class AwsLambda {
164
171
 
165
172
  // context.{done,fail,succeed} are all considered deprecated by
166
173
  // AWS, but are considered functional.
167
- context.done = wrapCallbackAndCaptureError(transaction, txnEnder, context.done)
168
- context.fail = wrapCallbackAndCaptureError(transaction, txnEnder, context.fail)
174
+ context.done = awsLambda.wrapCallbackAndCaptureError(transaction, txnEnder, context.done)
175
+ context.fail = awsLambda.wrapCallbackAndCaptureError(transaction, txnEnder, context.fail)
169
176
  shim.wrap(context, 'succeed', function wrapSucceed(shim, original) {
170
177
  return function wrappedSucceed() {
171
178
  txnEnder()
@@ -215,25 +222,26 @@ class AwsLambda {
215
222
  )
216
223
  return prom
217
224
  }
225
+ }
218
226
 
219
- function wrapCallbackAndCaptureError(transaction, txnEnder, cb, processResult) {
220
- return function wrappedCallback() {
221
- let err = arguments[0]
222
- if (typeof err === 'string') {
223
- err = new Error(err)
224
- }
227
+ wrapCallbackAndCaptureError(transaction, txnEnder, cb, processResult) {
228
+ const shim = this.shim
229
+ return function wrappedCallback() {
230
+ let err = arguments[0]
231
+ if (typeof err === 'string') {
232
+ err = new Error(err)
233
+ }
225
234
 
226
- shim.agent.errors.add(transaction, err)
235
+ shim.agent.errors.add(transaction, err)
227
236
 
228
- if (processResult) {
229
- const result = arguments[1]
230
- processResult(result)
231
- }
237
+ if (processResult) {
238
+ const result = arguments[1]
239
+ processResult(result)
240
+ }
232
241
 
233
- txnEnder()
242
+ txnEnder()
234
243
 
235
- return cb.apply(this, arguments)
236
- }
244
+ return cb.apply(this, arguments)
237
245
  }
238
246
  }
239
247
 
@@ -36,6 +36,7 @@ const FRAMEWORK_NAMES = {
36
36
  HAPI: 'Hapi',
37
37
  KOA: 'Koa',
38
38
  NEXT: 'Nextjs',
39
+ NEST: 'Nestjs',
39
40
  RESTIFY: 'Restify'
40
41
  }
41
42
 
@@ -0,0 +1,135 @@
1
+ /*
2
+ * Copyright 2020 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ const logger = require('../logger').child({ component: 'base-span-streamer' })
9
+ const NAMES = require('../metrics/names').INFINITE_TRACING
10
+ const SPAN_DROP_MSG_INTERVAL_MS = 30000
11
+ const SPAN_DROP_MSG =
12
+ 'Queue full, dropping spans. ' +
13
+ `Will not warn again for ${SPAN_DROP_MSG_INTERVAL_MS / 1000} seconds.`
14
+
15
+ class BaseSpanStreamer {
16
+ constructor(licenseKey, connection, metrics, queueSize) {
17
+ this.stream = null
18
+ this.license_key = licenseKey
19
+ this.connection = connection
20
+ this.queue_size = queueSize
21
+ this.spans = []
22
+ this._metrics = metrics
23
+ this._writable = false
24
+
25
+ // 'connected' indicates a safely writeable stream.
26
+ // May still be mid-connect to gRPC server.
27
+ this.connection.on('connected', (stream) => {
28
+ logger.info('Span streamer connected')
29
+ this.stream = stream
30
+ this._writable = true
31
+ this.sendQueue()
32
+ })
33
+
34
+ this.connection.on('disconnected', () => {
35
+ logger.info('Span streamer disconnected')
36
+ this.stream = null
37
+ this._writable = false
38
+ })
39
+ }
40
+
41
+ addToQueue() {
42
+ throw new Error('addToQueue is not implemented')
43
+ }
44
+
45
+ sendQueue() {
46
+ throw new Error('sendQueue is not implemented')
47
+ }
48
+
49
+ /* Accepts a span and either writes it to the stream, queues it to be sent,
50
+ * or drops it depending on stream/queue state
51
+ */
52
+ write(span) {
53
+ this._metrics.getOrCreateMetric(NAMES.SEEN).incrementCallCount()
54
+
55
+ // If not writeable (because of backpressure) queue the span
56
+ if (!this._writable) {
57
+ if (this.spans.length < this.queue_size) {
58
+ this.addToQueue(span)
59
+ return
60
+ }
61
+
62
+ // While this can be directionally calculated between seen/sent the
63
+ // queue makes that a bit more disconnected. This will be a bit more specific.
64
+ this._metrics.getOrCreateMetric(NAMES.DROPPED).incrementCallCount()
65
+
66
+ // If the queue is full drop the span
67
+ logger.infoOncePer(
68
+ 'SPAN_DROP_MSG', // key for the OncePer
69
+ SPAN_DROP_MSG_INTERVAL_MS,
70
+ SPAN_DROP_MSG
71
+ )
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Sends the data(spans or span) over the stream. Spans are only sent here if the stream is
77
+ * in a writable state. If the stream becomes unwritable after sending the
78
+ * span, a drain event handler is setup to continue writing when possible.
79
+ *
80
+ * @param {*} data spans or span
81
+ * @param {number} [spanLen=1] number of spans sent in a stream(defaults to 1)
82
+ */
83
+ send(data, spanLen = 1) {
84
+ // false indicates the stream has reached the highWaterMark
85
+ // and future writes should be avoided until drained. written items,
86
+ // including the one that returned false, will still be buffered.
87
+ this._writable = this.stream.write(data)
88
+ this._metrics.getOrCreateMetric(NAMES.SENT).incrementCallCount(spanLen)
89
+
90
+ if (!this._writable) {
91
+ const waitDrainStart = Date.now()
92
+ const onDrain = this.drain.bind(this, waitDrainStart)
93
+ this.stream.once('drain', onDrain)
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Drains the span queue that built up when the connection was
99
+ * back-pressured or disconnected. `waitDrainStart` is when the stream
100
+ * initially blocked, used to time how long the stream was blocked. If this
101
+ * is not defined, it is assumed this is being called after a reconnect,
102
+ * and the metric is not used.
103
+ *
104
+ * @param {number} waitDrainStart time that drain started
105
+ */
106
+ drain(waitDrainStart) {
107
+ // Metric can be used to see how frequently completing drains as well as
108
+ // average time to drain from when we first notice.
109
+ const drainCompleted = Date.now()
110
+ const drainDurationMs = drainCompleted - waitDrainStart
111
+ this._metrics.getOrCreateMetric(NAMES.DRAIN_DURATION).recordValue(drainDurationMs / 1000)
112
+
113
+ // Once the 'drain' event fires we can begin writing to the stream again
114
+ this._writable = true
115
+
116
+ this.sendQueue()
117
+ }
118
+
119
+ connect(agentRunId, requestHeadersMap) {
120
+ this.connection.setConnectionDetails(this.license_key, agentRunId, requestHeadersMap)
121
+
122
+ this.connection.connectSpans(this.method)
123
+ }
124
+
125
+ disconnect() {
126
+ this.connection.disconnect()
127
+ }
128
+
129
+ createMetrics() {
130
+ this._metrics.getOrCreateMetric(NAMES.QUEUE_CAPACITY).recordValue(this.queue_size)
131
+ this._metrics.getOrCreateMetric(NAMES.QUEUE_SIZE).recordValue(this.spans.length)
132
+ }
133
+ }
134
+
135
+ module.exports = BaseSpanStreamer
@@ -0,0 +1,79 @@
1
+ /*
2
+ * Copyright 2023 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ const logger = require('../logger').child({ component: 'batch-span-streamer' })
9
+ const BaseSpanStreamer = require('./base-span-streamer')
10
+
11
+ class BatchSpanStreamer extends BaseSpanStreamer {
12
+ constructor(licenseKey, connection, metrics, queueSize) {
13
+ super(licenseKey, connection, metrics, queueSize)
14
+ this.sendTimer = null
15
+ this.batchSize = 750
16
+ this.connection.on('connected', () => {
17
+ logger.debug('Setting up batch interval')
18
+ this.sendTimer = setInterval(this.sendQueue.bind(this), 5000)
19
+ this.sendTimer.unref()
20
+ })
21
+
22
+ this.connection.on('disconnected', () => {
23
+ logger.debug('Clearing batch interval')
24
+ if (this.sendTimer) {
25
+ clearInterval(this.sendTimer)
26
+ }
27
+ })
28
+ }
29
+
30
+ addToQueue(span) {
31
+ this.spans.push(span.toStreamingFormat())
32
+ }
33
+
34
+ /* Accepts a span and either writes it to the stream, queues it to be sent,
35
+ * or drops it depending on stream/queue state
36
+ */
37
+ write(span) {
38
+ super.write(span)
39
+
40
+ if (!this._writable) {
41
+ return
42
+ }
43
+
44
+ this.addToQueue(span)
45
+
46
+ if (this.spans.length < this.queue_size) {
47
+ return
48
+ }
49
+
50
+ this.sendQueue()
51
+ }
52
+
53
+ /**
54
+ * Chunks the span queue into n per batch(currently at 750).
55
+ * The avg span is generally 1kb, picking an option slightly under to avoid
56
+ * being over 1MB uncompressed limit being imposed on the gRPC server.
57
+ * Since the processing happens asyc it'll be very hard to split further
58
+ * if a span batch is too big. We are being conservative here and other
59
+ * language agents transmit even smaller batches(100 per batch).
60
+ */
61
+ sendQueue() {
62
+ if (!this.spans.length) {
63
+ logger.trace('Queue is empty, not sending spans.')
64
+ return
65
+ }
66
+
67
+ for (let i = 0; i < this.spans.length; i += this.batchSize) {
68
+ const spans = this.spans.slice(i, i + this.batchSize)
69
+ logger.trace('Sending spans from queue: %s', spans.length)
70
+ this.send({ spans }, spans.length)
71
+ }
72
+
73
+ this.spans = []
74
+
75
+ logger.trace('Finished sending spans from queue.')
76
+ }
77
+ }
78
+
79
+ module.exports = BatchSpanStreamer
@@ -8,6 +8,7 @@
8
8
  const logger = require('../logger')
9
9
  const SpanEventAggregator = require('./span-event-aggregator')
10
10
  const StreamingSpanEventAggregator = require('./streaming-span-event-aggregator')
11
+ const NAMES = require('../metrics/names').INFINITE_TRACING
11
12
 
12
13
  function createSpanEventAggregator(config, collector, metrics) {
13
14
  const traceObserver = config.infinite_tracing.trace_observer
@@ -42,16 +43,32 @@ function createSpanEventAggregator(config, collector, metrics) {
42
43
  function createStreamingAggregator(config, collector, metrics) {
43
44
  logger.trace('Creating streaming span event aggregator for infinite tracing.')
44
45
  const GrpcConnection = require('../grpc/connection')
45
- const SpanStreamer = require('./span-streamer')
46
46
 
47
- const connection = new GrpcConnection(config.infinite_tracing.trace_observer, metrics)
48
- const spanStreamer = new SpanStreamer(
49
- config.license_key,
50
- connection,
51
- metrics,
52
- config.infinite_tracing.span_events.queue_size
53
- )
47
+ const connection = new GrpcConnection(config.infinite_tracing, metrics)
48
+ let spanStreamer
54
49
 
50
+ if (config.infinite_tracing.batching) {
51
+ const BatchSpanStreamer = require('./batch-span-streamer')
52
+ spanStreamer = new BatchSpanStreamer(
53
+ config.license_key,
54
+ connection,
55
+ metrics,
56
+ config.infinite_tracing.span_events.queue_size
57
+ )
58
+ metrics.getOrCreateMetric(`${NAMES.BATCHING}/enabled`).incrementCallCount()
59
+ } else {
60
+ const SpanStreamer = require('./span-streamer')
61
+ spanStreamer = new SpanStreamer(
62
+ config.license_key,
63
+ connection,
64
+ metrics,
65
+ config.infinite_tracing.span_events.queue_size
66
+ )
67
+ metrics.getOrCreateMetric(`${NAMES.BATCHING}/disabled`).incrementCallCount()
68
+ }
69
+
70
+ // this periodMs has no affect on gRPC calls
71
+ // the send method on StreamingSpanEventAggregator is a no-op
55
72
  const opts = {
56
73
  periodMs: 1000,
57
74
  limit: 50000,
@@ -1,68 +1,29 @@
1
1
  /*
2
- * Copyright 2020 New Relic Corporation. All rights reserved.
2
+ * Copyright 2023 New Relic Corporation. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
 
6
6
  'use strict'
7
7
 
8
8
  const logger = require('../logger').child({ component: 'span-streamer' })
9
- const NAMES = require('../metrics/names').INFINITE_TRACING
9
+ const BaseSpanStreamer = require('./base-span-streamer')
10
10
 
11
- const SPAN_DROP_MSG_INTERVAL_MS = 30000
12
- const SPAN_DROP_MSG =
13
- 'Queue full, dropping spans. ' +
14
- `Will not warn again for ${SPAN_DROP_MSG_INTERVAL_MS / 1000} seconds.`
15
-
16
- class SpanStreamer {
11
+ class SpanStreamer extends BaseSpanStreamer {
17
12
  constructor(licenseKey, connection, metrics, queueSize) {
18
- this.stream = null
19
- this.license_key = licenseKey
20
- this.connection = connection
21
- this.queue_size = queueSize
22
- this.spans = []
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
- logger.info('Span streamer connected')
30
- this.stream = stream
31
- this._writable = true
32
- this.sendQueue()
33
- })
13
+ super(licenseKey, connection, metrics, queueSize)
14
+ }
34
15
 
35
- this.connection.on('disconnected', () => {
36
- logger.info('Span streamer disconnected')
37
- this.stream = null
38
- this._writable = false
39
- })
16
+ addToQueue(span) {
17
+ this.spans.push(span)
40
18
  }
41
19
 
42
20
  /* Accepts a span and either writes it to the stream, queues it to be sent,
43
21
  * or drops it depending on stream/queue state
44
22
  */
45
23
  write(span) {
46
- this._metrics.getOrCreateMetric(NAMES.SEEN).incrementCallCount()
24
+ super.write(span)
47
25
 
48
- // If not writeable (because of backpressure) queue the span
49
26
  if (!this._writable) {
50
- if (this.spans.length < this.queue_size) {
51
- this.spans.push(span)
52
- return
53
- }
54
-
55
- // While this can be directionally calculated between seen/sent the
56
- // queue makes that a bit more disconnected. This will be a bit more specific.
57
- this._metrics.getOrCreateMetric(NAMES.DROPPED).incrementCallCount()
58
-
59
- // If the queue is full drop the span
60
- logger.infoOncePer(
61
- 'SPAN_DROP_MSG', // key for the OncePer
62
- SPAN_DROP_MSG_INTERVAL_MS,
63
- SPAN_DROP_MSG
64
- )
65
-
66
27
  return
67
28
  }
68
29
 
@@ -79,51 +40,13 @@ class SpanStreamer {
79
40
  }
80
41
  }
81
42
 
82
- /**
83
- * Sends the span over the stream. Spans are only sent here if the stream is
84
- * in a writable state. If the stream becomes unwritable after sending the
85
- * span, a drain event handler is setup to continue writing when possible.
86
- *
87
- * @param span
88
- */
89
- send(span) {
90
- // false indicates the stream has reached the highWaterMark
91
- // and future writes should be avoided until drained. written items,
92
- // including the one that returned false, will still be buffered.
93
- this._writable = this.stream.write(span)
94
- this._metrics.getOrCreateMetric(NAMES.SENT).incrementCallCount()
95
-
96
- if (!this._writable) {
97
- const waitDrainStart = Date.now()
98
- const onDrain = this.drain.bind(this, waitDrainStart)
99
- this.stream.once('drain', onDrain)
43
+ sendQueue() {
44
+ if (!this.spans.length) {
45
+ logger.trace('Queue is empty, not sending spans.')
46
+ return
100
47
  }
101
- }
102
48
 
103
- /**
104
- * Drains the span queue that built up when the connection was
105
- * back-pressured or disconnected. `waitDrainStart` is when the stream
106
- * initially blocked, used to time how long the stream was blocked. If this
107
- * is not defined, it is assumed this is being called after a reconnect,
108
- * and the metric is not used.
109
- *
110
- * @param waitDrainStart
111
- */
112
- drain(waitDrainStart) {
113
- // Metric can be used to see how frequently completing drains as well as
114
- // average time to drain from when we first notice.
115
- const drainCompleted = Date.now()
116
- const drainDurationMs = drainCompleted - waitDrainStart
117
- this._metrics.getOrCreateMetric(NAMES.DRAIN_DURATION).recordValue(drainDurationMs / 1000)
118
-
119
- // Once the 'drain' event fires we can begin writing to the stream again
120
- this._writable = true
121
-
122
- this.sendQueue()
123
- }
124
-
125
- sendQueue() {
126
- logger.trace('Sending spans from queue.')
49
+ logger.trace('Sending spans from queue: %s.', this.spans.length)
127
50
 
128
51
  // Continue sending the spans that were in the queue. _writable is checked
129
52
  // so that if a send fails while clearing the queue, this drain handler can
@@ -136,21 +59,6 @@ class SpanStreamer {
136
59
 
137
60
  logger.trace('Finished sending spans from queue. Items left in queue: %s', this.spans.length)
138
61
  }
139
-
140
- connect(agentRunId, requestHeadersMap) {
141
- this.connection.setConnectionDetails(this.license_key, agentRunId, requestHeadersMap)
142
-
143
- this.connection.connectSpans()
144
- }
145
-
146
- disconnect() {
147
- this.connection.disconnect()
148
- }
149
-
150
- createMetrics() {
151
- this._metrics.getOrCreateMetric(NAMES.QUEUE_CAPACITY).recordValue(this.queue_size)
152
- this._metrics.getOrCreateMetric(NAMES.QUEUE_SIZE).recordValue(this.spans.length)
153
- }
154
62
  }
155
63
 
156
64
  module.exports = SpanStreamer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "10.0.0",
3
+ "version": "10.1.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [
@@ -231,7 +231,6 @@
231
231
  "prettier": "^2.3.2",
232
232
  "proxyquire": "^1.8.0",
233
233
  "q": "*",
234
- "request": "^2.88.2",
235
234
  "rimraf": "^2.6.3",
236
235
  "should": "*",
237
236
  "sinon": "^4.5.0",