newrelic 12.15.0 → 12.16.1
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 +46 -0
- package/README.md +36 -31
- package/api.js +2 -0
- package/lib/agent.js +5 -5
- package/lib/context-manager/context.js +1 -1
- package/lib/errors/error-collector.js +3 -2
- package/lib/instrumentation/kafkajs/consumer.js +1 -1
- package/lib/otel/constants.js +35 -2
- package/lib/otel/segments/internal.js +2 -2
- package/lib/otel/span-processor.js +68 -4
- package/lib/serverless/aws-lambda.js +119 -37
- package/lib/spans/helpers.js +69 -0
- package/lib/spans/span-event.js +14 -16
- package/lib/spans/streaming-span-event.js +11 -16
- package/lib/{sampler.js → system-metrics-sampler.js} +11 -10
- package/lib/transaction/index.js +6 -48
- package/lib/transaction/trace/segment.js +3 -0
- package/lib/transaction/tracer/index.js +2 -1
- package/package.json +1 -1
package/NEWS.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
+
### v12.16.1 (2025-03-24)
|
|
2
|
+
|
|
3
|
+
#### Features
|
|
4
|
+
|
|
5
|
+
* Assign all attributes on internal spans to segment ([#3000](https://github.com/newrelic/node-newrelic/pull/3000)) ([5403018](https://github.com/newrelic/node-newrelic/commit/54030185ba5630099b76182a52c629276af4d93e))
|
|
6
|
+
|
|
7
|
+
#### Bug fixes
|
|
8
|
+
|
|
9
|
+
* Fixed `tracer.transactionNestProxy` to create a new transaction if there is no transaction or transaction is not active ([#3007](https://github.com/newrelic/node-newrelic/pull/3007)) ([ced9e22](https://github.com/newrelic/node-newrelic/commit/ced9e22a23ddeed860c1fd88f0afb10bb94343cb))
|
|
10
|
+
|
|
11
|
+
#### Documentation
|
|
12
|
+
|
|
13
|
+
* Updated compatibility report ([#2993](https://github.com/newrelic/node-newrelic/pull/2993)) ([cdfa042](https://github.com/newrelic/node-newrelic/commit/cdfa042325a7636e84a7b19a34874b20915dac63))
|
|
14
|
+
|
|
15
|
+
#### Tests
|
|
16
|
+
|
|
17
|
+
* Fixed handling require esm for >20.19.0+ ([#3002](https://github.com/newrelic/node-newrelic/pull/3002)) ([b808e00](https://github.com/newrelic/node-newrelic/commit/b808e00c399d3c924eeeb19f9c6b9fba4695a720))
|
|
18
|
+
|
|
19
|
+
### v12.16.0 (2025-03-17)
|
|
20
|
+
|
|
21
|
+
#### Features
|
|
22
|
+
|
|
23
|
+
* Added support for response streaming Lambda functions ([#2981](https://github.com/newrelic/node-newrelic/pull/2981)) ([61dbbf9](https://github.com/newrelic/node-newrelic/commit/61dbbf9b4a6cf69f5378387fc9c17c31671e9da4))
|
|
24
|
+
* Added AWS entity linking segment attributes for otel bridge ([#2978](https://github.com/newrelic/node-newrelic/pull/2978)) ([6bf1ccc](https://github.com/newrelic/node-newrelic/commit/6bf1ccc657a955b4064a7a3a473bf24948d4ff56))
|
|
25
|
+
* Added error handling on transactions for otel spans ([#2985](https://github.com/newrelic/node-newrelic/pull/2985)) ([4e61e09](https://github.com/newrelic/node-newrelic/commit/4e61e0935394744345c39f6b581ee86e66d0f82c))
|
|
26
|
+
|
|
27
|
+
#### Code refactoring
|
|
28
|
+
|
|
29
|
+
* Updated span event generation to assign the appropriate `span.kind` based on the segment name ([#2976](https://github.com/newrelic/node-newrelic/pull/2976)) ([697b17e](https://github.com/newrelic/node-newrelic/commit/697b17e0553111aa494d08bc33eb7043cdfa8ca6))
|
|
30
|
+
|
|
31
|
+
#### Documentation
|
|
32
|
+
|
|
33
|
+
* Updated compatibility report ([#2988](https://github.com/newrelic/node-newrelic/pull/2988)) ([ed17a6d](https://github.com/newrelic/node-newrelic/commit/ed17a6df1152a8e54cb8c8570fec0015990a4247)
|
|
34
|
+
|
|
35
|
+
#### Miscellaneous chores
|
|
36
|
+
|
|
37
|
+
* Clarified supported next.js middleware versions in docs ([#2984](https://github.com/newrelic/node-newrelic/pull/2984)) ([15cb454](https://github.com/newrelic/node-newrelic/commit/15cb454f9cc38ccc22089d62aaeea54713159aa7))
|
|
38
|
+
* Clarified system metrics sampler naming ([#2987](https://github.com/newrelic/node-newrelic/pull/2987)) ([8647d43](https://github.com/newrelic/node-newrelic/commit/8647d43f097d6d3d68a372824d9feb325604be96))
|
|
39
|
+
* Refactored loops to be simpler ([#2990](https://github.com/newrelic/node-newrelic/pull/2990)) ([79fb8e9](https://github.com/newrelic/node-newrelic/commit/79fb8e90802954b617c1c00aecc866aa065aee12))
|
|
40
|
+
* Removed unused transaction method ([#2986](https://github.com/newrelic/node-newrelic/pull/2986)) ([cb4e2f7](https://github.com/newrelic/node-newrelic/commit/cb4e2f7a8b84adb6d744a2083b083c92e306fbd5))
|
|
41
|
+
* Reverted restriction in NestJS versioned tests ([#2979](https://github.com/newrelic/node-newrelic/pull/2979)) ([ffddcab](https://github.com/newrelic/node-newrelic/commit/ffddcab6d77bfc10c0df9cbfa724bc1c8f5fb251))
|
|
42
|
+
|
|
43
|
+
#### Tests
|
|
44
|
+
|
|
45
|
+
* Fixed fastify assertions around span kind while running security agent ([#2983](https://github.com/newrelic/node-newrelic/pull/2983)) ([c641645](https://github.com/newrelic/node-newrelic/commit/c6416451f1fa6126b7dfd59f6b9267f9d2188ad0))
|
|
46
|
+
|
|
1
47
|
### v12.15.0 (2025-03-03)
|
|
2
48
|
|
|
3
49
|
#### Features
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ To use New Relic's Node.js agent entails these three steps, which are described
|
|
|
31
31
|
|
|
32
32
|
3. Now, add your New Relic license key and application/service name to that file:
|
|
33
33
|
|
|
34
|
-
```js
|
|
34
|
+
```js
|
|
35
35
|
/* File: newrelic.js */
|
|
36
36
|
'use strict'
|
|
37
37
|
/**
|
|
@@ -45,30 +45,29 @@ To use New Relic's Node.js agent entails these three steps, which are described
|
|
|
45
45
|
license_key: 'your new relic license key',
|
|
46
46
|
/* ... rest of configuration .. */
|
|
47
47
|
}
|
|
48
|
-
```
|
|
48
|
+
```
|
|
49
49
|
|
|
50
50
|
4. Finally, run your program with the `newrelic` module loaded first by using node's `-r/--require` flag.
|
|
51
51
|
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
```
|
|
52
|
+
```sh
|
|
53
|
+
$ node -r newrelic your-program.js
|
|
54
|
+
```
|
|
55
55
|
|
|
56
|
-
If you cannot control how your program is run, you can load the `newrelic` module _before any other module_ in your program.
|
|
56
|
+
If you cannot control how your program is run, you can load the `newrelic` module _before any other module_ in your program.
|
|
57
57
|
|
|
58
|
-
```js
|
|
58
|
+
```js
|
|
59
59
|
const newrelic = require('newrelic')
|
|
60
60
|
|
|
61
61
|
/* ... the rest of your program ... */
|
|
62
|
-
```
|
|
62
|
+
```
|
|
63
63
|
|
|
64
64
|
## Next.js instrumentation
|
|
65
65
|
**Note**: The minimum supported Next.js version is [12.0.9](https://github.com/vercel/next.js/releases/tag/v12.0.9). If you are using Next.js middleware the minimum supported version is [12.2.0](https://github.com/vercel/next.js/releases/tag/v12.2.0).
|
|
66
66
|
|
|
67
|
-
The New Relic Node.js agent provides instrumentation for Next.js The instrumentation provides telemetry for server-side rendering via [getServerSideProps](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props), [middleware](https://nextjs.org/docs/middleware), and New Relic transaction naming for both page and server requests. It does not provide any instrumentation for actions occurring during build or in client-side code. If you want telemetry data on actions occurring on the client (browser), you can [inject the browser agent](./documentation/nextjs/faqs/browser-agent.md).
|
|
67
|
+
The New Relic Node.js agent provides instrumentation for Next.js The instrumentation provides telemetry for server-side rendering via [`getServerSideProps`](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props), [middleware](https://nextjs.org/docs/middleware) (limited to Next.js versions [12.2.0](https://github.com/vercel/next.js/releases/tag/v12.2.0) - [13.4.12](https://github.com/vercel/next.js/releases/tag/v13.4.12)), and New Relic transaction naming for both page and server requests. It does not provide any instrumentation for actions occurring during build or in client-side code. If you want telemetry data on actions occurring on the client (browser), you can [inject the browser agent](./documentation/nextjs/faqs/browser-agent.md).
|
|
68
68
|
|
|
69
69
|
Here are documents for more in-depth explanations about [transaction naming](./documentation/nextjs/transactions.md), and [segments/spans](./documentation/nextjs/segments-and-spans.md).
|
|
70
70
|
|
|
71
|
-
|
|
72
71
|
### Setup
|
|
73
72
|
Typically you are running a Next.js app with the `next` cli and you must load the agent via `NODE_OPTIONS`:
|
|
74
73
|
|
|
@@ -102,15 +101,15 @@ The New Relic Node.js agent includes ***_experimental_*** support for ES Modules
|
|
|
102
101
|
|
|
103
102
|
1. If you rely on a configuration file to run the agent, you must rename the file from `newrelic.js` to `newrelic.cjs` so it can be properly loaded. All the contents of the configuration file will behave the same once you rename. See [CommonJS modules in ESM](https://nodejs.org/api/modules.html#enabling) for more details.
|
|
104
103
|
|
|
105
|
-
```sh
|
|
106
|
-
$ mv newrelic.js newrelic.cjs
|
|
107
|
-
```
|
|
104
|
+
```sh
|
|
105
|
+
$ mv newrelic.js newrelic.cjs
|
|
106
|
+
```
|
|
108
107
|
|
|
109
108
|
2. To use the newrelic ESM loader, start your program with node and use the `--experimental-loader` flag and a path to the loader file, like this:
|
|
110
109
|
|
|
111
|
-
```sh
|
|
112
|
-
$ node --experimental-loader newrelic/esm-loader.mjs -r newrelic your-program.js
|
|
113
|
-
```
|
|
110
|
+
```sh
|
|
111
|
+
$ node --experimental-loader newrelic/esm-loader.mjs -r newrelic your-program.js
|
|
112
|
+
```
|
|
114
113
|
|
|
115
114
|
**Note**: Unlike the CommonJS methods listed above, there are no alternatives to running the agent without the `--experimental-loader` flag.
|
|
116
115
|
|
|
@@ -122,11 +121,11 @@ The agent supports adding your own custom instrumentation to ES module applicati
|
|
|
122
121
|
import newrelic from 'newrelic'
|
|
123
122
|
newrelic.instrument({ moduleName: 'parse-json', isEsm: true }, function wrap(shim, parseJson, moduleName) {
|
|
124
123
|
shim.wrap(parseJson.default, function wrapParseJson(shim, orig) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
124
|
+
return function wrappedParseJson() {
|
|
125
|
+
const result = orig.apply(this, arguments)
|
|
126
|
+
result.instrumented = true
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
130
129
|
})
|
|
131
130
|
})
|
|
132
131
|
```
|
|
@@ -151,11 +150,11 @@ For more information on getting started, [check the Node.js docs](https://docs.n
|
|
|
151
150
|
|
|
152
151
|
There are modules that can be installed and configured to accompany the Node.js agent:
|
|
153
152
|
|
|
154
|
-
* [
|
|
153
|
+
* [`@newrelic/apollo-server-plugin`](https://github.com/newrelic/newrelic-node-apollo-server-plugin): New Relic's official Apollo Server plugin for use with the Node.js agent.
|
|
155
154
|
|
|
156
155
|
There are modules included within the Node.js agent to add more instrumentation for 3rd party modules:
|
|
157
156
|
|
|
158
|
-
* [
|
|
157
|
+
* [`@newrelic/native-metrics`](https://github.com/newrelic/node-native-metrics): Provides hooks into the native v8 layer of Node.js to provide metrics to the Node.js agent.
|
|
159
158
|
|
|
160
159
|
## Usage
|
|
161
160
|
|
|
@@ -164,10 +163,10 @@ There are modules included within the Node.js agent to add more instrumentation
|
|
|
164
163
|
The `newrelic` module returns an object with the Node.js agent's API methods attached.
|
|
165
164
|
|
|
166
165
|
```js
|
|
167
|
-
|
|
166
|
+
const newrelic = require('newrelic')
|
|
168
167
|
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
/* ... */
|
|
169
|
+
newrelic.addCustomAttribute('some-attribute', 'some-value')
|
|
171
170
|
```
|
|
172
171
|
|
|
173
172
|
You can read more about using the API over on the [New Relic documentation](https://docs.newrelic.com/docs/agents/nodejs-agent/api-guides/guide-using-nodejs-agent-api) site.
|
|
@@ -182,12 +181,16 @@ These are the steps to work on core agent features, with more detail below:
|
|
|
182
181
|
|
|
183
182
|
1. [Fork](https://github.com/newrelic/node-newrelic/fork) and clone this GitHub repository:
|
|
184
183
|
|
|
184
|
+
```sh
|
|
185
185
|
$ git clone git@github.com:your-user-name/node-newrelic.git
|
|
186
186
|
$ cd node-newrelic
|
|
187
|
+
```
|
|
187
188
|
|
|
188
189
|
2. Install the project's dependencies:
|
|
189
190
|
|
|
191
|
+
```sh
|
|
190
192
|
$ npm install
|
|
193
|
+
```
|
|
191
194
|
|
|
192
195
|
Then you're all set to start programming.
|
|
193
196
|
|
|
@@ -199,11 +202,13 @@ Then you're all set to start programming.
|
|
|
199
202
|
|
|
200
203
|
Available test suites include:
|
|
201
204
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
```sh
|
|
206
|
+
$ npm run unit
|
|
207
|
+
$ npm run integration
|
|
208
|
+
$ npm run versioned
|
|
209
|
+
$ npm run lint
|
|
210
|
+
$ npm run smoke
|
|
211
|
+
```
|
|
207
212
|
|
|
208
213
|
## Further Reading
|
|
209
214
|
|
package/api.js
CHANGED
|
@@ -992,6 +992,7 @@ API.prototype.startWebTransaction = function startWebTransaction(url, handle) {
|
|
|
992
992
|
transaction: tx,
|
|
993
993
|
parent
|
|
994
994
|
})
|
|
995
|
+
tx.baseSegment.spanKind = 'server'
|
|
995
996
|
const newContext = context.enterSegment({ transaction: tx, segment: tx.baseSegment })
|
|
996
997
|
tx.baseSegment.start()
|
|
997
998
|
|
|
@@ -1101,6 +1102,7 @@ function startBackgroundTransaction(name, group, handle) {
|
|
|
1101
1102
|
parent
|
|
1102
1103
|
})
|
|
1103
1104
|
const newContext = context.enterSegment({ transaction: tx, segment: tx.baseSegment })
|
|
1105
|
+
tx.baseSegment.spanKind = 'server'
|
|
1104
1106
|
tx.baseSegment.partialName = group
|
|
1105
1107
|
tx.baseSegment.start()
|
|
1106
1108
|
|
package/lib/agent.js
CHANGED
|
@@ -21,7 +21,7 @@ const MetricNormalizer = require('./metrics/normalizer')
|
|
|
21
21
|
const MetricAggregator = require('./metrics/metric-aggregator')
|
|
22
22
|
const NAMES = require('./metrics/names')
|
|
23
23
|
const QueryTraceAggregator = require('./db/query-trace-aggregator')
|
|
24
|
-
const
|
|
24
|
+
const systemMetricsSampler = require('./system-metrics-sampler')
|
|
25
25
|
const TransactionTraceAggregator = require('./transaction/trace/aggregator')
|
|
26
26
|
const TransactionEventAggregator = require('./transaction/transaction-event-aggregator')
|
|
27
27
|
const Tracer = require('./transaction/tracer')
|
|
@@ -334,7 +334,7 @@ Agent.prototype.start = function start(callback) {
|
|
|
334
334
|
return process.nextTick(callback)
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
|
|
337
|
+
systemMetricsSampler.start(agent)
|
|
338
338
|
|
|
339
339
|
if (this.config.serverless_mode.enabled) {
|
|
340
340
|
return this._serverlessModeStart(callback)
|
|
@@ -349,7 +349,7 @@ Agent.prototype.start = function start(callback) {
|
|
|
349
349
|
this.healthReporter.setStatus(HealthReporter.STATUS_LICENSE_KEY_MISSING)
|
|
350
350
|
|
|
351
351
|
this.setState('errored')
|
|
352
|
-
|
|
352
|
+
systemMetricsSampler.stop()
|
|
353
353
|
return process.nextTick(function onNextTick() {
|
|
354
354
|
agent.healthReporter.stop(() => {
|
|
355
355
|
callback(new Error('Not starting without license key!'))
|
|
@@ -362,7 +362,7 @@ Agent.prototype.start = function start(callback) {
|
|
|
362
362
|
if (error || response.shouldShutdownRun()) {
|
|
363
363
|
agent.healthReporter.setStatus(HealthReporter.STATUS_CONNECT_ERROR)
|
|
364
364
|
agent.setState('errored')
|
|
365
|
-
|
|
365
|
+
systemMetricsSampler.stop()
|
|
366
366
|
callback(error || new Error('Failed to connect to collector'), response && response.payload)
|
|
367
367
|
return
|
|
368
368
|
}
|
|
@@ -482,7 +482,7 @@ Agent.prototype.stop = function stop(callback) {
|
|
|
482
482
|
|
|
483
483
|
this.harvester.stop()
|
|
484
484
|
|
|
485
|
-
|
|
485
|
+
systemMetricsSampler.stop()
|
|
486
486
|
|
|
487
487
|
this.healthReporter.setStatus(HealthReporter.STATUS_AGENT_SHUTDOWN)
|
|
488
488
|
this.healthReporter.stop(() => {
|
|
@@ -29,7 +29,7 @@ module.exports = class Context {
|
|
|
29
29
|
* @returns {Context} a newly constructed context
|
|
30
30
|
*/
|
|
31
31
|
enterSegment({ segment, transaction = this._transaction }) {
|
|
32
|
-
return new this.constructor(transaction, segment
|
|
32
|
+
return new this.constructor(transaction, segment)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
@@ -215,8 +215,9 @@ class ErrorCollector {
|
|
|
215
215
|
* @param {?Transaction} transaction Transaction associated with the error.
|
|
216
216
|
* @param {Error} error The error to be traced.
|
|
217
217
|
* @param {?object} customAttributes Custom attributes associated with the request (optional).
|
|
218
|
+
* @param {object} segment The segment associated with the error (optional). Only used in otel bridge.
|
|
218
219
|
*/
|
|
219
|
-
add(transaction, error, customAttributes) {
|
|
220
|
+
add(transaction, error, customAttributes, segment) {
|
|
220
221
|
if (!error) {
|
|
221
222
|
return
|
|
222
223
|
}
|
|
@@ -236,7 +237,7 @@ class ErrorCollector {
|
|
|
236
237
|
const exception = new Exception({ error, timestamp, customAttributes })
|
|
237
238
|
|
|
238
239
|
if (transaction) {
|
|
239
|
-
transaction.addException(exception)
|
|
240
|
+
transaction.addException(exception, segment)
|
|
240
241
|
} else {
|
|
241
242
|
this.collect(transaction, exception)
|
|
242
243
|
}
|
package/lib/otel/constants.js
CHANGED
|
@@ -61,6 +61,14 @@ module.exports = {
|
|
|
61
61
|
*/
|
|
62
62
|
ATTR_DB_SYSTEM: 'db.system',
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* The cloud provider of the invoked function.
|
|
66
|
+
*
|
|
67
|
+
* @example aws
|
|
68
|
+
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/v1.7.0/specification/trace/semantic_conventions/faas.md#outgoing-invocations
|
|
69
|
+
*/
|
|
70
|
+
ATTR_FAAS_INVOKED_PROVIDER: 'faas.invoked_provider',
|
|
71
|
+
|
|
64
72
|
/**
|
|
65
73
|
* The full resource URL.
|
|
66
74
|
*
|
|
@@ -182,7 +190,8 @@ module.exports = {
|
|
|
182
190
|
* Target messaging system name.
|
|
183
191
|
*
|
|
184
192
|
* @example kafka
|
|
185
|
-
* @
|
|
193
|
+
* @example aws_sqs
|
|
194
|
+
* @see https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/#messaging-attributes
|
|
186
195
|
*/
|
|
187
196
|
ATTR_MESSAGING_SYSTEM: 'messaging.system',
|
|
188
197
|
|
|
@@ -233,7 +242,7 @@ module.exports = {
|
|
|
233
242
|
ATTR_NET_HOST_NAME: 'net.host.name',
|
|
234
243
|
|
|
235
244
|
/**
|
|
236
|
-
*
|
|
245
|
+
* Port of the local HTTP server that received the request.
|
|
237
246
|
*
|
|
238
247
|
* @example 80
|
|
239
248
|
*/
|
|
@@ -322,11 +331,35 @@ module.exports = {
|
|
|
322
331
|
VERTICA: 'vertica',
|
|
323
332
|
},
|
|
324
333
|
|
|
334
|
+
/**
|
|
335
|
+
* The exception message
|
|
336
|
+
*/
|
|
337
|
+
EXCEPTION_MESSAGE: 'exception.message',
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* The exception stacktrace
|
|
341
|
+
*/
|
|
342
|
+
EXCEPTION_STACKTRACE: 'exception.stacktrace',
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* The exception type
|
|
346
|
+
*/
|
|
347
|
+
EXCEPTION_TYPE: 'exception.type',
|
|
348
|
+
|
|
325
349
|
/**
|
|
326
350
|
* Kinds of messaging system destinations.
|
|
327
351
|
*/
|
|
328
352
|
MESSAGING_SYSTEM_KIND_VALUES: {
|
|
329
353
|
QUEUE: 'queue',
|
|
330
354
|
TOPIC: 'topic'
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* An enumeration of status codes
|
|
359
|
+
*/
|
|
360
|
+
SPAN_STATUS_CODE: {
|
|
361
|
+
UNSET: 0,
|
|
362
|
+
OK: 1,
|
|
363
|
+
ERROR: 2
|
|
331
364
|
}
|
|
332
365
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
'use strict'
|
|
7
|
-
const
|
|
7
|
+
const genericRecorder = require('../../metrics/recorders/generic')
|
|
8
8
|
|
|
9
9
|
module.exports = function createInternalSegment(agent, otelSpan) {
|
|
10
10
|
const context = agent.tracer.getContext()
|
|
@@ -13,7 +13,7 @@ module.exports = function createInternalSegment(agent, otelSpan) {
|
|
|
13
13
|
id: otelSpan?.spanContext()?.spanId,
|
|
14
14
|
name,
|
|
15
15
|
parent: context.segment,
|
|
16
|
-
recorder:
|
|
16
|
+
recorder: genericRecorder,
|
|
17
17
|
transaction: context.transaction
|
|
18
18
|
})
|
|
19
19
|
return { segment, transaction: context.transaction }
|
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
ATTR_DB_NAME,
|
|
17
17
|
ATTR_DB_STATEMENT,
|
|
18
18
|
ATTR_DB_SYSTEM,
|
|
19
|
+
ATTR_FAAS_INVOKED_PROVIDER,
|
|
19
20
|
ATTR_FULL_URL,
|
|
20
21
|
ATTR_GRPC_STATUS_CODE,
|
|
21
22
|
ATTR_HTTP_METHOD,
|
|
@@ -34,9 +35,15 @@ const {
|
|
|
34
35
|
ATTR_NET_PEER_NAME,
|
|
35
36
|
ATTR_NET_PEER_PORT,
|
|
36
37
|
ATTR_RPC_SYSTEM,
|
|
38
|
+
ATTR_RPC_SERVICE,
|
|
37
39
|
ATTR_SERVER_ADDRESS,
|
|
38
40
|
ATTR_SERVER_PORT,
|
|
39
41
|
ATTR_URL_QUERY,
|
|
42
|
+
DB_SYSTEM_VALUES,
|
|
43
|
+
EXCEPTION_MESSAGE,
|
|
44
|
+
EXCEPTION_STACKTRACE,
|
|
45
|
+
EXCEPTION_TYPE,
|
|
46
|
+
SPAN_STATUS_CODE
|
|
40
47
|
} = require('./constants')
|
|
41
48
|
const { DESTINATIONS } = require('../config/attribute-filter')
|
|
42
49
|
|
|
@@ -62,19 +69,33 @@ module.exports = class NrSpanProcessor {
|
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
/**
|
|
65
|
-
* Update the segment duration from span and
|
|
66
|
-
* any attributes that were added after the start
|
|
72
|
+
* Update the segment duration from span, handle errors and
|
|
73
|
+
* reconcile any attributes that were added after the start
|
|
67
74
|
* @param {object} span otel span getting updated
|
|
68
75
|
*/
|
|
69
76
|
onEnd(span) {
|
|
70
77
|
if (span[otelSynthesis] && span[otelSynthesis].segment) {
|
|
71
78
|
const { segment, transaction } = span[otelSynthesis]
|
|
72
79
|
this.updateDuration(segment, span)
|
|
80
|
+
this.handleError({ segment, transaction, span })
|
|
73
81
|
this.reconcileAttributes({ segment, span, transaction })
|
|
74
82
|
delete span[otelSynthesis]
|
|
75
83
|
}
|
|
76
84
|
}
|
|
77
85
|
|
|
86
|
+
handleError({ segment, transaction, span }) {
|
|
87
|
+
if (span?.status?.code === SPAN_STATUS_CODE.ERROR) {
|
|
88
|
+
const errorEvents = span.events.filter((event) => event.name === 'exception')
|
|
89
|
+
errorEvents.forEach((err) => {
|
|
90
|
+
const msg = err.attributes[EXCEPTION_MESSAGE] || err.attributes[EXCEPTION_TYPE] || `Error from ${segment.name}`
|
|
91
|
+
const stack = err.attributes[EXCEPTION_STACKTRACE]
|
|
92
|
+
const error = new Error(msg)
|
|
93
|
+
error.stack = stack
|
|
94
|
+
this.agent.errors.add(transaction, error, null, segment)
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
78
99
|
updateDuration(segment, span) {
|
|
79
100
|
segment.touch()
|
|
80
101
|
const duration = hrTimeToMilliseconds(span.duration)
|
|
@@ -92,11 +113,15 @@ module.exports = class NrSpanProcessor {
|
|
|
92
113
|
this.reconcileProducerAttributes({ segment, span })
|
|
93
114
|
} else if (span.kind === SpanKind.CLIENT && (span.attributes[ATTR_HTTP_METHOD] || span.attributes[ATTR_HTTP_REQUEST_METHOD])) {
|
|
94
115
|
this.reconcileHttpExternalAttributes({ segment, span })
|
|
116
|
+
} else {
|
|
117
|
+
this.#reconciler.reconcile({ segment, otelSpan: span })
|
|
95
118
|
}
|
|
119
|
+
|
|
120
|
+
this.addAWSLinkingAttributes({ segment, span })
|
|
96
121
|
}
|
|
97
122
|
|
|
98
123
|
reconcileHttpExternalAttributes({ segment, span }) {
|
|
99
|
-
const noOpMapper = () => {}
|
|
124
|
+
const noOpMapper = () => { }
|
|
100
125
|
const statusCode = (value) => segment.addSpanAttribute('http.statusCode', value)
|
|
101
126
|
const statusText = (value) => segment.addSpanAttribute('http.statusText', value)
|
|
102
127
|
const mapper = {
|
|
@@ -147,6 +172,7 @@ module.exports = class NrSpanProcessor {
|
|
|
147
172
|
[ATTR_MESSAGING_DESTINATION_NAME]: queueNameMapper,
|
|
148
173
|
[ATTR_MESSAGING_DESTINATION]: queueNameMapper
|
|
149
174
|
}
|
|
175
|
+
|
|
150
176
|
this.#reconciler.reconcile({ segment: baseSegment, otelSpan: span, mapper })
|
|
151
177
|
|
|
152
178
|
transaction.end()
|
|
@@ -240,7 +266,7 @@ module.exports = class NrSpanProcessor {
|
|
|
240
266
|
* to be skipped because it will be the raw value.
|
|
241
267
|
*/
|
|
242
268
|
},
|
|
243
|
-
[ATTR_DB_STATEMENT]: () => {}
|
|
269
|
+
[ATTR_DB_STATEMENT]: () => { }
|
|
244
270
|
}
|
|
245
271
|
this.#reconciler.reconcile({ segment, otelSpan: span, mapper })
|
|
246
272
|
}
|
|
@@ -252,6 +278,44 @@ module.exports = class NrSpanProcessor {
|
|
|
252
278
|
[ATTR_MESSAGING_MESSAGE_CONVERSATION_ID]: (value) => segment.addAttribute('correlation_id', value),
|
|
253
279
|
[ATTR_MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY]: (value) => segment.addAttribute('routing_key', value)
|
|
254
280
|
}
|
|
281
|
+
|
|
255
282
|
this.#reconciler.reconcile({ segment, otelSpan: span, mapper })
|
|
256
283
|
}
|
|
284
|
+
|
|
285
|
+
addAWSLinkingAttributes({ segment, span }) {
|
|
286
|
+
const accountId = this.agent?.config?.cloud?.aws?.account_id
|
|
287
|
+
const region = span.attributes['faas.invoked_region'] ?? span.attributes['aws.region'] // faas.invoked_region is for Lambda and takes precedence
|
|
288
|
+
|
|
289
|
+
// DynamoDB
|
|
290
|
+
if (span.attributes[ATTR_DB_SYSTEM] === DB_SYSTEM_VALUES.DYNAMODB ||
|
|
291
|
+
span.attributes[ATTR_RPC_SERVICE]?.toLowerCase() === 'dynamodb'
|
|
292
|
+
) {
|
|
293
|
+
const collection = span.attributes?.['aws.dynamodb.table_names'][0]
|
|
294
|
+
if (region && accountId && collection) {
|
|
295
|
+
segment.addAttribute('cloud.resource_id', `arn:aws:dynamodb:${region}:${accountId}:table/${collection}`)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Lambda
|
|
300
|
+
if (span.attributes[ATTR_FAAS_INVOKED_PROVIDER] === 'aws') {
|
|
301
|
+
const functionName = span.attributes['faas.invoked_name']
|
|
302
|
+
if (region && accountId && functionName) {
|
|
303
|
+
segment.addAttribute('cloud.platform', 'aws_lambda')
|
|
304
|
+
segment.addAttribute('cloud.resource_id', `arn:aws:lambda:${region}:${accountId}:function:${functionName}`)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// SQS
|
|
309
|
+
if (span.attributes[ATTR_RPC_SERVICE]?.toLowerCase() === 'sqs') {
|
|
310
|
+
if (accountId) {
|
|
311
|
+
segment.addAttribute('cloud.account.id', accountId)
|
|
312
|
+
}
|
|
313
|
+
if (region) {
|
|
314
|
+
segment.addAttribute('cloud.region', region)
|
|
315
|
+
}
|
|
316
|
+
const messagingDestination = span.attributes[ATTR_MESSAGING_DESTINATION]
|
|
317
|
+
segment.addAttribute('messaging.destination.name', messagingDestination)
|
|
318
|
+
segment.addAttribute('messaging.system', 'aws_sqs')
|
|
319
|
+
}
|
|
320
|
+
}
|
|
257
321
|
}
|
|
@@ -24,6 +24,7 @@ const EVENT_SOURCE_TYPE_KEY = `${EVENT_SOURCE_PREFIX}.eventType`
|
|
|
24
24
|
const NAMES = require('../metrics/names')
|
|
25
25
|
|
|
26
26
|
const EVENT_SOURCE_INFO = require('./event-sources')
|
|
27
|
+
const HANDLER_STREAMING = Symbol.for('aws.lambda.runtime.handler.streaming')
|
|
27
28
|
|
|
28
29
|
// A function with no references used to stub out closures
|
|
29
30
|
function cleanClosure() {}
|
|
@@ -35,7 +36,7 @@ let transactionEnders = []
|
|
|
35
36
|
// the invocation transaction.
|
|
36
37
|
let uncaughtException = null
|
|
37
38
|
|
|
38
|
-
// Tracking the first time patchLambdaHandler is called for one
|
|
39
|
+
// Tracking the first time patchLambdaHandler is called for one-off functionality
|
|
39
40
|
let patchCalled = false
|
|
40
41
|
let coldStartRecorded = false
|
|
41
42
|
|
|
@@ -105,6 +106,45 @@ class AwsLambda {
|
|
|
105
106
|
})
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Response-streaming handlers are identified by symbol properties on the function.
|
|
111
|
+
* We propagate any symbols if they're present, so that the handler keeps its signatue for any AWS features that rely on symbols
|
|
112
|
+
* @param handler
|
|
113
|
+
* @param nrHandler
|
|
114
|
+
*/
|
|
115
|
+
propagateSymbols(handler, nrHandler) {
|
|
116
|
+
for (const symbol of Object.getOwnPropertySymbols(handler)) {
|
|
117
|
+
logger.trace(`Setting symbol ${symbol.toString()} on handler`)
|
|
118
|
+
nrHandler[symbol] = handler[symbol]
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
createSegment({ event, context, transaction, recorder }) {
|
|
123
|
+
const shim = this.shim
|
|
124
|
+
const functionName = context.functionName
|
|
125
|
+
const group = NAMES.FUNCTION.PREFIX
|
|
126
|
+
const transactionName = group + functionName
|
|
127
|
+
|
|
128
|
+
const activeSegment = shim.tracer.getSegment()
|
|
129
|
+
|
|
130
|
+
transaction.setPartialName(transactionName)
|
|
131
|
+
const txnEnder = endTransaction.bind(null, transaction, transactionEnders.length)
|
|
132
|
+
|
|
133
|
+
transactionEnders.push(txnEnder)
|
|
134
|
+
const segment = shim.createSegment(functionName, recorder, activeSegment)
|
|
135
|
+
transaction.baseSegment = segment
|
|
136
|
+
const awsAttributes = this._getAwsAgentAttributes(event, context)
|
|
137
|
+
transaction.trace.attributes.addAttributes(ATTR_DEST.TRANS_COMMON, awsAttributes)
|
|
138
|
+
|
|
139
|
+
shim.agent.setLambdaArn(context.invokedFunctionArn)
|
|
140
|
+
|
|
141
|
+
shim.agent.setLambdaFunctionVersion(context.functionVersion)
|
|
142
|
+
segment.addSpanAttributes(awsAttributes)
|
|
143
|
+
|
|
144
|
+
segment.start()
|
|
145
|
+
return { segment, txnEnder }
|
|
146
|
+
}
|
|
147
|
+
|
|
108
148
|
patchLambdaHandler(handler) {
|
|
109
149
|
const awsLambda = this
|
|
110
150
|
const shim = this.shim
|
|
@@ -114,6 +154,11 @@ class AwsLambda {
|
|
|
114
154
|
return handler
|
|
115
155
|
}
|
|
116
156
|
|
|
157
|
+
const isStreamHandler = handler[HANDLER_STREAMING] === 'response'
|
|
158
|
+
if (isStreamHandler) {
|
|
159
|
+
this.agent.recordSupportability('Nodejs/Serverless/Lambda/ResponseStreaming')
|
|
160
|
+
}
|
|
161
|
+
|
|
117
162
|
if (!patchCalled) {
|
|
118
163
|
// Only wrap emit on process the first time patch is called.
|
|
119
164
|
patchCalled = true
|
|
@@ -122,52 +167,87 @@ class AwsLambda {
|
|
|
122
167
|
this.wrapFatal()
|
|
123
168
|
}
|
|
124
169
|
|
|
125
|
-
|
|
170
|
+
const wrapper = isStreamHandler ? wrappedStreamHandler : wrappedHandler
|
|
171
|
+
const nrHandler = shim.bindCreateTransaction(wrapper, new specs.TransactionSpec({ type: shim.BG }))
|
|
172
|
+
awsLambda.propagateSymbols(handler, nrHandler)
|
|
173
|
+
|
|
174
|
+
return nrHandler
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Wraps a response streaming lambda handler.
|
|
178
|
+
*
|
|
179
|
+
* Creates and applies segment based on function name, assigns attributes to transaction trace,
|
|
180
|
+
* listen when stream errors(log error), ends(end transaction)
|
|
181
|
+
*
|
|
182
|
+
* **Note**: AWS doesn't support response streaming with API gateway invoked lambdas.
|
|
183
|
+
* This means we do not handle that as it would require intercepting the stream to parse
|
|
184
|
+
* the response code and headers.
|
|
185
|
+
*/
|
|
186
|
+
function wrappedStreamHandler() {
|
|
187
|
+
const transaction = shim.tracer.getTransaction()
|
|
188
|
+
if (!transaction) {
|
|
189
|
+
logger.trace('No active transaction, not wrapping streaming handler')
|
|
190
|
+
return handler.apply(this, arguments)
|
|
191
|
+
}
|
|
126
192
|
|
|
127
|
-
function wrappedHandler() {
|
|
128
193
|
const args = shim.argsToArray.apply(shim, arguments)
|
|
129
|
-
|
|
130
194
|
const event = args[0]
|
|
131
|
-
const context = args[
|
|
195
|
+
const context = args[2]
|
|
196
|
+
logger.trace('In stream handler, lambda function name', context?.functionName)
|
|
197
|
+
const { segment, txnEnder } = awsLambda.createSegment({ context, event, transaction, recorder: recordBackground })
|
|
198
|
+
args[1] = awsLambda.wrapStreamAndCaptureError(
|
|
199
|
+
transaction,
|
|
200
|
+
txnEnder,
|
|
201
|
+
args[1]
|
|
202
|
+
)
|
|
132
203
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
204
|
+
let res
|
|
205
|
+
try {
|
|
206
|
+
res = shim.applySegment(handler, segment, false, this, args)
|
|
207
|
+
} catch (err) {
|
|
208
|
+
uncaughtException = err
|
|
209
|
+
txnEnder()
|
|
210
|
+
throw err
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return res
|
|
214
|
+
}
|
|
136
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Wraps a non response streaming lambda handler.
|
|
218
|
+
*
|
|
219
|
+
* Creates and applies segment based on function name, assigns attributes to transaction trace,
|
|
220
|
+
* adds handlers if api gateway to wrap request/response
|
|
221
|
+
* wraps the callback(if present), wraps the context `done`, `succeed`, `fail methods`, intercepts promise
|
|
222
|
+
* and properly ends transaction
|
|
223
|
+
*/
|
|
224
|
+
function wrappedHandler() {
|
|
137
225
|
const transaction = shim.tracer.getTransaction()
|
|
138
226
|
if (!transaction) {
|
|
227
|
+
logger.trace('No active transaction, not wrapping handler')
|
|
139
228
|
return handler.apply(this, arguments)
|
|
140
229
|
}
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
transaction.setPartialName(transactionName)
|
|
230
|
+
const args = shim.argsToArray.apply(shim, arguments)
|
|
144
231
|
|
|
232
|
+
const event = args[0]
|
|
233
|
+
const context = args[1]
|
|
234
|
+
logger.trace('Lambda function name', context?.functionName)
|
|
145
235
|
const isApiGatewayLambdaProxy = apiGateway.isLambdaProxyEvent(event)
|
|
236
|
+
logger.trace('Is this Lambda event an API Gateway or ALB web proxy?', isApiGatewayLambdaProxy)
|
|
237
|
+
logger.trace('Lambda event keys', Object.keys(event))
|
|
146
238
|
const segmentRecorder = isApiGatewayLambdaProxy ? recordWeb : recordBackground
|
|
147
|
-
const segment =
|
|
148
|
-
|
|
239
|
+
const { segment, txnEnder } = awsLambda.createSegment({ context, event, transaction, recorder: segmentRecorder })
|
|
240
|
+
|
|
149
241
|
// resultProcessor is used to execute additional logic based on the
|
|
150
242
|
// payload supplied to the callback.
|
|
151
243
|
let resultProcessor
|
|
152
244
|
|
|
153
|
-
logger.trace('Is this Lambda event an API Gateway or ALB web proxy?', isApiGatewayLambdaProxy)
|
|
154
|
-
logger.trace('Lambda event keys', Object.keys(event))
|
|
155
|
-
|
|
156
245
|
if (isApiGatewayLambdaProxy) {
|
|
157
246
|
const webRequest = new apiGateway.LambdaProxyWebRequest(event)
|
|
158
247
|
setWebRequest(shim, transaction, webRequest)
|
|
159
248
|
resultProcessor = getApiGatewayLambdaProxyResultProcessor(transaction)
|
|
160
249
|
}
|
|
161
|
-
|
|
162
250
|
const cbIndex = args.length - 1
|
|
163
|
-
|
|
164
|
-
// Add transaction ending closure to the list of functions to be called on
|
|
165
|
-
// beforeExit (i.e. in the case that context.{done,fail,succeed} or callback
|
|
166
|
-
// were not called).
|
|
167
|
-
const txnEnder = endTransaction.bind(null, transaction, transactionEnders.length)
|
|
168
|
-
|
|
169
|
-
transactionEnders.push(txnEnder)
|
|
170
|
-
|
|
171
251
|
args[cbIndex] = awsLambda.wrapCallbackAndCaptureError(
|
|
172
252
|
transaction,
|
|
173
253
|
txnEnder,
|
|
@@ -186,16 +266,6 @@ class AwsLambda {
|
|
|
186
266
|
}
|
|
187
267
|
})
|
|
188
268
|
|
|
189
|
-
const awsAttributes = awsLambda._getAwsAgentAttributes(event, context)
|
|
190
|
-
transaction.trace.attributes.addAttributes(ATTR_DEST.TRANS_COMMON, awsAttributes)
|
|
191
|
-
|
|
192
|
-
shim.agent.setLambdaArn(context.invokedFunctionArn)
|
|
193
|
-
|
|
194
|
-
shim.agent.setLambdaFunctionVersion(context.functionVersion)
|
|
195
|
-
segment.addSpanAttributes(awsAttributes)
|
|
196
|
-
|
|
197
|
-
segment.start()
|
|
198
|
-
|
|
199
269
|
let res
|
|
200
270
|
try {
|
|
201
271
|
res = shim.applySegment(handler, segment, false, this, args)
|
|
@@ -251,6 +321,18 @@ class AwsLambda {
|
|
|
251
321
|
}
|
|
252
322
|
}
|
|
253
323
|
|
|
324
|
+
wrapStreamAndCaptureError(transaction, txnEnder, stream) {
|
|
325
|
+
const shim = this.shim
|
|
326
|
+
stream.on('error', (error) => {
|
|
327
|
+
shim.agent.errors.add(transaction, error)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
stream.on('close', () => {
|
|
331
|
+
txnEnder()
|
|
332
|
+
})
|
|
333
|
+
return stream
|
|
334
|
+
}
|
|
335
|
+
|
|
254
336
|
_getAwsAgentAttributes(event, context) {
|
|
255
337
|
const attributes = {
|
|
256
338
|
'aws.lambda.arn': context.invokedFunctionArn,
|
|
@@ -372,7 +454,7 @@ function lowercaseObjectKeys(original) {
|
|
|
372
454
|
}
|
|
373
455
|
|
|
374
456
|
function endTransaction(transaction, enderIndex) {
|
|
375
|
-
if (transactionEnders[enderIndex] === cleanClosure) {
|
|
457
|
+
if (transactionEnders.length === 0 || transactionEnders[enderIndex] === cleanClosure) {
|
|
376
458
|
// In the case where we have already been called, we return early. There may be a
|
|
377
459
|
// case where this is called more than once, given the lambda is left in a dirty
|
|
378
460
|
// state after thread suspension (e.g. timeouts)
|
|
@@ -411,7 +493,7 @@ function setWebResponse(transaction, response) {
|
|
|
411
493
|
|
|
412
494
|
// We are adding http.statusCode to base segment as
|
|
413
495
|
// we found in testing async invoked lambdas, the
|
|
414
|
-
// active
|
|
496
|
+
// active segment is not available at this point.
|
|
415
497
|
const segment = transaction.baseSegment
|
|
416
498
|
|
|
417
499
|
segment.addSpanAttribute('http.statusCode', responseCode)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const HTTP_LIBRARY = 'http'
|
|
7
|
+
const CATEGORIES = {
|
|
8
|
+
HTTP: 'http',
|
|
9
|
+
DATASTORE: 'datastore',
|
|
10
|
+
GENERIC: 'generic'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const SPAN_KIND = {
|
|
14
|
+
CONSUMER: 'consumer',
|
|
15
|
+
CLIENT: 'client',
|
|
16
|
+
INTERNAL: 'internal',
|
|
17
|
+
PRODUCER: 'producer',
|
|
18
|
+
SERVER: 'server'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const REGEXS = {
|
|
22
|
+
CONSUMER: /^(?:Truncated\/)?OtherTransaction\/Message\//,
|
|
23
|
+
CLIENT: {
|
|
24
|
+
EXTERNAL: /^(?:Truncated\/)?External\//,
|
|
25
|
+
DATASTORE: /^(?:Truncated\/)?Datastore\//,
|
|
26
|
+
},
|
|
27
|
+
PRODUCER: /^(?:Truncated\/)?MessageBroker\//,
|
|
28
|
+
SERVER: /^(?:Truncated\/)?(WebTransaction)\//
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Assigns the appropriate span kind based on the segment name.
|
|
33
|
+
* Does not handle client kind as this is done in the `HttpSpanEvent` and `DatastoreSpanEvent`
|
|
34
|
+
* Our agent has conventions for naming all types of segments.
|
|
35
|
+
* The only place this convention does not exist is within the `api.startWebTransaction`
|
|
36
|
+
* and `api.startBackgroundTransaction`. For those, we have assigned a `spanKind` property
|
|
37
|
+
* on the segment. We default to `internal` if it cannot match a regex.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} params to function
|
|
40
|
+
* @param {TraceSegment} params.segment segment that is creating span
|
|
41
|
+
* @param {object} params.span span to add `intrinsics['span.kind']`
|
|
42
|
+
*/
|
|
43
|
+
function addSpanKind({ segment, span }) {
|
|
44
|
+
const intrinsics = span.getIntrinsicAttributes()
|
|
45
|
+
if (!intrinsics['span.kind']) {
|
|
46
|
+
let spanKind
|
|
47
|
+
if (segment.spanKind) {
|
|
48
|
+
spanKind = segment.spanKind
|
|
49
|
+
} else if (REGEXS.CONSUMER.test(segment.name)) {
|
|
50
|
+
spanKind = SPAN_KIND.CONSUMER
|
|
51
|
+
} else if (REGEXS.PRODUCER.test(segment.name)) {
|
|
52
|
+
spanKind = SPAN_KIND.PRODUCER
|
|
53
|
+
} else if (REGEXS.SERVER.test(segment.name)) {
|
|
54
|
+
spanKind = SPAN_KIND.SERVER
|
|
55
|
+
} else {
|
|
56
|
+
spanKind = SPAN_KIND.INTERNAL
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
span.addIntrinsicAttribute('span.kind', spanKind)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
HTTP_LIBRARY,
|
|
65
|
+
CATEGORIES,
|
|
66
|
+
SPAN_KIND,
|
|
67
|
+
REGEXS,
|
|
68
|
+
addSpanKind
|
|
69
|
+
}
|
package/lib/spans/span-event.js
CHANGED
|
@@ -9,18 +9,7 @@ const Config = require('../config')
|
|
|
9
9
|
const { truncate } = require('../util/byte-limit')
|
|
10
10
|
|
|
11
11
|
const { DESTINATIONS } = require('../config/attribute-filter')
|
|
12
|
-
|
|
13
|
-
const HTTP_LIBRARY = 'http'
|
|
14
|
-
const CLIENT_KIND = 'client'
|
|
15
|
-
const CATEGORIES = {
|
|
16
|
-
HTTP: 'http',
|
|
17
|
-
DATASTORE: 'datastore',
|
|
18
|
-
GENERIC: 'generic'
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const EXTERNAL_REGEX = /^(?:Truncated\/)?External\//
|
|
22
|
-
const DATASTORE_REGEX = /^(?:Truncated\/)?Datastore\//
|
|
23
|
-
|
|
12
|
+
const { addSpanKind, HTTP_LIBRARY, REGEXS, SPAN_KIND, CATEGORIES } = require('./helpers')
|
|
24
13
|
const EMPTY_USER_ATTRS = Object.freeze(Object.create(null))
|
|
25
14
|
const SERVER_ADDRESS = 'server.address'
|
|
26
15
|
|
|
@@ -74,6 +63,14 @@ class SpanEvent {
|
|
|
74
63
|
}
|
|
75
64
|
}
|
|
76
65
|
|
|
66
|
+
getIntrinsicAttributes() {
|
|
67
|
+
return this.intrinsics
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
addIntrinsicAttribute(key, value) {
|
|
71
|
+
this.intrinsics[key] = value
|
|
72
|
+
}
|
|
73
|
+
|
|
77
74
|
static get CATEGORIES() {
|
|
78
75
|
return CATEGORIES
|
|
79
76
|
}
|
|
@@ -153,6 +150,7 @@ class SpanEvent {
|
|
|
153
150
|
span.intrinsics.timestamp = segment.timer.start
|
|
154
151
|
span.intrinsics.duration = segment.timer.getDurationInMillis() / 1000
|
|
155
152
|
|
|
153
|
+
addSpanKind({ segment, span })
|
|
156
154
|
return span
|
|
157
155
|
}
|
|
158
156
|
|
|
@@ -193,7 +191,7 @@ class HttpSpanEvent extends SpanEvent {
|
|
|
193
191
|
|
|
194
192
|
this.intrinsics.category = CATEGORIES.HTTP
|
|
195
193
|
this.intrinsics.component = attributes.library || HTTP_LIBRARY
|
|
196
|
-
this.intrinsics['span.kind'] =
|
|
194
|
+
this.intrinsics['span.kind'] = SPAN_KIND.CLIENT
|
|
197
195
|
|
|
198
196
|
if (attributes.library) {
|
|
199
197
|
attributes.library = null
|
|
@@ -217,7 +215,7 @@ class HttpSpanEvent extends SpanEvent {
|
|
|
217
215
|
}
|
|
218
216
|
|
|
219
217
|
static testSegment(segment) {
|
|
220
|
-
return
|
|
218
|
+
return REGEXS.CLIENT.EXTERNAL.test(segment.name)
|
|
221
219
|
}
|
|
222
220
|
}
|
|
223
221
|
|
|
@@ -232,7 +230,7 @@ class DatastoreSpanEvent extends SpanEvent {
|
|
|
232
230
|
super(attributes, customAttributes)
|
|
233
231
|
|
|
234
232
|
this.intrinsics.category = CATEGORIES.DATASTORE
|
|
235
|
-
this.intrinsics['span.kind'] =
|
|
233
|
+
this.intrinsics['span.kind'] = SPAN_KIND.CLIENT
|
|
236
234
|
|
|
237
235
|
if (attributes.product) {
|
|
238
236
|
this.intrinsics.component = attributes.product
|
|
@@ -279,7 +277,7 @@ class DatastoreSpanEvent extends SpanEvent {
|
|
|
279
277
|
}
|
|
280
278
|
|
|
281
279
|
static testSegment(segment) {
|
|
282
|
-
return
|
|
280
|
+
return REGEXS.CLIENT.DATASTORE.test(segment.name)
|
|
283
281
|
}
|
|
284
282
|
}
|
|
285
283
|
|
|
@@ -10,17 +10,7 @@ const { truncate } = require('../util/byte-limit')
|
|
|
10
10
|
const Config = require('../config')
|
|
11
11
|
|
|
12
12
|
const { DESTINATIONS } = require('../config/attribute-filter')
|
|
13
|
-
|
|
14
|
-
const HTTP_LIBRARY = 'http'
|
|
15
|
-
const CLIENT_KIND = 'client'
|
|
16
|
-
const CATEGORIES = {
|
|
17
|
-
HTTP: 'http',
|
|
18
|
-
DATASTORE: 'datastore',
|
|
19
|
-
GENERIC: 'generic'
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const EXTERNAL_REGEX = /^(?:Truncated\/)?External\//
|
|
23
|
-
const DATASTORE_REGEX = /^(?:Truncated\/)?Datastore\//
|
|
13
|
+
const { addSpanKind, HTTP_LIBRARY, REGEXS, SPAN_KIND, CATEGORIES } = require('./helpers')
|
|
24
14
|
|
|
25
15
|
/**
|
|
26
16
|
* Specialized span event class for use with infinite streaming.
|
|
@@ -59,7 +49,7 @@ class StreamingSpanEvent {
|
|
|
59
49
|
}
|
|
60
50
|
|
|
61
51
|
/**
|
|
62
|
-
* Add a key/value pair to the Span's
|
|
52
|
+
* Add a key/value pair to the Span's intrinsics collection.
|
|
63
53
|
*
|
|
64
54
|
* @param {string} key Name of the attribute to be stored.
|
|
65
55
|
* @param {string|boolean|number} value Value of the attribute to be stored.
|
|
@@ -68,6 +58,10 @@ class StreamingSpanEvent {
|
|
|
68
58
|
this._intrinsicAttributes.addAttribute(key, value)
|
|
69
59
|
}
|
|
70
60
|
|
|
61
|
+
getIntrinsicAttributes() {
|
|
62
|
+
return this._intrinsicAttributes
|
|
63
|
+
}
|
|
64
|
+
|
|
71
65
|
/**
|
|
72
66
|
* Add a key/value pair to the Span's custom/user attributes collection.
|
|
73
67
|
*
|
|
@@ -169,6 +163,7 @@ class StreamingSpanEvent {
|
|
|
169
163
|
// Timestamp in milliseconds, duration in seconds. Yay consistency!
|
|
170
164
|
span.addIntrinsicAttribute('timestamp', segment.timer.start)
|
|
171
165
|
span.addIntrinsicAttribute('duration', segment.timer.getDurationInMillis() / 1000)
|
|
166
|
+
addSpanKind({ segment, span })
|
|
172
167
|
|
|
173
168
|
return span
|
|
174
169
|
}
|
|
@@ -196,7 +191,7 @@ class StreamingHttpSpanEvent extends StreamingSpanEvent {
|
|
|
196
191
|
|
|
197
192
|
this.addIntrinsicAttribute('category', CATEGORIES.HTTP)
|
|
198
193
|
this.addIntrinsicAttribute('component', library || HTTP_LIBRARY)
|
|
199
|
-
this.addIntrinsicAttribute('span.kind',
|
|
194
|
+
this.addIntrinsicAttribute('span.kind', SPAN_KIND.CLIENT)
|
|
200
195
|
|
|
201
196
|
if (url) {
|
|
202
197
|
this.addAgentAttribute('http.url', url)
|
|
@@ -213,7 +208,7 @@ class StreamingHttpSpanEvent extends StreamingSpanEvent {
|
|
|
213
208
|
}
|
|
214
209
|
|
|
215
210
|
static isHttpSegment(segment) {
|
|
216
|
-
return
|
|
211
|
+
return REGEXS.CLIENT.EXTERNAL.test(segment.name)
|
|
217
212
|
}
|
|
218
213
|
}
|
|
219
214
|
|
|
@@ -248,7 +243,7 @@ class StreamingDatastoreSpanEvent extends StreamingSpanEvent {
|
|
|
248
243
|
super(traceId, agentAttrs, customAttributes)
|
|
249
244
|
|
|
250
245
|
this.addIntrinsicAttribute('category', CATEGORIES.DATASTORE)
|
|
251
|
-
this.addIntrinsicAttribute('span.kind',
|
|
246
|
+
this.addIntrinsicAttribute('span.kind', SPAN_KIND.CLIENT)
|
|
252
247
|
|
|
253
248
|
if (product) {
|
|
254
249
|
this.addIntrinsicAttribute('component', product)
|
|
@@ -288,7 +283,7 @@ class StreamingDatastoreSpanEvent extends StreamingSpanEvent {
|
|
|
288
283
|
/* eslint-enable camelcase */
|
|
289
284
|
|
|
290
285
|
static isDatastoreSegment(segment) {
|
|
291
|
-
return
|
|
286
|
+
return REGEXS.CLIENT.DATASTORE.test(segment.name)
|
|
292
287
|
}
|
|
293
288
|
}
|
|
294
289
|
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict'
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
const logger = require('./logger').child({ component: 'sampler' })
|
|
8
|
+
const os = require('node:os')
|
|
9
|
+
const logger = require('./logger').child({ component: 'system-metrics-sampler' })
|
|
10
10
|
const Timer = require('./timer')
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
const NAMES = require('./metrics/names')
|
|
12
13
|
|
|
13
14
|
/*
|
|
14
15
|
*
|
|
@@ -22,12 +23,12 @@ const SAMPLE_INTERVAL = 15 * MILLIS
|
|
|
22
23
|
|
|
23
24
|
let samplers = []
|
|
24
25
|
|
|
25
|
-
function
|
|
26
|
+
function SystemMetricsSampler(sampler, interval) {
|
|
26
27
|
this.id = setInterval(sampler, interval)
|
|
27
28
|
this.id.unref()
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
SystemMetricsSampler.prototype.stop = function stop() {
|
|
31
32
|
clearInterval(this.id)
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -148,8 +149,8 @@ module.exports = {
|
|
|
148
149
|
nativeMetrics: null,
|
|
149
150
|
|
|
150
151
|
start: function start(agent) {
|
|
151
|
-
samplers.push(new
|
|
152
|
-
samplers.push(new
|
|
152
|
+
samplers.push(new SystemMetricsSampler(sampleMemory(agent), 5 * MILLIS))
|
|
153
|
+
samplers.push(new SystemMetricsSampler(checkEvents(agent), SAMPLE_INTERVAL))
|
|
153
154
|
|
|
154
155
|
// This requires a native module which may have failed to build.
|
|
155
156
|
if (agent.config.plugins.native_metrics.enabled && !this.nativeMetrics) {
|
|
@@ -175,17 +176,17 @@ module.exports = {
|
|
|
175
176
|
|
|
176
177
|
// Add GC events if available.
|
|
177
178
|
if (this.nativeMetrics.gcEnabled) {
|
|
178
|
-
samplers.push(new
|
|
179
|
+
samplers.push(new SystemMetricsSampler(sampleGc(agent, this.nativeMetrics), SAMPLE_INTERVAL))
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
// Add loop metrics if available.
|
|
182
183
|
if (this.nativeMetrics.loopEnabled) {
|
|
183
|
-
samplers.push(new
|
|
184
|
+
samplers.push(new SystemMetricsSampler(sampleLoop(agent, this.nativeMetrics), SAMPLE_INTERVAL))
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
// Add CPU sampling using the built-in data (introduced in 6.1.0)
|
|
188
|
-
samplers.push(new
|
|
189
|
+
samplers.push(new SystemMetricsSampler(sampleCpu(agent), SAMPLE_INTERVAL))
|
|
189
190
|
|
|
190
191
|
this.state = 'running'
|
|
191
192
|
},
|
package/lib/transaction/index.js
CHANGED
|
@@ -15,7 +15,6 @@ const props = require('../util/properties')
|
|
|
15
15
|
const Timer = require('../timer')
|
|
16
16
|
const Trace = require('./trace')
|
|
17
17
|
const synthetics = require('../synthetics')
|
|
18
|
-
const url = require('url')
|
|
19
18
|
const urltils = require('../util/urltils')
|
|
20
19
|
const TraceContext = require('./tracecontext').TraceContext
|
|
21
20
|
const Logs = require('./logs')
|
|
@@ -609,49 +608,6 @@ Transaction.prototype.getFullName = function getFullName() {
|
|
|
609
608
|
return fullName
|
|
610
609
|
}
|
|
611
610
|
|
|
612
|
-
/**
|
|
613
|
-
* Returns the full URL of the transaction with query, search, or hash portions
|
|
614
|
-
* removed. This is only applicable for web transactions.
|
|
615
|
-
*
|
|
616
|
-
* Caches to ._scrubbedUrl, pulls in from .parsedUrl if it is available,
|
|
617
|
-
* otherwise it will parse .url, store it on .parsedUrl, then scrub the URL and
|
|
618
|
-
* store it in the cache.
|
|
619
|
-
*
|
|
620
|
-
* @returns {(string|undefined)} Returns a string or undefined.
|
|
621
|
-
*/
|
|
622
|
-
Transaction.prototype.getScrubbedUrl = function getScrubbedUrl() {
|
|
623
|
-
if (!this.isWeb()) {
|
|
624
|
-
return
|
|
625
|
-
}
|
|
626
|
-
if (this._scrubbedUrl) {
|
|
627
|
-
return this._scrubbedUrl
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// If we don't have a parsedUrl, lets populate it from .url
|
|
631
|
-
if (!this.parsedUrl) {
|
|
632
|
-
// At time of writing .url should always be set by the time we get here
|
|
633
|
-
// because that is what .isWeb() checks against. In the future it may be
|
|
634
|
-
// instead checking a enum or other property so guard ourselves just in
|
|
635
|
-
// case.
|
|
636
|
-
if (!this.url) {
|
|
637
|
-
return
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
this.parsedUrl = url.parse(this.url)
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
const scrubbedParsedUrl = Object.assign(Object.create(null), this.parsedUrl)
|
|
644
|
-
scrubbedParsedUrl.search = null
|
|
645
|
-
scrubbedParsedUrl.query = null
|
|
646
|
-
scrubbedParsedUrl.href = null
|
|
647
|
-
scrubbedParsedUrl.path = null
|
|
648
|
-
scrubbedParsedUrl.hash = null
|
|
649
|
-
|
|
650
|
-
this._scrubbedUrl = url.format(scrubbedParsedUrl)
|
|
651
|
-
|
|
652
|
-
return this._scrubbedUrl
|
|
653
|
-
}
|
|
654
|
-
|
|
655
611
|
/**
|
|
656
612
|
* The instrumentation associates metrics with the different kinds of trace
|
|
657
613
|
* segments. The metrics recorders are dependent on the transaction name to
|
|
@@ -779,11 +735,12 @@ Transaction.prototype.alternatePathHashes = function alternatePathHashes() {
|
|
|
779
735
|
* an attribute onto the exception.
|
|
780
736
|
*
|
|
781
737
|
* @param {Exception} exception The exception object to be collected.
|
|
738
|
+
* @param {Segment} segment The segment to which the exception is linked.
|
|
782
739
|
*/
|
|
783
740
|
Transaction.prototype._linkExceptionToSegment = _linkExceptionToSegment
|
|
784
741
|
|
|
785
|
-
function _linkExceptionToSegment(exception) {
|
|
786
|
-
|
|
742
|
+
function _linkExceptionToSegment(exception, segment) {
|
|
743
|
+
segment = segment || this.agent.tracer.getSegment()
|
|
787
744
|
if (!segment) {
|
|
788
745
|
return
|
|
789
746
|
}
|
|
@@ -806,16 +763,17 @@ function _linkExceptionToSegment(exception) {
|
|
|
806
763
|
* the exception will be collected along with the transaction details.
|
|
807
764
|
*
|
|
808
765
|
* @param {Exception} exception The exception object to be collected.
|
|
766
|
+
* @param {Segment} segment The segment to which the exception is linked.
|
|
809
767
|
*/
|
|
810
768
|
Transaction.prototype.addException = _addException
|
|
811
769
|
|
|
812
|
-
function _addException(exception) {
|
|
770
|
+
function _addException(exception, segment) {
|
|
813
771
|
if (!this.isActive()) {
|
|
814
772
|
logger.trace('Transaction is not active. Not capturing error: ', exception)
|
|
815
773
|
return
|
|
816
774
|
}
|
|
817
775
|
|
|
818
|
-
this._linkExceptionToSegment(exception)
|
|
776
|
+
this._linkExceptionToSegment(exception, segment)
|
|
819
777
|
this.exceptions.push(exception)
|
|
820
778
|
}
|
|
821
779
|
|
|
@@ -64,6 +64,9 @@ function TraceSegment({ id, config, name, collect, parentId, root, isRoot = fals
|
|
|
64
64
|
this.state = STATE.EXTERNAL
|
|
65
65
|
this.async = true
|
|
66
66
|
this.ignore = false
|
|
67
|
+
// only use to specify spanKind on segments created with `api.startBackgroundTransaction` and `api.startWebTransaction`
|
|
68
|
+
// we typically determine the span kind based on the segment name
|
|
69
|
+
this.spanKind = null
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
TraceSegment.prototype.getSpanContext = function getSpanContext() {
|
|
@@ -222,10 +222,11 @@ function transactionNestProxy(type, handler) {
|
|
|
222
222
|
|
|
223
223
|
// don't nest transactions, reuse existing ones
|
|
224
224
|
let context = tracer.getContext()
|
|
225
|
+
const transaction = tracer.getTransaction()
|
|
225
226
|
|
|
226
227
|
let createNew = false
|
|
227
228
|
|
|
228
|
-
if (!
|
|
229
|
+
if (!transaction || transaction.type !== type) {
|
|
229
230
|
createNew = true
|
|
230
231
|
}
|
|
231
232
|
|