newrelic 13.6.6 → 13.7.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 +41 -0
- package/THIRD_PARTY_NOTICES.md +10 -127
- package/index.js +1 -1
- package/lib/agent.js +5 -9
- package/lib/attributes.js +13 -6
- package/lib/config/default.js +1 -2
- package/lib/config/index.js +17 -10
- package/lib/config/samplers.js +114 -59
- package/lib/feature_flags.js +1 -1
- package/lib/instrumentation/koa/router-instrumentation.js +2 -1
- package/lib/otel/logs/index.js +1 -1
- package/lib/otel/metrics/index.js +2 -2
- package/lib/otel/{logs/normalize-timestamp.js → normalize-timestamp.js} +32 -6
- package/lib/otel/setup.js +9 -9
- package/lib/otel/traces/segment-synthesis.js +30 -6
- package/lib/otel/traces/segments/index.js +1 -1
- package/lib/otel/traces/span-processor.js +15 -1
- package/lib/otel/traces/utils.js +3 -1
- package/lib/samplers/README.md +79 -0
- package/lib/samplers/adaptive-sampler.js +27 -3
- package/lib/samplers/always-off-sampler.js +16 -0
- package/lib/samplers/always-on-sampler.js +17 -0
- package/lib/samplers/index.js +270 -0
- package/lib/samplers/ratio-based-sampler.js +74 -0
- package/lib/samplers/sampler.js +58 -0
- package/lib/shimmer.js +2 -2
- package/lib/spans/span-event-aggregator.js +6 -2
- package/lib/spans/span-event.js +145 -45
- package/lib/spans/span-link.js +99 -0
- package/lib/system-info.js +1 -1
- package/lib/transaction/index.js +13 -109
- package/lib/transaction/trace/index.js +1 -1
- package/lib/transaction/trace/segment.js +6 -9
- package/lib/transaction/tracer/index.js +11 -8
- package/lib/utilization/index.js +46 -17
- package/package.json +1 -5
package/lib/config/samplers.js
CHANGED
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
'use strict'
|
|
7
|
-
const { allowList, float } = require('./formatters')
|
|
7
|
+
const { allowList, float, int } = require('./formatters')
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* This configuration is currently the same for `distributed_tracing.sampler
|
|
11
|
-
* `distributed_tracing.sampler.full_granularity` and `distributed_tracing.sampler.partial_granularity`
|
|
10
|
+
* This configuration is currently the same for `distributed_tracing.sampler` and `distributed_tracing.sampler.partial_granularity`
|
|
12
11
|
* Note: This also just defines the outer structure. These values are mixed data types. Most of the time
|
|
13
12
|
* it is just a string. But if you want to use the `trace_id_ratio_based` sampler, then you need to
|
|
14
13
|
* provide an object with a `ratio` property. All of that mapping is done in `lib/config/index.js`
|
|
@@ -27,8 +26,8 @@ const config = {
|
|
|
27
26
|
* }
|
|
28
27
|
*/
|
|
29
28
|
root: {
|
|
30
|
-
formatter: allowList.bind(null, ['trace_id_ratio_based', 'adaptive', 'always_on', 'always_off'
|
|
31
|
-
default: '
|
|
29
|
+
formatter: allowList.bind(null, ['trace_id_ratio_based', 'adaptive', 'always_on', 'always_off']),
|
|
30
|
+
default: 'adaptive',
|
|
32
31
|
},
|
|
33
32
|
|
|
34
33
|
/**
|
|
@@ -42,8 +41,8 @@ const config = {
|
|
|
42
41
|
* setting.
|
|
43
42
|
*/
|
|
44
43
|
remote_parent_sampled: {
|
|
45
|
-
formatter: allowList.bind(null, ['trace_id_ratio_based', 'adaptive', 'always_on', 'always_off'
|
|
46
|
-
default: '
|
|
44
|
+
formatter: allowList.bind(null, ['trace_id_ratio_based', 'adaptive', 'always_on', 'always_off']),
|
|
45
|
+
default: 'adaptive',
|
|
47
46
|
},
|
|
48
47
|
|
|
49
48
|
/**
|
|
@@ -57,21 +56,22 @@ const config = {
|
|
|
57
56
|
* is set to 0.
|
|
58
57
|
*/
|
|
59
58
|
remote_parent_not_sampled: {
|
|
60
|
-
formatter: allowList.bind(null, ['trace_id_ratio_based', 'adaptive', 'always_on', 'always_off'
|
|
61
|
-
default: '
|
|
59
|
+
formatter: allowList.bind(null, ['trace_id_ratio_based', 'adaptive', 'always_on', 'always_off']),
|
|
60
|
+
default: 'adaptive',
|
|
62
61
|
}
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
/**
|
|
66
|
-
* Builds the trace_id_ratio_based
|
|
65
|
+
* Builds the `trace_id_ratio_based.ratio` and `adaptive.sampling_target`
|
|
66
|
+
* for distributed tracing sampler configuration structure.
|
|
67
67
|
*
|
|
68
68
|
* @param {object} params to function
|
|
69
69
|
* @param {object} params.config - The full configuration object
|
|
70
70
|
* @param {string} params.key - The configuration key being processed
|
|
71
|
-
* @param {object} params.
|
|
71
|
+
* @param {object} params.configToUpdate - The internal configuration object being modified
|
|
72
72
|
* @param {Logger} params.logger - The logger instance
|
|
73
73
|
*/
|
|
74
|
-
function
|
|
74
|
+
function buildSamplers({ config, key, configToUpdate, logger }) {
|
|
75
75
|
if (!isValidDTSampler(key)) {
|
|
76
76
|
return
|
|
77
77
|
}
|
|
@@ -79,12 +79,29 @@ function buildTraceIdRatioSamplers({ config, key, incomingConfig, logger }) {
|
|
|
79
79
|
const samplerConfig = config[key]
|
|
80
80
|
const samplers = Object.keys(samplerConfig)
|
|
81
81
|
|
|
82
|
-
// user can set
|
|
82
|
+
// user can set the following samplers:
|
|
83
|
+
// 'root', 'remote_parent_sampled', and `remote_parent_not_sampled'
|
|
84
|
+
// under 'distributed_tracing.sampler` and `distributed_tracing.sampler.partial_granularity`
|
|
83
85
|
for (const sampler of samplers) {
|
|
86
|
+
if (isAdaptiveSamplingTargetConfig(sampler, samplerConfig)) {
|
|
87
|
+
const samplingValue = samplerConfig[sampler].adaptive.sampling_target
|
|
88
|
+
// sampling_target is an int [1, 120]
|
|
89
|
+
if (samplingValue && samplingValue >= 1 && samplingValue <= 120) {
|
|
90
|
+
configToUpdate[key][sampler] = {
|
|
91
|
+
adaptive: {
|
|
92
|
+
sampling_target: samplingValue
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
logger.trace('Setting adaptive.sampling_target on %s.%s', key, sampler)
|
|
96
|
+
} else {
|
|
97
|
+
logger.trace('Not setting adaptive.sampling_target on %s.%s as value is not in range [1,120].', key, sampler)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
84
101
|
if (isTraceIdRatioBasedConfig(sampler, samplerConfig)) {
|
|
85
102
|
const ratioValue = samplerConfig[sampler].trace_id_ratio_based.ratio
|
|
86
|
-
if (ratioValue) {
|
|
87
|
-
|
|
103
|
+
if (typeof ratioValue === 'number') {
|
|
104
|
+
configToUpdate[key][sampler] = {
|
|
88
105
|
trace_id_ratio_based: {
|
|
89
106
|
ratio: ratioValue
|
|
90
107
|
}
|
|
@@ -97,6 +114,71 @@ function buildTraceIdRatioSamplers({ config, key, incomingConfig, logger }) {
|
|
|
97
114
|
}
|
|
98
115
|
}
|
|
99
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Assigns the value of the distributed tracing samplers env var as an
|
|
119
|
+
* trace_id_ratio_based or adaptive object to the sampler in the config.
|
|
120
|
+
*
|
|
121
|
+
* @param {object} params object passed to fn
|
|
122
|
+
* @param {string} params.key key of the sampler
|
|
123
|
+
* Example: 'root' or 'remote_parent_sampled'
|
|
124
|
+
* @param {object} params.config agent config
|
|
125
|
+
* @param {Array} params.paths list of leaf nodes leading to the sampling configuration value
|
|
126
|
+
* Example: ['distributed_tracing', 'sampler']
|
|
127
|
+
* @param {Function} params.setNestedKey function to set nested key in config
|
|
128
|
+
* @param {Logger} params.logger logger instance
|
|
129
|
+
*/
|
|
130
|
+
function setSamplersFromEnv({ key, config, paths, setNestedKey, logger }) {
|
|
131
|
+
if (paths.length === 0) {
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const lastPath = paths[paths.length - 1]
|
|
136
|
+
if (!isValidDTSampler(lastPath) || !isValidDTSamplerType(key)) {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const nestedValue = getNestedValue(config, [...paths, key])
|
|
141
|
+
|
|
142
|
+
if (nestedValue === 'trace_id_ratio_based') {
|
|
143
|
+
handleTraceIdRatioBased({ paths, key, config, setNestedKey, logger })
|
|
144
|
+
} else if (nestedValue === 'adaptive') {
|
|
145
|
+
handleAdaptiveSampling({ paths, key, config, setNestedKey, logger })
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function handleTraceIdRatioBased({ paths, key, config, setNestedKey, logger }) {
|
|
150
|
+
const envVar = `NEW_RELIC_${[...paths, key, 'trace_id_ratio_based', 'ratio'].join('_').toUpperCase()}`
|
|
151
|
+
const setting = process.env[envVar]
|
|
152
|
+
|
|
153
|
+
if (setting) {
|
|
154
|
+
const formattedSetting = float(setting)
|
|
155
|
+
setNestedKey(config, [...paths, key], { trace_id_ratio_based: { ratio: formattedSetting } })
|
|
156
|
+
logger.trace('Setting %s environment variable to %s', envVar, formattedSetting)
|
|
157
|
+
} else {
|
|
158
|
+
logger.trace('Not setting %s environment variable. Setting %s.%s to `adaptive`', envVar, paths.join('.'), key)
|
|
159
|
+
setNestedKey(config, [...paths, key], 'adaptive')
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function handleAdaptiveSampling({ paths, key, config, setNestedKey, logger }) {
|
|
164
|
+
const envVar = `NEW_RELIC_${[...paths, key, 'adaptive', 'sampling_target'].join('_').toUpperCase()}`
|
|
165
|
+
const setting = process.env[envVar]
|
|
166
|
+
|
|
167
|
+
if (setting) {
|
|
168
|
+
const formattedSetting = int(setting)
|
|
169
|
+
if (formattedSetting >= 1 && formattedSetting <= 120) {
|
|
170
|
+
setNestedKey(config, [...paths, key], { adaptive: { sampling_target: formattedSetting } })
|
|
171
|
+
logger.trace('Setting %s environment variable to %s', envVar, formattedSetting)
|
|
172
|
+
} else {
|
|
173
|
+
logger.trace('Not setting %s environment variable; value not in range [1,120]. Setting %s.%s as `adaptive` with no `sampling_target`',
|
|
174
|
+
envVar,
|
|
175
|
+
paths.join('.'),
|
|
176
|
+
key)
|
|
177
|
+
setNestedKey(config, [...paths, key], 'adaptive')
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
100
182
|
/**
|
|
101
183
|
* Checks if the external config contains a trace_id_ratio_based for distributed tracing
|
|
102
184
|
* sampler configuration
|
|
@@ -111,13 +193,27 @@ function isTraceIdRatioBasedConfig(sampler, samplerConfig) {
|
|
|
111
193
|
'trace_id_ratio_based' in samplerConfig[sampler]
|
|
112
194
|
}
|
|
113
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Checks if the external config contains a adaptive object
|
|
198
|
+
* for distributed tracing sampler configuration
|
|
199
|
+
*
|
|
200
|
+
* @param {string} sampler - The selected sampler key
|
|
201
|
+
* @param {object} samplerConfig - The sampler configuration object
|
|
202
|
+
* @returns {boolean} true if it's a adaptive config
|
|
203
|
+
*/
|
|
204
|
+
function isAdaptiveSamplingTargetConfig(sampler, samplerConfig) {
|
|
205
|
+
return isValidDTSamplerType(sampler) &&
|
|
206
|
+
typeof samplerConfig[sampler] === 'object' &&
|
|
207
|
+
'adaptive' in samplerConfig[sampler]
|
|
208
|
+
}
|
|
209
|
+
|
|
114
210
|
/**
|
|
115
211
|
* Check if the sampler is a valid value of either 'sampler', 'full_granularity', or 'partial_granularity'
|
|
116
212
|
* @param {string} sampler - The sampler key to validate
|
|
117
213
|
* @returns {boolean} true if valid, false otherwise
|
|
118
214
|
*/
|
|
119
215
|
function isValidDTSampler(sampler) {
|
|
120
|
-
return ['sampler', '
|
|
216
|
+
return ['sampler', 'partial_granularity'].includes(sampler)
|
|
121
217
|
}
|
|
122
218
|
|
|
123
219
|
/**
|
|
@@ -129,47 +225,6 @@ function isValidDTSamplerType(samplerType) {
|
|
|
129
225
|
return ['root', 'remote_parent_sampled', 'remote_parent_not_sampled'].includes(samplerType)
|
|
130
226
|
}
|
|
131
227
|
|
|
132
|
-
/**
|
|
133
|
-
* Assigns the value of the distributed tracing samplers env var as an trace_id_ratio_based
|
|
134
|
-
* object to the sampler in the config.
|
|
135
|
-
*
|
|
136
|
-
* @param {object} params object passed to fn
|
|
137
|
-
* @param {string} params.key key of the sampler
|
|
138
|
-
* Example: 'root' or 'remote_parent_sampled'
|
|
139
|
-
* @param {object} params.config agent config
|
|
140
|
-
* @param {Array} params.paths list of leaf nodes leading to the sampling configuration value
|
|
141
|
-
* Example: ['distributed_tracing', 'sampler']
|
|
142
|
-
* @param {Function} params.setNestedKey function to set nested key in config
|
|
143
|
-
* @param {Logger} params.logger logger instance
|
|
144
|
-
*/
|
|
145
|
-
function setTraceIdRatioSamplerFromEnv({ key, config, paths, setNestedKey, logger }) {
|
|
146
|
-
// trace id ratio based sampler is nested in leaf nodes under distributed_tracing > samplers > key
|
|
147
|
-
// so don't continue if path is empty
|
|
148
|
-
if (paths.length === 0) {
|
|
149
|
-
return
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const lastPath = paths[paths.length - 1]
|
|
153
|
-
if (isValidDTSampler(lastPath) && isValidDTSamplerType(key)) {
|
|
154
|
-
// Get the value of `config.distributed_tracing.sampler[key]` or `config.distributed_tracing.sampler.full_granularity[key]` or `config.distributed_tracing.sampler.partial_granularity[key]`
|
|
155
|
-
const nestedValue = getNestedValue(config, [...paths, key])
|
|
156
|
-
if (nestedValue !== 'trace_id_ratio_based') {
|
|
157
|
-
return
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// set the trace id based ratio
|
|
161
|
-
const envVar = `NEW_RELIC_${[...paths, key, 'trace_id_ratio_based', 'ratio'].join('_').toUpperCase()}`
|
|
162
|
-
const setting = process.env[envVar]
|
|
163
|
-
if (setting) {
|
|
164
|
-
const formattedSetting = float(setting)
|
|
165
|
-
setNestedKey(config, [...paths, key], { trace_id_ratio_based: { ratio: formattedSetting } })
|
|
166
|
-
logger.trace('Setting %s environment variable to %s', envVar, formattedSetting)
|
|
167
|
-
} else {
|
|
168
|
-
logger.trace('Not setting %s environment variable. Setting %s.%s to `default`', envVar, paths.join('.'), key)
|
|
169
|
-
setNestedKey(config, [...paths, key], 'default')
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
228
|
/**
|
|
174
229
|
* Retrieves a value from a nested object by providing the list of parent keys.
|
|
175
230
|
* @param {object} obj object to assign value to
|
|
@@ -191,6 +246,6 @@ function getNestedValue(obj, keys) {
|
|
|
191
246
|
|
|
192
247
|
module.exports = {
|
|
193
248
|
config,
|
|
194
|
-
|
|
195
|
-
|
|
249
|
+
buildSamplers,
|
|
250
|
+
setSamplersFromEnv
|
|
196
251
|
}
|
package/lib/feature_flags.js
CHANGED
|
@@ -10,7 +10,8 @@ const { MiddlewareSpec, MiddlewareMounterSpec } = require('../../shim/specs')
|
|
|
10
10
|
module.exports = function instrumentRouter(shim, Router) {
|
|
11
11
|
shim.setFramework(shim.KOA)
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// @koa/router 15.x exports the Router class as the default export
|
|
14
|
+
const proto = Router.prototype || Router.default?.prototype
|
|
14
15
|
|
|
15
16
|
shim.wrapReturn(proto, 'register', wrapMiddleware)
|
|
16
17
|
shim.wrapReturn(proto, 'allowedMethods', wrapAllowedMethods)
|
package/lib/otel/logs/index.js
CHANGED
|
@@ -18,7 +18,7 @@ const defaultLogger = require('../../logger').child({ component: 'opentelemetry-
|
|
|
18
18
|
const SetupSignal = require('../setup-signal.js')
|
|
19
19
|
const NewRelicLoggerProvider = require('./proxying-provider.js')
|
|
20
20
|
const NoOpExporter = require('./no-op-exporter.js')
|
|
21
|
-
const normalizeTimestamp = require('
|
|
21
|
+
const normalizeTimestamp = require('../normalize-timestamp.js')
|
|
22
22
|
const severityToString = require('./severity-to-string.js')
|
|
23
23
|
|
|
24
24
|
class SetupLogs extends SetupSignal {
|
|
@@ -23,8 +23,8 @@ class SetupMetrics extends SetupSignal {
|
|
|
23
23
|
super({ agent, logger })
|
|
24
24
|
|
|
25
25
|
const { config } = agent
|
|
26
|
-
const exportInterval = config.
|
|
27
|
-
const exportTimeout = config.
|
|
26
|
+
const exportInterval = config.opentelemetry.metrics.exportInterval
|
|
27
|
+
const exportTimeout = config.opentelemetry.metrics.exportTimeout
|
|
28
28
|
|
|
29
29
|
const resource = resourceFromAttributes({ })
|
|
30
30
|
const memExporter = new InMemoryMetricExporter(AggregationTemporality.DELTA)
|
|
@@ -42,12 +42,7 @@ function normalizeTimestamp(input) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
if (isTimeInputHrTime(input) === true) {
|
|
45
|
-
|
|
46
|
-
// Such tuples are relative to an arbitrary point in time, not the Unix
|
|
47
|
-
// epoch. So there's no way to determine an actual wall clock time and date
|
|
48
|
-
// from such input. For lack of a better solution, we'll return the
|
|
49
|
-
// current epoch in this case.
|
|
50
|
-
return Date.now()
|
|
45
|
+
return hrtimeToMilliseconds(input)
|
|
51
46
|
}
|
|
52
47
|
|
|
53
48
|
if (Object.getPrototypeOf(input) === Date.prototype) {
|
|
@@ -57,3 +52,34 @@ function normalizeTimestamp(input) {
|
|
|
57
52
|
// We don't know what they've given us, so just return the current time.
|
|
58
53
|
return Date.now()
|
|
59
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Converts a JS Open Telemetry hrtime tuple to milliseconds since the
|
|
58
|
+
* standard epoch. The OTEL hrtime is created as follows:
|
|
59
|
+
*
|
|
60
|
+
* 1. Convert `performance.timeOrigin` to a `[seconds, nanoseconds]` tuple.
|
|
61
|
+
* 2. Add the result of `performance.now()` to the tuple to adjust to the
|
|
62
|
+
* current time.
|
|
63
|
+
*
|
|
64
|
+
* Thus, the tuple represents a literal point in time that, when converted
|
|
65
|
+
* back to a single integer, can be passed to `new Date()` to get an accurate
|
|
66
|
+
* representation.
|
|
67
|
+
*
|
|
68
|
+
* @param {number[]} hrtime Upstream represents hrtime as `[seconds, nanoseconds]`.
|
|
69
|
+
*
|
|
70
|
+
* @returns {number} Milliseconds since standard epoch time.
|
|
71
|
+
*/
|
|
72
|
+
function hrtimeToMilliseconds(hrtime) {
|
|
73
|
+
const seconds = hrtime[0]
|
|
74
|
+
const nanoseconds = hrtime[1]
|
|
75
|
+
|
|
76
|
+
// Convert the seconds portion to milliseconds.
|
|
77
|
+
const msSeconds = seconds * 1_000
|
|
78
|
+
// Convert the nanoseconds portion to milliseconds.
|
|
79
|
+
const msNanoseconds = nanoseconds / 1_000_000
|
|
80
|
+
// At this point, we have the number of milliseconds since the standard
|
|
81
|
+
// epoch.
|
|
82
|
+
const msSinceEpoch = msSeconds + msNanoseconds
|
|
83
|
+
|
|
84
|
+
return Math.trunc(msSinceEpoch)
|
|
85
|
+
}
|
package/lib/otel/setup.js
CHANGED
|
@@ -19,9 +19,9 @@ const interceptSpanKey = require('./span-key-interceptor')
|
|
|
19
19
|
const signals = []
|
|
20
20
|
|
|
21
21
|
function setupOtel(agent, logger = defaultLogger) {
|
|
22
|
-
if (agent.config.
|
|
22
|
+
if (agent.config.opentelemetry.enabled !== true) {
|
|
23
23
|
logger.warn(
|
|
24
|
-
'`
|
|
24
|
+
'`opentelemetry` is not enabled, skipping setup of opentelemetry'
|
|
25
25
|
)
|
|
26
26
|
return
|
|
27
27
|
}
|
|
@@ -36,31 +36,31 @@ function setupOtel(agent, logger = defaultLogger) {
|
|
|
36
36
|
opentelemetry.context.setGlobalContextManager(new ContextManager(agent))
|
|
37
37
|
opentelemetry.propagation.setGlobalPropagator(new TracePropagator(agent))
|
|
38
38
|
|
|
39
|
-
if (agent.config.
|
|
39
|
+
if (agent.config.opentelemetry.traces.enabled === true) {
|
|
40
40
|
const signal = new SetupTraces({ agent })
|
|
41
41
|
signals.push(signal)
|
|
42
42
|
} else {
|
|
43
|
-
logger.debug('`
|
|
43
|
+
logger.debug('`opentelemetry.traces` is not enabled, skipping')
|
|
44
44
|
agent.metrics
|
|
45
45
|
.getOrCreateMetric('Supportability/Tracing/Nodejs/OpenTelemetryBridge/disabled')
|
|
46
46
|
.incrementCallCount()
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
if (agent.config.
|
|
49
|
+
if (agent.config.opentelemetry.metrics.enabled === true) {
|
|
50
50
|
const signal = new SetupMetrics({ agent })
|
|
51
51
|
signals.push(signal)
|
|
52
52
|
} else {
|
|
53
|
-
logger.debug('`
|
|
53
|
+
logger.debug('`opentelemetry.metrics` is not enabled, skipping')
|
|
54
54
|
agent.metrics
|
|
55
55
|
.getOrCreateMetric('Supportability/Metrics/Nodejs/OpenTelemetryBridge/disabled')
|
|
56
56
|
.incrementCallCount()
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
if (agent.config.
|
|
59
|
+
if (agent.config.opentelemetry.logs.enabled === true) {
|
|
60
60
|
const signal = new SetupLogs({ agent })
|
|
61
61
|
signals.push(signal)
|
|
62
62
|
} else {
|
|
63
|
-
logger.debug('`
|
|
63
|
+
logger.debug('`opentelemetry.logs` is not enabled, skipping')
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
agent.metrics
|
|
@@ -69,7 +69,7 @@ function setupOtel(agent, logger = defaultLogger) {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
function teardownOtel(agent) {
|
|
72
|
-
if (agent?.config?.
|
|
72
|
+
if (agent?.config?.opentelemetry?.enabled !== true) {
|
|
73
73
|
return
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
'use strict'
|
|
7
7
|
|
|
8
8
|
const defaultLogger = require('#agentlib/logger.js').child({ component: 'segment-synthesizer' })
|
|
9
|
+
const SpanLink = require('#agentlib/spans/span-link.js')
|
|
10
|
+
const normalizeTimestamp = require('../normalize-timestamp.js')
|
|
9
11
|
const { RulesEngine } = require('./rules.js')
|
|
10
12
|
const {
|
|
11
13
|
createConsumerSegment,
|
|
@@ -34,35 +36,57 @@ class SegmentSynthesizer {
|
|
|
34
36
|
return
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
let mapResult
|
|
37
40
|
switch (rule.type) {
|
|
38
41
|
case 'consumer': {
|
|
39
|
-
|
|
42
|
+
mapResult = createConsumerSegment(this.agent, otelSpan, rule)
|
|
43
|
+
break
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
case 'db': {
|
|
43
|
-
|
|
47
|
+
mapResult = createDbSegment(this.agent, otelSpan, rule)
|
|
48
|
+
break
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
case 'external': {
|
|
47
|
-
|
|
52
|
+
mapResult = createHttpExternalSegment(this.agent, otelSpan, rule, this.logger)
|
|
53
|
+
break
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
case 'internal': {
|
|
51
|
-
|
|
57
|
+
mapResult = createInternalSegment(this.agent, otelSpan, rule)
|
|
58
|
+
break
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
case 'producer': {
|
|
55
|
-
|
|
62
|
+
mapResult = createProducerSegment(this.agent, otelSpan, rule)
|
|
63
|
+
break
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
case 'server': {
|
|
59
|
-
|
|
67
|
+
mapResult = createServerSegment(this.agent, otelSpan, rule)
|
|
68
|
+
break
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
default: {
|
|
63
72
|
this.logger.debug('Found type: %s, no synthesis rule currently built', rule.type)
|
|
73
|
+
return
|
|
64
74
|
}
|
|
65
75
|
}
|
|
76
|
+
|
|
77
|
+
if (otelSpan.links?.length > 0) {
|
|
78
|
+
// We need to map span link data over to the New Relic segment.
|
|
79
|
+
for (let i = 0; i < otelSpan.links.length; i += 1) {
|
|
80
|
+
const link = new SpanLink({
|
|
81
|
+
link: otelSpan.links.at(i),
|
|
82
|
+
spanContext: otelSpan.spanContext(),
|
|
83
|
+
timestamp: normalizeTimestamp(otelSpan.startTime)
|
|
84
|
+
})
|
|
85
|
+
mapResult.segment.addSpanLink(link)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return mapResult
|
|
66
90
|
}
|
|
67
91
|
}
|
|
68
92
|
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
const createConsumerSegment = require('./consumer.js')
|
|
9
9
|
const createDbSegment = require('./database.js')
|
|
10
10
|
const createHttpExternalSegment = require('./http-external.js')
|
|
11
|
+
const createInternalSegment = require('./internal.js')
|
|
11
12
|
const createProducerSegment = require('./producer.js')
|
|
12
13
|
const createServerSegment = require('./server.js')
|
|
13
|
-
const createInternalSegment = require('./internal.js')
|
|
14
14
|
|
|
15
15
|
module.exports = {
|
|
16
16
|
createConsumerSegment,
|
|
@@ -45,12 +45,26 @@ module.exports = class NrSpanProcessor {
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Update the segment duration from span, handle errors and
|
|
48
|
-
* map and reconcile any attributes that were to span.
|
|
48
|
+
* map and reconcile any attributes that were added to the span.
|
|
49
49
|
* @param {object} span otel span getting updated
|
|
50
50
|
*/
|
|
51
51
|
onEnd(span) {
|
|
52
52
|
if (span[otelSynthesis] && span[otelSynthesis].segment) {
|
|
53
53
|
const { segment, transaction, rule } = span[otelSynthesis]
|
|
54
|
+
const { instrumentationScope } = span
|
|
55
|
+
|
|
56
|
+
// We always attach the instrumentation scope data as agent attributes
|
|
57
|
+
// if the OTEL span has them set.
|
|
58
|
+
// See https://opentelemetry.io/docs/specs/otel/common/mapping-to-non-otlp/#instrumentationscope
|
|
59
|
+
if (typeof instrumentationScope.name === 'string') {
|
|
60
|
+
segment.addAttribute('otel.scope.name', instrumentationScope.name)
|
|
61
|
+
segment.addAttribute('otel.library.name', instrumentationScope.name)
|
|
62
|
+
}
|
|
63
|
+
if (typeof instrumentationScope.version === 'string') {
|
|
64
|
+
segment.addAttribute('otel.scope.version', instrumentationScope.version)
|
|
65
|
+
segment.addAttribute('otel.library.version', instrumentationScope.version)
|
|
66
|
+
}
|
|
67
|
+
|
|
54
68
|
this.updateDuration(segment, span)
|
|
55
69
|
this.handleError({ segment, transaction, span })
|
|
56
70
|
this.reconcileAttributes({ segment, span, transaction, rule })
|
package/lib/otel/traces/utils.js
CHANGED
|
@@ -22,7 +22,9 @@ function transformTemplate(template, data, rules = {}) {
|
|
|
22
22
|
if (key in rules) {
|
|
23
23
|
return rules[key](data[key])
|
|
24
24
|
} else {
|
|
25
|
-
|
|
25
|
+
// We use || here instead of ?? because we could be comparing against
|
|
26
|
+
// an empty string, not `null` or `undefined`.
|
|
27
|
+
return data[key] || 'unknown'
|
|
26
28
|
}
|
|
27
29
|
} else {
|
|
28
30
|
return 'unknown'
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Sampling at New Relic
|
|
2
|
+
|
|
3
|
+
The New Relic agent supports a robust sampling decision making interface. This is a work-in-progress feature.
|
|
4
|
+
|
|
5
|
+
## Config
|
|
6
|
+
|
|
7
|
+
Customers configure how they would like their transactions to be sampled under our `distributed_tracing` section in our config. Remember, sampling will only apply if a customer has `distributed_tracing.enabled` set to `true`, `distributed_tracing.sampler.full_granularity.enabled = true`, and if they want partial granularity traces, `distributed_tracing.sampler.partial_granularity.enabled = true`.
|
|
8
|
+
|
|
9
|
+
### Types
|
|
10
|
+
|
|
11
|
+
- A "sampler mode" refers to the following config sections: `distributed_tracing.sampler` and `distributed_tracing.sampler.partial_granularity`. They are defined by the three sections: `root`, `remote_parent_sampled`, and `remote_parent_not_sampled`.
|
|
12
|
+
- A "sampler section" refers to `root`, `remote_parent_sampled`, or `remote_parent_not_sampled` within a particular sampler mode. The config defined at this section, i.e. `SAMPLER_TYPE: SAMPLER_SUBOPTION?`, describes the sampling decision for that particular scenario within that mode.
|
|
13
|
+
- `root`: This is the main sampler for traces originating from the current service.
|
|
14
|
+
- `remote_parent_sampled`: The sampler for when the upstream service has sampled the trace.
|
|
15
|
+
- `remote_parent_not_sampled`: The sampler for when the upstream service has not sampled the trace.
|
|
16
|
+
- `SAMPLER_TYPE` can be `adaptive`, `always_on`, `always_off`, and `trace_id_ratio_based`.
|
|
17
|
+
- `SAMPLER_SUBOPTION` is only valid for `adaptive` and `trace_id_ratio_based` and only required for `trace_id_ratio_based`. `adaptive` will fall back to a global `AdaptiveSampler` with a sampling target defined by `distributed_tracing.sampler.adaptive_sampling_target` if `adaptive.sampling_target` is not given.
|
|
18
|
+
|
|
19
|
+
NOTE: `distributed_tracing.sampler` will be used as the setting for the full granularity samplers.
|
|
20
|
+
|
|
21
|
+
### Full Config in Accordance to Spec
|
|
22
|
+
Full Config in Accordance to Pending Spec Changes (as of agent team discussions, 11/21/2025):
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
...
|
|
26
|
+
(Serverless DT attributes; they may be defined under distributed_tracing instead)
|
|
27
|
+
account_id: string (unset by default, only set when using Serverless Mode)
|
|
28
|
+
trusted_account_key: string (unset by default, only set when using Serverless Mode)
|
|
29
|
+
primary_application_id: string (unset by default, only set when using Serverless Mode)
|
|
30
|
+
...
|
|
31
|
+
distributed_tracing:
|
|
32
|
+
enabled: boolean (default true)
|
|
33
|
+
exclude_newrelic_header: boolean (default false)
|
|
34
|
+
enable_success_metrics (OPTIONAL): boolean (default true, set to false to disable supportability metrics)
|
|
35
|
+
sampler: (section for sampling config options for different scenarios)
|
|
36
|
+
adaptive_sampling_target (see note on Sampling Target above)
|
|
37
|
+
root: (when the trace originates from the current service)
|
|
38
|
+
${SAMPLER_TYPE} (See `Sampler Options` above)
|
|
39
|
+
${SAMPLER_SUBOPTION}
|
|
40
|
+
remote_parent_sampled: (when the upstream service has sampled the trace)
|
|
41
|
+
${SAMPLER_TYPE})
|
|
42
|
+
${SAMPLER_SUBOPTION}
|
|
43
|
+
remote_parent_not_sampled: (when the upstream service has not sampled the trace)
|
|
44
|
+
${SAMPLER_TYPE}
|
|
45
|
+
${SAMPLER_SUBOPTION}
|
|
46
|
+
full_granularity:
|
|
47
|
+
enabled
|
|
48
|
+
partial_granularity:
|
|
49
|
+
enabled
|
|
50
|
+
type ("reduced", "essential", "compact")
|
|
51
|
+
root: (when the trace originates from the current service)
|
|
52
|
+
${SAMPLER_TYPE}
|
|
53
|
+
${SAMPLER_SUBOPTION}
|
|
54
|
+
remote_parent_sampled: (when the upstream service has sampled the trace)
|
|
55
|
+
${SAMPLER_TYPE}
|
|
56
|
+
${SAMPLER_SUBOPTION}
|
|
57
|
+
remote_parent_not_sampled: (when the upstream service has not sampled the trace)
|
|
58
|
+
${SAMPLER_TYPE}
|
|
59
|
+
${SAMPLER_SUBOPTION}
|
|
60
|
+
...
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Solution
|
|
64
|
+
|
|
65
|
+
There are two sampler modes, each with three sampler sections, resulting in potentially six different sampling decisions that the agent would have to support. We create a new `Sampler` instance (`AdaptiveSampler`, `AlwaysOnSampler`, `AlwaysOffSampler`, or `TraceIdRatioBasedSampler`, defined in this folder) for each of these sampler modes' sections.
|
|
66
|
+
|
|
67
|
+
`agent.samplers` is defined as:
|
|
68
|
+
|
|
69
|
+
* `agent.samplers.root`
|
|
70
|
+
* `agent.samplers.remoteParentSampled`
|
|
71
|
+
* `agent.samplers.remoteParentNotSampled`
|
|
72
|
+
* `agent.samplers.partialRoot`
|
|
73
|
+
* `agent.samplers.partialRemoteParentSampled`
|
|
74
|
+
* `agent.samplers.partialRemoteParentNotSampled`
|
|
75
|
+
* `agent.samplers.adaptiveSampler` (if needed, see below)
|
|
76
|
+
|
|
77
|
+
These samplers have a `applySamplingDecision({transaction})` function, which `Transaction` calls (in `lib/transaction/index.js`) to update its `sampled` field and therefore its `priority`.
|
|
78
|
+
|
|
79
|
+
Unlike the other samplers, the `AdaptiveSampler` must share state with other `AdaptiveSampler`s if they do not have their suboption, `sampling_target,` defined. Thus, this introduces our seventh field on `agent.sampler.*`: `agent.sampler._globalAdaptiveSampler `. The intent of this field is to have one instance of an `AdaptiveSampler` for `adaptive` sampler sections that do not specify an `adaptive.sampling_target` to share.
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict'
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
const Sampler = require('./sampler')
|
|
9
|
+
|
|
10
|
+
class AdaptiveSampler extends Sampler {
|
|
9
11
|
/**
|
|
10
12
|
*
|
|
11
13
|
* @param {object} opts Sampler options.
|
|
@@ -15,12 +17,16 @@ class AdaptiveSampler {
|
|
|
15
17
|
* @param {boolean} opts.serverless - Indicates if the environment is serverless.
|
|
16
18
|
*/
|
|
17
19
|
constructor(opts) {
|
|
20
|
+
super() // no-op
|
|
18
21
|
this._serverless = opts.serverless
|
|
19
22
|
this._seen = 0
|
|
20
23
|
this._sampled = 0
|
|
21
24
|
this._samplingPeriod = 0
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
// sampling target cannot be a float
|
|
26
|
+
// config should enforce that values outside
|
|
27
|
+
// of [1,120] are not allowed
|
|
28
|
+
this._samplingTarget = parseInt(opts.target, 10)
|
|
29
|
+
this._maxSamples = 2 * this._samplingTarget
|
|
24
30
|
this._samplingThreshold = 0
|
|
25
31
|
this._resetCount = 0
|
|
26
32
|
this._resetInterval = null
|
|
@@ -99,6 +105,24 @@ class AdaptiveSampler {
|
|
|
99
105
|
return false
|
|
100
106
|
}
|
|
101
107
|
|
|
108
|
+
applySamplingDecision({ transaction, tracestate, partialType }) {
|
|
109
|
+
if (!transaction) return
|
|
110
|
+
transaction.partialType = partialType
|
|
111
|
+
if (tracestate) {
|
|
112
|
+
// Explicitly set sampled and priority from tracestate intrinsics if available
|
|
113
|
+
transaction.sampled = tracestate?.intrinsics ? tracestate.isSampled : null
|
|
114
|
+
transaction.priority = tracestate?.intrinsics ? tracestate.priority : null
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// If a tracestate is not defined, then do our normal priority calculation.
|
|
119
|
+
|
|
120
|
+
const initPriority = Sampler.generatePriority()
|
|
121
|
+
transaction.sampled = this.shouldSample(initPriority)
|
|
122
|
+
// only add 1 to priority when doing a full trace
|
|
123
|
+
transaction.priority = transaction.sampled && !partialType ? Sampler.incrementPriority(initPriority, 1) : initPriority
|
|
124
|
+
}
|
|
125
|
+
|
|
102
126
|
/**
|
|
103
127
|
* Starts a new sample period after adjusting the sampling statistics.
|
|
104
128
|
*/
|