newrelic 12.17.0 → 12.18.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,32 +1,56 @@
1
- ### v12.17.0 (2025-04-02)
2
-
3
- #### Features
4
-
5
- * Azure Function utilization ([#3017](https://github.com/newrelic/node-newrelic/pull/3017)) ([80ab93e](https://github.com/newrelic/node-newrelic/commit/80ab93ee5599800a823e3828f9730d5421b460c2))
6
- * Support honoring W3C `traceparent` sampled flag ([#3009](https://github.com/newrelic/node-newrelic/pull/3009)) ([d903413](https://github.com/newrelic/node-newrelic/commit/d903413a6216dae2cde3e6b6366c3a390d4ed4cd))
7
- * By default the agent will not honor the `traceparent` sampled flag.
8
- * To control how sampling works with `traceparent` set the following in config:
9
- * `distributed_tracing.sampler.remote_parent_sampled`(when the traceparent sampled flag is `01`)
10
- * `always_on`: the agent will sample spans
11
- * `always_off`: the agent will not sample spans
12
- * `default`: the agent will rely on existing priority sampling to make its decisions
13
- * `distributed_tracing.sampler.remote_parent_not_sampled`(when the traceparent sampled flag is `00`)
14
- * `always_on`: the agent will sample spans
15
- * `always_off`: the agent will not sample spans
16
- * `default`: the agent will rely on existing priority sampling to make its decisions
17
-
18
- #### Code refactoring
19
-
20
- * Updated otel bridge to centralize mapping rules for a given span attribute to accomondate semantic convention spec updates ([#3010](https://github.com/newrelic/node-newrelic/pull/3010)) ([c20c36b](https://github.com/newrelic/node-newrelic/commit/c20c36bbe0f63ab18fbbe85ad4bfaa3b80f1475d))
21
-
22
- #### Documentation
23
-
24
- * Updated compatibility report ([#3013](https://github.com/newrelic/node-newrelic/pull/3013)) ([8fab715](https://github.com/newrelic/node-newrelic/commit/8fab715581ccc5da7118884a0080557891329daa))
25
-
26
- #### Tests
27
-
28
- * Pinned `openai@4.90.0` to work with `@langchain/openai` ([#3019](https://github.com/newrelic/node-newrelic/pull/3019)) ([eaa3db0](https://github.com/newrelic/node-newrelic/commit/eaa3db0a556a19c44a05926dc339ea7bcbc9cebd))
29
-
1
+ ### v12.18.0 (2025-04-14)
2
+
3
+ #### Features
4
+
5
+ * Support Azure functions background trigger types ([#3028](https://github.com/newrelic/node-newrelic/pull/3028)) ([3bf17b0](https://github.com/newrelic/node-newrelic/commit/3bf17b03abaa713c1f9ad544c54c24a6374359c2))
6
+ * Support Azure functions HTTP trigger type ([#3021](https://github.com/newrelic/node-newrelic/pull/3021)) ([90d894b](https://github.com/newrelic/node-newrelic/commit/90d894bf1eb4a48f4b7f314ae22eef2cc76c32b9))
7
+
8
+ #### Bug fixes
9
+
10
+ * Updated span streamer to properly retry failed batches and handle flushing batch queue every 5 seconds ([#3033](https://github.com/newrelic/node-newrelic/pull/3033)) ([7db0e7c](https://github.com/newrelic/node-newrelic/commit/7db0e7ca929d54858addd5255b46507f82333045))
11
+
12
+ #### Code refactoring
13
+
14
+ * Update otel bridge rules synthesizer to properly handle consumer kind rules ([#3003](https://github.com/newrelic/node-newrelic/pull/3003)) ([3295dd1](https://github.com/newrelic/node-newrelic/commit/3295dd12bbebb708f3eeca2ad32fbc157a892fec))
15
+
16
+ #### Documentation
17
+
18
+ * Updated compatibility report ([#3020](https://github.com/newrelic/node-newrelic/pull/3020)) ([ce32b67](https://github.com/newrelic/node-newrelic/commit/ce32b6754383b6b1fe95d1d37759810b4caaf34c))
19
+
20
+ #### Miscellaneous chores
21
+
22
+ * Fixed prisma CI issues ([#3031](https://github.com/newrelic/node-newrelic/pull/3031)) ([1349cae](https://github.com/newrelic/node-newrelic/commit/1349cae4d87941e4f55dffc2445c90e73d823bbc))
23
+ * Improved logging within utilization detection ([#3034](https://github.com/newrelic/node-newrelic/pull/3034)) ([b47bd46](https://github.com/newrelic/node-newrelic/commit/b47bd46ad144723ac62192eb9cc474ddd1fce98d))
24
+
25
+ ### v12.17.0 (2025-04-02)
26
+
27
+ #### Features
28
+
29
+ * Azure Function utilization ([#3017](https://github.com/newrelic/node-newrelic/pull/3017)) ([80ab93e](https://github.com/newrelic/node-newrelic/commit/80ab93ee5599800a823e3828f9730d5421b460c2))
30
+ * Support honoring W3C `traceparent` sampled flag ([#3009](https://github.com/newrelic/node-newrelic/pull/3009)) ([d903413](https://github.com/newrelic/node-newrelic/commit/d903413a6216dae2cde3e6b6366c3a390d4ed4cd))
31
+ * By default the agent will not honor the `traceparent` sampled flag.
32
+ * To control how sampling works with `traceparent` set the following in config:
33
+ * `distributed_tracing.sampler.remote_parent_sampled`(when the traceparent sampled flag is `01`)
34
+ * `always_on`: the agent will sample spans
35
+ * `always_off`: the agent will not sample spans
36
+ * `default`: the agent will rely on existing priority sampling to make its decisions
37
+ * `distributed_tracing.sampler.remote_parent_not_sampled`(when the traceparent sampled flag is `00`)
38
+ * `always_on`: the agent will sample spans
39
+ * `always_off`: the agent will not sample spans
40
+ * `default`: the agent will rely on existing priority sampling to make its decisions
41
+
42
+ #### Code refactoring
43
+
44
+ * Updated otel bridge to centralize mapping rules for a given span attribute to accomondate semantic convention spec updates ([#3010](https://github.com/newrelic/node-newrelic/pull/3010)) ([c20c36b](https://github.com/newrelic/node-newrelic/commit/c20c36bbe0f63ab18fbbe85ad4bfaa3b80f1475d))
45
+
46
+ #### Documentation
47
+
48
+ * Updated compatibility report ([#3013](https://github.com/newrelic/node-newrelic/pull/3013)) ([8fab715](https://github.com/newrelic/node-newrelic/commit/8fab715581ccc5da7118884a0080557891329daa))
49
+
50
+ #### Tests
51
+
52
+ * Pinned `openai@4.90.0` to work with `@langchain/openai` ([#3019](https://github.com/newrelic/node-newrelic/pull/3019)) ([eaa3db0](https://github.com/newrelic/node-newrelic/commit/eaa3db0a556a19c44a05926dc339ea7bcbc9cebd))
53
+
30
54
  ### v12.16.1 (2025-03-24)
31
55
 
32
56
  #### Features
@@ -91,7 +91,7 @@ code, the source code can be found at [https://github.com/newrelic/node-newrelic
91
91
 
92
92
  ### @grpc/grpc-js
93
93
 
94
- This product includes source derived from [@grpc/grpc-js](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) ([v1.12.4](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/tree/v1.12.4)), distributed under the [Apache-2.0 License](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/blob/v1.12.4/LICENSE):
94
+ This product includes source derived from [@grpc/grpc-js](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) ([v1.13.2](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/tree/v1.13.2)), distributed under the [Apache-2.0 License](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/blob/v1.13.2/LICENSE):
95
95
 
96
96
  ```
97
97
  Apache License
@@ -509,7 +509,7 @@ This product includes source derived from [@grpc/proto-loader](https://github.co
509
509
 
510
510
  ### @newrelic/security-agent
511
511
 
512
- This product includes source derived from [@newrelic/security-agent](https://github.com/newrelic/csec-node-agent) ([v2.3.1](https://github.com/newrelic/csec-node-agent/tree/v2.3.1)), distributed under the [UNKNOWN License](https://github.com/newrelic/csec-node-agent/blob/v2.3.1/LICENSE):
512
+ This product includes source derived from [@newrelic/security-agent](https://github.com/newrelic/csec-node-agent) ([v2.4.0](https://github.com/newrelic/csec-node-agent/tree/v2.4.0)), distributed under the [UNKNOWN License](https://github.com/newrelic/csec-node-agent/blob/v2.4.0/LICENSE):
513
513
 
514
514
  ```
515
515
  ## New Relic Software License v1.0
@@ -1271,7 +1271,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1271
1271
 
1272
1272
  ### import-in-the-middle
1273
1273
 
1274
- This product includes source derived from [import-in-the-middle](https://github.com/nodejs/import-in-the-middle) ([v1.13.0](https://github.com/nodejs/import-in-the-middle/tree/v1.13.0)), distributed under the [Apache-2.0 License](https://github.com/nodejs/import-in-the-middle/blob/v1.13.0/LICENSE):
1274
+ This product includes source derived from [import-in-the-middle](https://github.com/nodejs/import-in-the-middle) ([v1.13.1](https://github.com/nodejs/import-in-the-middle/tree/v1.13.1)), distributed under the [Apache-2.0 License](https://github.com/nodejs/import-in-the-middle/blob/v1.13.1/LICENSE):
1275
1275
 
1276
1276
  ```
1277
1277
  Apache License
@@ -1615,7 +1615,7 @@ IN THE SOFTWARE.
1615
1615
 
1616
1616
  ### require-in-the-middle
1617
1617
 
1618
- This product includes source derived from [require-in-the-middle](https://github.com/elastic/require-in-the-middle) ([v7.4.0](https://github.com/elastic/require-in-the-middle/tree/v7.4.0)), distributed under the [MIT License](https://github.com/elastic/require-in-the-middle/blob/v7.4.0/LICENSE):
1618
+ This product includes source derived from [require-in-the-middle](https://github.com/elastic/require-in-the-middle) ([v7.5.0](https://github.com/elastic/require-in-the-middle/tree/v7.5.0)), distributed under the [MIT License](https://github.com/elastic/require-in-the-middle/blob/v7.5.0/LICENSE):
1619
1619
 
1620
1620
  ```
1621
1621
  The MIT License (MIT)
@@ -1701,7 +1701,7 @@ SOFTWARE.
1701
1701
 
1702
1702
  ### @aws-sdk/client-s3
1703
1703
 
1704
- This product includes source derived from [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3) ([v3.714.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.714.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.714.0/LICENSE):
1704
+ This product includes source derived from [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3) ([v3.726.1](https://github.com/aws/aws-sdk-js-v3/tree/v3.726.1)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.726.1/LICENSE):
1705
1705
 
1706
1706
  ```
1707
1707
  Apache License
@@ -1910,7 +1910,7 @@ This product includes source derived from [@aws-sdk/client-s3](https://github.co
1910
1910
 
1911
1911
  ### @aws-sdk/s3-request-presigner
1912
1912
 
1913
- This product includes source derived from [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3) ([v3.714.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.714.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.714.0/LICENSE):
1913
+ This product includes source derived from [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3) ([v3.726.1](https://github.com/aws/aws-sdk-js-v3/tree/v3.726.1)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.726.1/LICENSE):
1914
1914
 
1915
1915
  ```
1916
1916
  Apache License
@@ -3770,7 +3770,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3770
3770
 
3771
3771
  ### eslint
3772
3772
 
3773
- This product includes source derived from [eslint](https://github.com/eslint/eslint) ([v9.17.0](https://github.com/eslint/eslint/tree/v9.17.0)), distributed under the [MIT License](https://github.com/eslint/eslint/blob/v9.17.0/LICENSE):
3773
+ This product includes source derived from [eslint](https://github.com/eslint/eslint) ([v9.18.0](https://github.com/eslint/eslint/tree/v9.18.0)), distributed under the [MIT License](https://github.com/eslint/eslint/blob/v9.18.0/LICENSE):
3774
3774
 
3775
3775
  ```
3776
3776
  Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
@@ -1197,7 +1197,14 @@ defaultConfig.definition = () => ({
1197
1197
  port: {
1198
1198
  formatter: int,
1199
1199
  default: 443
1200
- }
1200
+ },
1201
+ /**
1202
+ * For testing only. This allows the connection to an insecure gRPC server.
1203
+ */
1204
+ insecure: {
1205
+ formatter: boolean,
1206
+ default: false
1207
+ },
1201
1208
  },
1202
1209
  span_events: {
1203
1210
  /**
@@ -1206,6 +1213,13 @@ defaultConfig.definition = () => ({
1206
1213
  queue_size: {
1207
1214
  formatter: int,
1208
1215
  default: 10000
1216
+ },
1217
+ /**
1218
+ * Size of batches to post to 8T server
1219
+ */
1220
+ batch_size: {
1221
+ formatter: int,
1222
+ default: 750
1209
1223
  }
1210
1224
  },
1211
1225
  batching: {
@@ -62,6 +62,7 @@ class GrpcConnection extends EventEmitter {
62
62
 
63
63
  const traceObserverConfig = infiniteTracingConfig.trace_observer
64
64
  this._endpoint = this.getTraceObserverEndpoint(traceObserverConfig)
65
+ this._insecureConnection = traceObserverConfig.insecure
65
66
 
66
67
  this._client = null
67
68
  this.stream = null
@@ -202,11 +203,13 @@ class GrpcConnection extends EventEmitter {
202
203
 
203
204
  _disconnect() {
204
205
  logger.trace('Disconnecting from gRPC endpoint.')
206
+ this._setState(connectionStates.disconnected)
205
207
 
206
208
  if (this.stream) {
207
209
  this.stream.removeAllListeners()
208
210
 
209
211
  const oldStream = this.stream
212
+
210
213
  this.stream.on('status', function endStreamStatusHandler(grpcStatus) {
211
214
  logger.trace('End stream status received [%s]: %s', grpcStatus.code, grpcStatus.details)
212
215
 
@@ -228,8 +231,6 @@ class GrpcConnection extends EventEmitter {
228
231
 
229
232
  this.stream = null
230
233
  }
231
-
232
- this._setState(connectionStates.disconnected)
233
234
  }
234
235
 
235
236
  /**
@@ -374,12 +375,10 @@ class GrpcConnection extends EventEmitter {
374
375
 
375
376
  const traceApi = protoDescriptor.com.newrelic.trace.v1
376
377
 
377
- const credentials = this._generateCredentials(grpc)
378
-
379
- // If you want to use mock server use insecure creds
380
- // const credentials = grpc.credentials.createInsecure()
378
+ const credentials = this._insecureConnection ? grpc.credentials.createInsecure() : this._generateCredentials(grpc)
381
379
 
382
380
  const opts = {}
381
+
383
382
  if (this._compression) {
384
383
  // 2 = gzip compression
385
384
  // see: https://github.com/grpc/grpc-node/blob/master/packages/grpc-js/src/compression-algorithms.ts#L21
@@ -393,6 +392,27 @@ class GrpcConnection extends EventEmitter {
393
392
  .incrementCallCount()
394
393
  }
395
394
 
395
+ // this defines retries of writes to stream if it fails with `UNAVAILABLE` or `INTERNAL`
396
+ const serviceConfig = {
397
+ methodConfig: [
398
+ {
399
+ name: [
400
+ {
401
+ service: 'com.newrelic.trace.v1.IngestService'
402
+ }
403
+ ],
404
+ retryPolicy: {
405
+ maxAttempts: 10,
406
+ initialBackoff: '0.1s',
407
+ maxBackoff: '1s',
408
+ backoffMultiplier: 2,
409
+ retryableStatusCodes: ['UNAVAILABLE', 'INTERNAL']
410
+ }
411
+ }
412
+ ]
413
+ }
414
+ opts['grpc.service_config'] = JSON.stringify(serviceConfig)
415
+
396
416
  return new traceApi.IngestService(endpoint, credentials, opts)
397
417
  }
398
418
  }
@@ -0,0 +1,311 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ const defaultLogger = require('../../logger').child({ component: 'azure-functions' })
9
+ const urltils = require('../../util/urltils')
10
+ const headerProcessing = require('../../header-processing')
11
+ const synthetics = require('../../synthetics')
12
+
13
+ const backgroundRecorder = require('../../metrics/recorders/other.js')
14
+ const recordWeb = require('../../metrics/recorders/http')
15
+
16
+ const {
17
+ DESTINATIONS: DESTS,
18
+ TYPES
19
+ } = require('../../transaction/index.js')
20
+
21
+ const {
22
+ WEBSITE_OWNER_NAME,
23
+ WEBSITE_RESOURCE_GROUP,
24
+ WEBSITE_SITE_NAME
25
+ } = process.env
26
+ const SUBSCRIPTION_ID = WEBSITE_OWNER_NAME?.split('+').shift()
27
+ const RESOURCE_GROUP_NAME = WEBSITE_RESOURCE_GROUP ?? WEBSITE_OWNER_NAME?.split('+').pop().split('-Linux').shift()
28
+ const AZURE_FUNCTION_APP_NAME = WEBSITE_SITE_NAME
29
+
30
+ let coldStart = true
31
+ let _agent
32
+ let _logger
33
+
34
+ module.exports = function initialize(agent, azureFunctions, _moduleName, shim, { logger = defaultLogger } = {}) {
35
+ _agent = agent
36
+ _logger = logger
37
+ if (!SUBSCRIPTION_ID || !RESOURCE_GROUP_NAME || !AZURE_FUNCTION_APP_NAME) {
38
+ logger.warn(
39
+ {
40
+ data: {
41
+ expectedVars: ['WEBSITE_OWNER_NAME', 'WEBSITE_RESOURCE_GROUP', 'WEBSITE_SITE_NAME'],
42
+ found: { WEBSITE_OWNER_NAME, WEBSITE_RESOURCE_GROUP, WEBSITE_SITE_NAME }
43
+ },
44
+ },
45
+ 'could not initialize azure functions instrumentation due to missing environment variables'
46
+ )
47
+ return
48
+ }
49
+
50
+ const httpMethods = ['http', 'get', 'put', 'post', 'patch', 'deleteRequest']
51
+ shim.wrap(azureFunctions.app, httpMethods, wrapAzureHttpMethods)
52
+
53
+ const backgroundMethods = [
54
+ 'cosmosDB',
55
+ 'eventGrid',
56
+ 'eventHub',
57
+ 'mySql',
58
+ 'serviceBusQueue',
59
+ 'serviceBusTopic',
60
+ 'sql',
61
+ 'storageBlob',
62
+ 'storageQueue',
63
+ 'timer',
64
+ 'warmup',
65
+ 'webPubSub'
66
+ ]
67
+ shim.wrap(azureFunctions.app, backgroundMethods, wrapAzureBackgroundMethods)
68
+ }
69
+
70
+ function wrapAzureHttpMethods(shim, appMethod) {
71
+ return async function wrappedAzureHttpMethod(...args) {
72
+ // If the app doesn't need an options object, the user can pass the
73
+ // handler function as the second argument
74
+ // (e.g. `app.get('name', handler)`).
75
+ // See https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node?tabs=javascript%2Cwindows%2Cazure-cli&pivots=nodejs-model-v4#registering-a-function
76
+ let handler
77
+ if (typeof args[1] === 'function') {
78
+ handler = args[1]
79
+ args[1] = { handler }
80
+ } else {
81
+ handler = args[1].handler
82
+ }
83
+
84
+ const tracer = shim.tracer
85
+ args[1].handler = tracer.transactionProxy(async function wrappedHandler(...args) {
86
+ const [request, context] = args
87
+ const ctx = tracer.getContext()
88
+ const tx = tracer.getTransaction()
89
+
90
+ // Set the transaction name according to our spec (category + function name).
91
+ tx.setPartialName(`AzureFunction/${context.functionName}`)
92
+
93
+ const url = new URL(request.url)
94
+ const segment = tracer.createSegment({
95
+ name: request.url,
96
+ recorder: recordWeb,
97
+ parent: ctx.segment,
98
+ transaction: tx
99
+ })
100
+ segment.start()
101
+
102
+ const transport = url.protocol === 'https:' ? 'HTTPS' : 'HTTP'
103
+ tx.type = TYPES.WEB
104
+ tx.baseSegment = segment
105
+ tx.parsedUrl = url
106
+ tx.url = urltils.obfuscatePath(_agent.config, url.pathname)
107
+ tx.verb = request.method
108
+ if (url.port === '') {
109
+ tx.port = transport === 'HTTPS' ? '443' : '80'
110
+ } else {
111
+ tx.port = url.port
112
+ }
113
+
114
+ tx.trace.attributes.addAttribute(
115
+ DESTS.TRANS_EVENT | DESTS.ERROR_EVENT,
116
+ 'request.uri',
117
+ tx.url
118
+ )
119
+ segment.addSpanAttribute('request.uri', tx.url)
120
+ if (request.method != null) {
121
+ segment.addSpanAttribute('request.method', request.method)
122
+ }
123
+ addAttributes({ transaction: tx, functionContext: context })
124
+
125
+ const queueTimeStamp = headerProcessing.getQueueTime(_logger, request.headers)
126
+ if (queueTimeStamp) {
127
+ tx.queueTime = Date.now() - queueTimeStamp
128
+ }
129
+
130
+ synthetics.assignHeadersToTransaction(_agent.config, tx, request.headers)
131
+ if (_agent.config.distributed_tracing.enabled === true) {
132
+ tx.acceptDistributedTraceHeaders(transport, request.headers)
133
+ }
134
+
135
+ const newContext = ctx.enterSegment({ segment })
136
+ const boundHandler = tracer.bindFunction(handler, newContext)
137
+
138
+ const result = await boundHandler(...args)
139
+ // Responses should have a shape as described at:
140
+ // https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node?tabs=javascript%2Cwindows%2Cazure-cli&pivots=nodejs-model-v4#http-response
141
+ if (result.status) {
142
+ tx.trace.attributes.addAttribute(
143
+ DESTS.TRANS_COMMON,
144
+ 'http.statusCode',
145
+ result.status
146
+ )
147
+ }
148
+
149
+ if (coldStart === true) {
150
+ tx.trace.attributes.addAttribute(DESTS.TRANS_COMMON, 'faas.coldStart', true)
151
+ coldStart = false
152
+ }
153
+
154
+ tx.end()
155
+ return result
156
+ })
157
+
158
+ await appMethod(...args)
159
+ }
160
+ }
161
+
162
+ function wrapAzureBackgroundMethods(shim, appMethod) {
163
+ return async function wrappedAzureBackgroundMethod(...args) {
164
+ let handler
165
+ if (typeof args[1] === 'function') {
166
+ handler = args[1]
167
+ args[1] = { handler }
168
+ } else {
169
+ handler = args[1].handler
170
+ }
171
+
172
+ const tracer = shim.tracer
173
+ args[1].handler = tracer.transactionProxy(async function wrappedHandler(...args) {
174
+ const [, context] = args
175
+ const ctx = tracer.getContext()
176
+ const tx = tracer.getTransaction()
177
+
178
+ tx.setPartialName(`AzureFunction/${context.functionName}`)
179
+
180
+ const segment = tracer.createSegment({
181
+ name: `${appMethod}-trigger`,
182
+ recorder: backgroundRecorder,
183
+ parent: ctx.segment,
184
+ transaction: tx
185
+ })
186
+ segment.start()
187
+
188
+ tx.type = TYPES.BG
189
+ tx.baseSegment = segment
190
+ addAttributes({ transaction: tx, functionContext: context })
191
+
192
+ const newContext = ctx.enterSegment({ segment })
193
+ const boundHandler = tracer.bindFunction(handler, newContext)
194
+
195
+ const result = await boundHandler(...args)
196
+ if (coldStart === true) {
197
+ tx.trace.attributes.addAttribute(DESTS.TRANS_COMMON, 'faas.coldStart', true)
198
+ coldStart = false
199
+ }
200
+
201
+ tx.end()
202
+ return result
203
+ })
204
+
205
+ await appMethod(...args)
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Add required New Relic attributes to the transaction.
211
+ *
212
+ * @param {object} params Function parameters.
213
+ * @param {Transaction} params.transaction The transaction to update.
214
+ * @param {object} params.functionContext The function invocation context
215
+ * provided by the Azure functions runtime.
216
+ */
217
+ function addAttributes({ transaction, functionContext }) {
218
+ transaction.trace.attributes.addAttribute(
219
+ DESTS.TRANS_COMMON,
220
+ 'faas.invocation_id',
221
+ functionContext.invocationId ?? 'unknown'
222
+ )
223
+ transaction.trace.attributes.addAttribute(
224
+ DESTS.TRANS_COMMON,
225
+ 'faas.name',
226
+ functionContext.functionName ?? 'unknown'
227
+ )
228
+ transaction.trace.attributes.addAttribute(
229
+ DESTS.TRANS_COMMON,
230
+ 'faas.trigger',
231
+ mapTriggerType({ functionContext })
232
+ )
233
+ transaction.trace.attributes.addAttribute(
234
+ DESTS.TRANS_COMMON,
235
+ 'cloud.resource_id',
236
+ buildCloudResourceId({ functionContext })
237
+ )
238
+ }
239
+
240
+ /**
241
+ * Inspects the provided function invocation context and returns a recognized
242
+ * trigger type suitable for sending to the collector as `faas.trigger`.
243
+ *
244
+ * @param {object} params Function parameters
245
+ * @param {object} params.functionContext The function context as provided by
246
+ * the Azure functions runtime.
247
+ *
248
+ * @returns {string} A string appropriate for New Relic.
249
+ */
250
+ function mapTriggerType({ functionContext }) {
251
+ const input = functionContext.options?.trigger?.type
252
+
253
+ // Input types are found at:
254
+ // https://github.com/Azure/azure-functions-nodejs-library/blob/138c021/src/trigger.ts
255
+ // https://learn.microsoft.com/en-us/azure/azure-functions/functions-triggers-bindings?tabs=isolated-process%2Cnode-v4%2Cpython-v2&pivots=programming-language-javascript#supported-bindings
256
+ switch (input) {
257
+ case 'httpTrigger': {
258
+ return 'http'
259
+ }
260
+
261
+ case 'timerTrigger': {
262
+ return 'timer'
263
+ }
264
+
265
+ case 'blobTrigger':
266
+ case 'cosmosDBTrigger':
267
+ case 'daprBindingTrigger':
268
+ case 'mysqlTrigger':
269
+ case 'queueTrigger':
270
+ case 'sqlTrigger': {
271
+ return 'datasource'
272
+ }
273
+
274
+ case 'daprTopicTrigger':
275
+ case 'eventGridTrigger':
276
+ case 'eventHubTrigger':
277
+ case 'kafkaTrigger':
278
+ case 'rabbitMQTrigger':
279
+ case 'redisListTrigger':
280
+ case 'redisPubSubTrigger':
281
+ case 'redisStreamTrigger':
282
+ case 'serviceBusTrigger':
283
+ case 'signalRTrigger':
284
+ case 'webPubSubTrigger': {
285
+ return 'pubsub'
286
+ }
287
+
288
+ default: {
289
+ return 'other'
290
+ }
291
+ }
292
+ }
293
+
294
+ function buildCloudResourceId({ functionContext }) {
295
+ return [
296
+ '/subscriptions/',
297
+ SUBSCRIPTION_ID,
298
+ '/resourceGroups/',
299
+ RESOURCE_GROUP_NAME,
300
+ '/providers/Microsoft.Web/sites/',
301
+ AZURE_FUNCTION_APP_NAME,
302
+ '/functions/',
303
+ functionContext.functionName
304
+ ].join('')
305
+ }
306
+
307
+ module.exports.internals = {
308
+ addAttributes,
309
+ mapTriggerType,
310
+ buildCloudResourceId
311
+ }
@@ -10,6 +10,7 @@ const InstrumentationDescriptor = require('./instrumentation-descriptor')
10
10
  // Return a new copy of this array every time we're called
11
11
  module.exports = function instrumentations() {
12
12
  return {
13
+ '@azure/functions': { type: InstrumentationDescriptor.TYPE_GENERIC },
13
14
  '@elastic/elasticsearch': { type: InstrumentationDescriptor.TYPE_DATASTORE },
14
15
  '@opensearch-project/opensearch': { type: InstrumentationDescriptor.TYPE_DATASTORE },
15
16
  '@grpc/grpc-js': { module: './instrumentation/grpc-js' },
package/lib/otel/rules.js CHANGED
@@ -46,6 +46,7 @@ class Rule {
46
46
  static OTEL_SPAN_KIND_SERVER = 'server'
47
47
  static OTEL_SPAN_KIND_CLIENT = 'client'
48
48
  static OTEL_SPAN_KIND_PRODUCER = 'producer'
49
+ static OTEL_SPAN_KIND_CONSUMER = 'consumer'
49
50
  static OTEL_SPAN_KIND_INTERNAL = 'internal'
50
51
 
51
52
  #name
@@ -87,10 +88,6 @@ class Rule {
87
88
  return this.#spanKinds.includes(Rule.OTEL_SPAN_KIND_CLIENT)
88
89
  }
89
90
 
90
- get isConsumer() {
91
- return this.#spanKinds.includes('consumer')
92
- }
93
-
94
91
  get isInternalRule() {
95
92
  return this.#spanKinds.includes('internal')
96
93
  }
@@ -99,8 +96,12 @@ class Rule {
99
96
  return this.#spanKinds.includes(Rule.OTEL_SPAN_KIND_PRODUCER)
100
97
  }
101
98
 
99
+ get isConsumerRule() {
100
+ return this.#spanKinds.includes(Rule.OTEL_SPAN_KIND_CONSUMER)
101
+ }
102
+
102
103
  get isServerRule() {
103
- return this.#spanKinds.includes(Rule.OTEL_SPAN_KIND_SERVER) || this.isConsumer
104
+ return this.#spanKinds.includes(Rule.OTEL_SPAN_KIND_SERVER)
104
105
  }
105
106
 
106
107
  /**
@@ -129,7 +130,9 @@ class Rule {
129
130
 
130
131
  class RulesEngine {
131
132
  #serverRules = new Map()
133
+ #consumerRules = new Map()
132
134
  #fallbackServerRules = new Map()
135
+ #fallbackConsumerRules = new Map()
133
136
  #clientRules = new Map()
134
137
  #fallbackClientRules = new Map()
135
138
  #fallbackInternalRules = new Map()
@@ -142,6 +145,8 @@ class RulesEngine {
142
145
  if (/fallback/i.test(rule.name) === true) {
143
146
  if (rule.isServerRule === true) {
144
147
  this.#fallbackServerRules.set(rule.name, rule)
148
+ } else if (rule.isConsumerRule === true) {
149
+ this.#fallbackConsumerRules.set(rule.name, rule)
145
150
  } else if (rule.isClientRule === true) {
146
151
  this.#fallbackClientRules.set(rule.name, rule)
147
152
  } else if (rule.isProducerRule === true) {
@@ -154,6 +159,8 @@ class RulesEngine {
154
159
 
155
160
  if (rule.isServerRule === true) {
156
161
  this.#serverRules.set(rule.name, rule)
162
+ } else if (rule.isConsumerRule === true) {
163
+ this.#consumerRules.set(rule.name, rule)
157
164
  } else if (rule.isClientRule === true) {
158
165
  this.#clientRules.set(rule.name, rule)
159
166
  }
@@ -174,8 +181,7 @@ class RulesEngine {
174
181
 
175
182
  // eslint-disable-next-line sonarjs/no-labels, no-labels
176
183
  detector: switch (otelSpan.kind) {
177
- case SpanKind.SERVER:
178
- case SpanKind.CONSUMER: {
184
+ case SpanKind.SERVER: {
179
185
  for (const rule of this.#serverRules.values()) {
180
186
  if (rule.matches(otelSpan) === true) {
181
187
  result = rule
@@ -192,6 +198,23 @@ class RulesEngine {
192
198
  break
193
199
  }
194
200
 
201
+ case SpanKind.CONSUMER: {
202
+ for (const rule of this.#consumerRules.values()) {
203
+ if (rule.matches(otelSpan) === true) {
204
+ result = rule
205
+ // eslint-disable-next-line no-labels
206
+ break detector
207
+ }
208
+ }
209
+ for (const rule of this.#fallbackConsumerRules.values()) {
210
+ if (rule.matches(otelSpan) === true) {
211
+ result = rule
212
+ break
213
+ }
214
+ }
215
+ break
216
+ }
217
+
195
218
  case SpanKind.CLIENT: {
196
219
  for (const rule of this.#clientRules.values()) {
197
220
  if (rule.matches(otelSpan) === true) {
@@ -25,14 +25,14 @@ class BaseSpanStreamer {
25
25
  // 'connected' indicates a safely writeable stream.
26
26
  // May still be mid-connect to gRPC server.
27
27
  this.connection.on('connected', (stream) => {
28
- logger.info('Span streamer connected')
28
+ logger.trace('Span streamer connected, flushing queue')
29
29
  this.stream = stream
30
30
  this._writable = true
31
31
  this.sendQueue()
32
32
  })
33
33
 
34
34
  this.connection.on('disconnected', () => {
35
- logger.info('Span streamer disconnected')
35
+ logger.trace('Span streamer disconnected')
36
36
  this.stream = null
37
37
  this._writable = false
38
38
  })
@@ -52,9 +52,10 @@ class BaseSpanStreamer {
52
52
  write(span) {
53
53
  this._metrics.getOrCreateMetric(NAMES.SEEN).incrementCallCount()
54
54
 
55
- // If not writeable (because of backpressure) queue the span
55
+ // If not writable (because of backpressure) queue the span
56
56
  if (!this._writable) {
57
57
  if (this.spans.length < this.queue_size) {
58
+ logger.trace('stream not writable, add spans to queue')
58
59
  this.addToQueue(span)
59
60
  return
60
61
  }
@@ -78,16 +79,17 @@ class BaseSpanStreamer {
78
79
  * span, a drain event handler is setup to continue writing when possible.
79
80
  *
80
81
  * @param {*} data spans or span
81
- * @param {number} [spanLen] number of spans sent in a stream(defaults to 1)
82
82
  */
83
- send(data, spanLen = 1) {
83
+ send(data) {
84
+ const spans = data?.spans?.length || 1
85
+ this._writable = this.stream.write(data)
86
+ this._metrics.getOrCreateMetric(NAMES.SENT).incrementCallCount(spans)
87
+
84
88
  // false indicates the stream has reached the highWaterMark
85
89
  // and future writes should be avoided until drained. written items,
86
90
  // 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
91
  if (!this._writable) {
92
+ logger.trace('Stream is not writable, adding a drain listener to flush queue when it is writable.')
91
93
  const waitDrainStart = Date.now()
92
94
  const onDrain = this.drain.bind(this, waitDrainStart)
93
95
  this.stream.once('drain', onDrain)
@@ -112,7 +114,6 @@ class BaseSpanStreamer {
112
114
 
113
115
  // Once the 'drain' event fires we can begin writing to the stream again
114
116
  this._writable = true
115
-
116
117
  this.sendQueue()
117
118
  }
118
119
 
@@ -9,22 +9,11 @@ const logger = require('../logger').child({ component: 'batch-span-streamer' })
9
9
  const BaseSpanStreamer = require('./base-span-streamer')
10
10
 
11
11
  class BatchSpanStreamer extends BaseSpanStreamer {
12
- constructor(licenseKey, connection, metrics, queueSize) {
12
+ constructor(licenseKey, connection, metrics, queueSize, batchSize) {
13
13
  super(licenseKey, connection, metrics, queueSize)
14
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
- })
15
+ this.batchSize = batchSize
16
+ this.queueInterval = 5000
28
17
  }
29
18
 
30
19
  addToQueue(span) {
@@ -35,6 +24,11 @@ class BatchSpanStreamer extends BaseSpanStreamer {
35
24
  * or drops it depending on stream/queue state
36
25
  */
37
26
  write(span) {
27
+ const currTime = Date.now()
28
+
29
+ if (!this.time) {
30
+ this.time = currTime
31
+ }
38
32
  super.write(span)
39
33
 
40
34
  if (!this._writable) {
@@ -43,18 +37,29 @@ class BatchSpanStreamer extends BaseSpanStreamer {
43
37
 
44
38
  this.addToQueue(span)
45
39
 
46
- if (this.spans.length < this.queue_size) {
47
- return
40
+ if (this.batchReady(currTime)) {
41
+ this.sendQueue()
42
+ this.time = currTime
48
43
  }
44
+ }
49
45
 
50
- this.sendQueue()
46
+ /**
47
+ * The Infinite Tracing trace observer will treat spans that are more than 10 seconds apart as not belonging to the same group.
48
+ * It is therefore recommended that the agent perform a RecordSpanBatch request when either a target maximum batch size is reached or 5 seconds have elapsed since the creation of a batch (giving precedence to whichever of these events takes place first).
49
+ * Otherwise, delayed recording of spans will lead to their being discarded.
50
+ *
51
+ * @param {number} currTime timestamp in milliseconds
52
+ * @returns {boolean} if a batch can we sent
53
+ */
54
+ batchReady(currTime) {
55
+ return this.spans.length >= this.batchSize || currTime - this.time >= this.queueInterval
51
56
  }
52
57
 
53
58
  /**
54
- * Chunks the span queue into n per batch(currently at 750).
59
+ * Chunks the span queue into n per batch.
55
60
  * The avg span is generally 1kb, picking an option slightly under to avoid
56
61
  * 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
62
+ * Since the processing happens async it'll be very hard to split further
58
63
  * if a span batch is too big. We are being conservative here and other
59
64
  * language agents transmit even smaller batches(100 per batch).
60
65
  */
@@ -64,15 +69,13 @@ class BatchSpanStreamer extends BaseSpanStreamer {
64
69
  return
65
70
  }
66
71
 
67
- for (let i = 0; i < this.spans.length; i += this.batchSize) {
68
- const spans = this.spans.slice(i, i + this.batchSize)
72
+ while (this.spans.length > 0 && this._writable) {
73
+ const spans = this.spans.splice(0, this.batchSize)
69
74
  logger.trace('Sending spans from queue: %s', spans.length)
70
- this.send({ spans }, spans.length)
75
+ this.send({ spans })
71
76
  }
72
77
 
73
- this.spans = []
74
-
75
- logger.trace('Finished sending spans from queue.')
78
+ logger.trace('Finished sending spans from queue. Items left in queue: %s', this.spans.length)
76
79
  }
77
80
  }
78
81
 
@@ -54,7 +54,8 @@ function createStreamingAggregator(config, agent) {
54
54
  config.license_key,
55
55
  connection,
56
56
  metrics,
57
- config.infinite_tracing.span_events.queue_size
57
+ config.infinite_tracing.span_events.queue_size,
58
+ config.infinite_tracing.span_events.batch_size
58
59
  )
59
60
  metrics.getOrCreateMetric(`${NAMES.BATCHING}/enabled`).incrementCallCount()
60
61
  } else {
@@ -44,7 +44,7 @@ class SpanStreamer extends BaseSpanStreamer {
44
44
 
45
45
  logger.trace('Sending spans from queue: %s.', this.spans.length)
46
46
 
47
- // Continue sending the spans that were in the queue. _writable is checked
47
+ // Continue sending the spans that were in the queue. this._writable is checked
48
48
  // so that if a send fails while clearing the queue, this drain handler can
49
49
  // finish, and the drain handler setup on the failed send will then attempt
50
50
  // to clear the queue
@@ -10,6 +10,33 @@ const util = require('util')
10
10
  const Readable = require('readable-stream')
11
11
  const os = require('os')
12
12
 
13
+ /**
14
+ * A logging utility with methods `info`, `warn`, `error`, `fatal`, `debug`,
15
+ * and `trace`. This logger is very similar to Pino.
16
+ *
17
+ * Each method supports the following invocation signatures:
18
+ *
19
+ * @example
20
+ * // Logs a single string.
21
+ * logger.info('hello world')
22
+ *
23
+ * @example
24
+ * // Logs an interpolated string.
25
+ * logger.info('hello %s', 'world')
26
+ *
27
+ * @example
28
+ * // Logs with data properties inlined:
29
+ * logger.info({a: 1, b: 2}, 'logged data')
30
+ * // e.g. `{ a: 1, b: 2, msg: 'logged data', level: 30 }`
31
+ *
32
+ * @example
33
+ * // Recommended for keeping data usable:
34
+ * logger.info({ data: { the: 'data', that: 'is important' }}, 'some message')
35
+ * // ^ only the `data` key will be inlined at the top level. The sub-properties
36
+ * // will still be nested under the data key.
37
+ *
38
+ * @type {Object}
39
+ */
13
40
  module.exports = Logger
14
41
 
15
42
  const LEVELS = {
@@ -294,12 +321,17 @@ function getPropertiesToLog(extra) {
294
321
  const obj = Object.assign(Object.create(null), extra)
295
322
  // Error properties (message, stack) are not enumerable, so getting them directly
296
323
  if (extra instanceof Error) {
297
- const names = Object.getOwnPropertyNames(extra)
298
- if (names) {
299
- for (let i = 0; i < names.length; i++) {
300
- obj[names[i]] = extra[names[i]]
301
- }
302
- }
324
+ decorateFromErrorInstance(obj, extra)
325
+ } else if (extra?.error != null) {
326
+ obj.error = {}
327
+ decorateFromErrorInstance(obj.error, extra.error)
303
328
  }
304
329
  return obj
305
330
  }
331
+
332
+ function decorateFromErrorInstance(toDecorate, errorInstance) {
333
+ const names = Object.getOwnPropertyNames(errorInstance)
334
+ for (const name of names) {
335
+ toDecorate[name] = errorInstance[name]
336
+ }
337
+ }
@@ -19,10 +19,12 @@ module.exports.clearCache = function clearAWSCache() {
19
19
 
20
20
  function fetchAWSInfo(agent, callback) {
21
21
  if (!agent.config.utilization || !agent.config.utilization.detect_aws) {
22
+ logger.trace({ utilization: 'aws' }, 'Skipping AWS due to being disabled via config.')
22
23
  return setImmediate(callback, null)
23
24
  }
24
25
 
25
26
  if (results) {
27
+ logger.trace({ utilization: 'aws' }, 'Returning previously found results.')
26
28
  return setImmediate(callback, null, results)
27
29
  }
28
30
 
@@ -35,7 +37,7 @@ function fetchAWSInfo(agent, callback) {
35
37
  }
36
38
  common.request(authTokenOpts, agent, function getAuthToken(err, authToken) {
37
39
  if (err) {
38
- logger.debug('Failed to get AWS auth token.')
40
+ logger.debug({ utilization: 'aws' }, 'Failed to get AWS auth token.')
39
41
  return callback(err)
40
42
  }
41
43
 
@@ -49,19 +51,20 @@ function fetchAWSInfo(agent, callback) {
49
51
 
50
52
  common.request(metadataOpts, agent, function getMetadata(metaErr, data) {
51
53
  if (metaErr) {
54
+ logger.trace({ utilization: 'aws' }, 'Returning due to error from metadata service.')
52
55
  return callback(metaErr)
53
56
  }
54
57
 
55
58
  try {
56
59
  data = JSON.parse(data)
57
60
  } catch (e) {
58
- logger.debug(e, 'Failed to parse AWS metadata.')
61
+ logger.debug({ utilization: 'aws', error: e }, 'Failed to parse AWS metadata.')
59
62
  data = null
60
63
  }
61
64
 
62
65
  results = common.getKeys(data, ['availabilityZone', 'instanceId', 'instanceType'])
63
66
  if (results == null) {
64
- logger.debug('AWS metadata was invalid.')
67
+ logger.debug({ utilization: 'aws' }, 'AWS metadata was invalid.')
65
68
  agent.metrics.getOrCreateMetric(NAMES.UTILIZATION.AWS_ERROR).incrementCallCount()
66
69
  }
67
70
  callback(null, results)
@@ -17,10 +17,12 @@ module.exports.clearCache = function clearAzureCache() {
17
17
 
18
18
  function fetchAzureInfo(agent, callback) {
19
19
  if (!agent.config.utilization || !agent.config.utilization.detect_azure) {
20
+ logger.trace({ utilization: 'azure' }, 'Skipping Azure due to being disabled via config.')
20
21
  return setImmediate(callback, null, null)
21
22
  }
22
23
 
23
24
  if (results) {
25
+ logger.trace({ utilization: 'azure' }, 'Returning previously found results.')
24
26
  return setImmediate(callback, null, results)
25
27
  }
26
28
 
@@ -37,6 +39,7 @@ function fetchAzureInfo(agent, callback) {
37
39
  agent,
38
40
  function getMetadata(err, data) {
39
41
  if (err) {
42
+ logger.trace({ utilization: 'azure', error: err }, 'Failed to query metadata endpoint.')
40
43
  return callback(err)
41
44
  }
42
45
 
@@ -44,14 +47,14 @@ function fetchAzureInfo(agent, callback) {
44
47
  try {
45
48
  data = JSON.parse(data)
46
49
  } catch (e) {
47
- logger.debug(e, 'Failed to parse Azure metadata.')
50
+ logger.debug({ utilization: 'azure', error: e }, 'Failed to parse Azure metadata.')
48
51
  data = null
49
52
  }
50
53
 
51
54
  // Get out just the keys we care about.
52
55
  results = common.getKeys(data, ['location', 'name', 'vmId', 'vmSize'])
53
56
  if (results == null) {
54
- logger.debug('Azure metadata was invalid.')
57
+ logger.debug({ utilization: 'azure' }, 'Azure metadata was invalid.')
55
58
  agent.metrics.getOrCreateMetric(NAMES.UTILIZATION.AZURE_ERROR).incrementCallCount()
56
59
  }
57
60
 
@@ -16,17 +16,19 @@ module.exports.clearCache = function clearAzureFunctionCache() {
16
16
 
17
17
  function fetchAzureFunctionInfo(agent, callback) {
18
18
  if (!agent.config.utilization || !agent.config.utilization.detect_azurefunction) {
19
+ logger.trace({ utilization: 'azurefunction' }, 'Skipping Azure function due to being disabled via config.')
19
20
  return setImmediate(callback, null, null)
20
21
  }
21
22
 
22
23
  if (results) {
24
+ logger.trace({ utilization: 'azurefunction' }, 'Returning previously found results.')
23
25
  return setImmediate(callback, null, results)
24
26
  }
25
27
 
26
28
  // Detect if Azure Function.
27
29
  const { REGION_NAME, WEBSITE_OWNER_NAME, WEBSITE_SITE_NAME, WEBSITE_RESOURCE_GROUP } = process.env
28
30
  if (!REGION_NAME || !WEBSITE_OWNER_NAME || !WEBSITE_SITE_NAME) {
29
- logger.debug('Azure Function metadata was invalid.')
31
+ logger.debug({ utilization: 'azurefunction' }, 'Azure Function metadata was invalid.')
30
32
  agent.metrics.getOrCreateMetric(NAMES.UTILIZATION.AZURE_ERROR).incrementCallCount()
31
33
  return setImmediate(callback, null, null)
32
34
  }
@@ -28,7 +28,7 @@ function clearDockerVendorCache() {
28
28
 
29
29
  function getBootId(agent, callback, logger = log) {
30
30
  if (!/linux/i.test(os.platform())) {
31
- logger.debug('Platform is not a flavor of linux, omitting boot info')
31
+ logger.debug({ utilization: 'docker' }, 'Platform is not a flavor of linux, omitting boot info')
32
32
  return setImmediate(callback, null, null)
33
33
  }
34
34
 
@@ -40,7 +40,7 @@ function getBootId(agent, callback, logger = log) {
40
40
  })
41
41
  }
42
42
 
43
- logger.debug('Container boot id is not available in cgroups info')
43
+ logger.debug({ utilization: 'docker' }, 'Container boot id is not available in cgroups info')
44
44
  callback(null, null)
45
45
  })
46
46
  }
@@ -98,6 +98,7 @@ function readProcBootId({ data, agent, callback }) {
98
98
  */
99
99
  function fetchDockerVendorInfo(agent, callback, logger = log) {
100
100
  if (!agent.config.utilization || !agent.config.utilization.detect_docker) {
101
+ logger.trace({ utilization: 'docker' }, 'Skipping Docker due to being disabled via config.')
101
102
  return callback(null, null)
102
103
  }
103
104
 
@@ -106,7 +107,7 @@ function fetchDockerVendorInfo(agent, callback, logger = log) {
106
107
  }
107
108
 
108
109
  if (!os.platform().match(/linux/i)) {
109
- logger.debug('Platform is not a flavor of linux, omitting docker info')
110
+ logger.debug({ utilization: 'docker' }, 'Platform is not a flavor of linux, omitting docker info')
110
111
  return callback(null, null)
111
112
  }
112
113
 
@@ -114,6 +115,7 @@ function fetchDockerVendorInfo(agent, callback, logger = log) {
114
115
  common.readProc(CGROUPS_V2_PATH, function getV2CGroup(_, data) {
115
116
  if (data === null) {
116
117
  logger.debug(
118
+ { utilization: 'docker' },
117
119
  `${CGROUPS_V2_PATH} not found, trying to parse container id from ${CGROUPS_V1_PATH}`
118
120
  )
119
121
  findCGroupsV1(callback, logger)
@@ -126,13 +128,14 @@ function fetchDockerVendorInfo(agent, callback, logger = log) {
126
128
  if (v2Data !== null) {
127
129
  // We found a valid Docker identifier in the v2 file, so we are going
128
130
  // to prioritize it.
131
+ logger.debug({ utilization: 'docker', v2Data }, 'Found identifier in cgroups v2 file.')
129
132
  return callback(null, v2Data)
130
133
  }
131
134
 
132
135
  // For some reason, we have a /proc/self/mountinfo but it does not have
133
136
  // any Docker information in it (that we have detected). So we will
134
137
  // fall back to trying the cgroups v1 file.
135
- logger.debug('Attempting to fall back to cgroups v1 parsing.')
138
+ logger.debug({ utilization: 'docker' }, 'Attempting to fall back to cgroups v1 parsing.')
136
139
  findCGroupsV1(callback, logger)
137
140
  },
138
141
  logger
@@ -152,10 +155,10 @@ function parseCGroupsV2(data, callback, logger = log) {
152
155
  const containerLine = /\/docker\/containers\/([0-9a-f]{64})\//
153
156
  const line = containerLine.exec(data)
154
157
  if (line) {
155
- logger.debug(`Found docker id from cgroups v2: ${line[1]}`)
158
+ logger.debug({ utilization: 'docker' }, `Found docker id from cgroups v2: ${line[1]}`)
156
159
  callback(null, { id: line[1] })
157
160
  } else {
158
- logger.debug(`Found ${CGROUPS_V2_PATH} but failed to parse Docker container id.`)
161
+ logger.debug({ utilization: 'docker' }, `Found ${CGROUPS_V2_PATH} but failed to parse Docker container id.`)
159
162
  callback(null, null)
160
163
  }
161
164
  }
@@ -170,7 +173,7 @@ function parseCGroupsV2(data, callback, logger = log) {
170
173
  function findCGroupsV1(callback, logger = log) {
171
174
  common.readProc(CGROUPS_V1_PATH, function getCGroup(_, data) {
172
175
  if (!data) {
173
- logger.debug(`${CGROUPS_V1_PATH} not found, exiting parsing containerId.`)
176
+ logger.debug({ utilization: 'docker' }, `${CGROUPS_V1_PATH} not found, exiting parsing containerId.`)
174
177
  return callback(null)
175
178
  }
176
179
 
@@ -187,10 +190,10 @@ function findCGroupsV1(callback, logger = log) {
187
190
 
188
191
  if (id) {
189
192
  vendorInfo = { id }
190
- logger.debug(`Found docker id from cgroups v1: ${id}`)
193
+ logger.debug({ utilization: 'docker' }, `Found docker id from cgroups v1: ${id}`)
191
194
  callback(null, vendorInfo)
192
195
  } else {
193
- logger.debug('No matching cpu group found.')
196
+ logger.debug({ utilization: 'docker' }, 'No matching cpu group found.')
194
197
  callback(null, null)
195
198
  }
196
199
  })
@@ -21,11 +21,12 @@ module.exports = function fetchEcsInfo(
21
21
  // Per spec, we do not have a `detect_ecs` key. Since ECS is a service of AWS,
22
22
  // we rely on the `detect_aws` setting.
23
23
  if (!agent.config.utilization || !agent.config.utilization.detect_aws) {
24
+ logger.trace({ utilization: 'ecs' }, 'Skipping ECS due to being disabled via config.')
24
25
  return setImmediate(callback, null)
25
26
  }
26
27
 
27
28
  if (hasAwsContainerApi() === false) {
28
- logger.debug('ECS API not available, omitting ECS container id info')
29
+ logger.debug({ utilization: 'ecs' }, 'ECS API not available, omitting ECS container id info')
29
30
  recordIdError(agent)
30
31
  return callback(null, null)
31
32
  }
@@ -70,13 +71,13 @@ function _getEcsContainerId({ agent, callback, logger, recordIdError }) {
70
71
  const json = body.toString('utf8')
71
72
  const data = JSON.parse(json)
72
73
  if (data.DockerId == null) {
73
- logger.debug('Failed to find DockerId in response, omitting boot info')
74
+ logger.debug({ utilization: 'ecs' }, 'Failed to find DockerId in response, omitting boot info')
74
75
  recordIdError(agent)
75
76
  return callback(null, null)
76
77
  }
77
78
  callback(null, data.DockerId)
78
79
  } catch (error) {
79
- logger.debug('Failed to process ECS API response, omitting boot info: ' + error.message)
80
+ logger.debug({ utilization: 'ecs' }, 'Failed to process ECS API response, omitting boot info: ' + error.message)
80
81
  recordIdError(agent)
81
82
  callback(null, null)
82
83
  }
@@ -84,7 +85,7 @@ function _getEcsContainerId({ agent, callback, logger, recordIdError }) {
84
85
  })
85
86
 
86
87
  req.on('error', () => {
87
- logger.debug('Failed to query ECS endpoint, omitting boot info')
88
+ logger.debug({ utilization: 'ecs' }, 'Failed to query ECS endpoint, omitting boot info')
88
89
  recordIdError(agent)
89
90
  callback(null, null)
90
91
  })
@@ -18,10 +18,12 @@ module.exports.clearCache = function clearGCPCache() {
18
18
 
19
19
  function fetchGCPInfo(agent, callback) {
20
20
  if (!agent.config.utilization || !agent.config.utilization.detect_gcp) {
21
+ logger.trace({ utilization: 'gcp' }, 'Skipping GCP due to being disabled via config.')
21
22
  return setImmediate(callback, null)
22
23
  }
23
24
 
24
25
  if (resultDict) {
26
+ logger.trace({ utilization: 'gcp' }, 'Returning previously found results.')
25
27
  return setImmediate(callback, null, resultDict)
26
28
  }
27
29
 
@@ -36,6 +38,7 @@ function fetchGCPInfo(agent, callback) {
36
38
  agent,
37
39
  function getMetadata(err, data) {
38
40
  if (err) {
41
+ logger.trace({ utilization: 'gcp', error: err }, 'Failed to communicate with metadata endpoint.')
39
42
  return callback(err)
40
43
  }
41
44
  try {
@@ -44,13 +47,13 @@ function fetchGCPInfo(agent, callback) {
44
47
  data.id = data.id.toString()
45
48
  }
46
49
  } catch (e) {
47
- logger.debug(e, 'Failed to parse GCP metadata.')
50
+ logger.debug({ utilization: 'gcp', error: e }, 'Failed to parse GCP metadata.')
48
51
  data = null
49
52
  }
50
53
 
51
54
  const results = common.getKeys(data, ['id', 'machineType', 'name', 'zone'])
52
55
  if (results == null) {
53
- logger.debug('GCP metadata was invalid.')
56
+ logger.debug({ utilization: 'gcp' }, 'GCP metadata was invalid.')
54
57
  agent.metrics.getOrCreateMetric(NAMES.UTILIZATION.GCP_ERROR).incrementCallCount()
55
58
  } else {
56
59
  // normalize
@@ -24,8 +24,9 @@ function getVendors(agent, callback) {
24
24
  let done = 0
25
25
  let vendors = null
26
26
  VENDOR_NAMES.forEach(function getVendorInfo(vendor) {
27
+ logger.trace({ utilization: vendor }, 'Detecting utilization info for vendor %s', vendor)
27
28
  VENDOR_METHODS[vendor](agent, function getInfo(_, result) {
28
- logger.trace('Vendor %s finished.', vendor)
29
+ logger.trace({ utilization: vendor, data: { result } }, 'Vendor %s finished.', vendor)
29
30
  if (result) {
30
31
  vendors = vendors || Object.create(null)
31
32
  vendors[vendor] = result
@@ -9,21 +9,23 @@ const logger = require('../logger').child({ component: 'kubernetes-info' })
9
9
  let info = null
10
10
 
11
11
  module.exports = getKubernetesInfo
12
- module.exports.clearCache = function clearAWSCache() {
12
+ module.exports.clearCache = function clearKubernetesCache() {
13
13
  info = null
14
14
  }
15
15
 
16
16
  function getKubernetesInfo(agent, callback) {
17
17
  if (!agent.config.utilization || !agent.config.utilization.detect_kubernetes) {
18
+ logger.trace({ utilization: 'kubernetes' }, 'Skipping Kubernetes due to being disabled via config.')
18
19
  return setImmediate(callback, null, null)
19
20
  }
20
21
 
21
22
  if (info) {
23
+ logger.trace({ utilization: 'kubernetes' }, 'Returning previously found results.')
22
24
  return setImmediate(callback, null, info)
23
25
  }
24
26
 
25
27
  if (!process.env.KUBERNETES_SERVICE_HOST) {
26
- logger.debug('No Kubernetes service host found.')
28
+ logger.debug({ utilization: 'kubernetes' }, 'No Kubernetes service host found.')
27
29
  return setImmediate(callback, null, null)
28
30
  }
29
31
 
@@ -13,6 +13,7 @@ module.exports = fetchPCFInfo
13
13
 
14
14
  function fetchPCFInfo(agent, callback) {
15
15
  if (!agent.config.utilization || !agent.config.utilization.detect_pcf) {
16
+ logger.trace({ utilization: 'pcf' }, 'Skipping PCF due to being disabled via config.')
16
17
  return setImmediate(callback, null, null)
17
18
  }
18
19
 
@@ -28,11 +29,11 @@ function fetchPCFInfo(agent, callback) {
28
29
  const key = keys[i]
29
30
  const value = process.env[key]
30
31
  if (value == null) {
31
- logger.trace('Could not find environment value for %s', key)
32
+ logger.trace({ utilization: 'pcf' }, 'Could not find environment value for %s', key)
32
33
  return setImmediate(callback, null, null)
33
34
  }
34
35
  if (!common.checkValueString(value)) {
35
- logger.trace('Invalid environment value for %s: %j', key, value)
36
+ logger.trace({ utilization: 'pcf' }, 'Invalid environment value for %s: %j', key, value)
36
37
  agent.metrics.getOrCreateMetric(NAMES.UTILIZATION.PCF_ERROR).incrementCallCount()
37
38
  return setImmediate(callback, null, null)
38
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "12.17.0",
3
+ "version": "12.18.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [
@@ -200,7 +200,7 @@
200
200
  "#test/assert": "./test/lib/custom-assertions/index.js"
201
201
  },
202
202
  "dependencies": {
203
- "@grpc/grpc-js": "^1.12.2",
203
+ "@grpc/grpc-js": "^1.13.2",
204
204
  "@grpc/proto-loader": "^0.7.5",
205
205
  "@newrelic/security-agent": "^2.3.0",
206
206
  "@opentelemetry/api": "^1.9.0",