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.
Files changed (36) hide show
  1. package/NEWS.md +41 -0
  2. package/THIRD_PARTY_NOTICES.md +10 -127
  3. package/index.js +1 -1
  4. package/lib/agent.js +5 -9
  5. package/lib/attributes.js +13 -6
  6. package/lib/config/default.js +1 -2
  7. package/lib/config/index.js +17 -10
  8. package/lib/config/samplers.js +114 -59
  9. package/lib/feature_flags.js +1 -1
  10. package/lib/instrumentation/koa/router-instrumentation.js +2 -1
  11. package/lib/otel/logs/index.js +1 -1
  12. package/lib/otel/metrics/index.js +2 -2
  13. package/lib/otel/{logs/normalize-timestamp.js → normalize-timestamp.js} +32 -6
  14. package/lib/otel/setup.js +9 -9
  15. package/lib/otel/traces/segment-synthesis.js +30 -6
  16. package/lib/otel/traces/segments/index.js +1 -1
  17. package/lib/otel/traces/span-processor.js +15 -1
  18. package/lib/otel/traces/utils.js +3 -1
  19. package/lib/samplers/README.md +79 -0
  20. package/lib/samplers/adaptive-sampler.js +27 -3
  21. package/lib/samplers/always-off-sampler.js +16 -0
  22. package/lib/samplers/always-on-sampler.js +17 -0
  23. package/lib/samplers/index.js +270 -0
  24. package/lib/samplers/ratio-based-sampler.js +74 -0
  25. package/lib/samplers/sampler.js +58 -0
  26. package/lib/shimmer.js +2 -2
  27. package/lib/spans/span-event-aggregator.js +6 -2
  28. package/lib/spans/span-event.js +145 -45
  29. package/lib/spans/span-link.js +99 -0
  30. package/lib/system-info.js +1 -1
  31. package/lib/transaction/index.js +13 -109
  32. package/lib/transaction/trace/index.js +1 -1
  33. package/lib/transaction/trace/segment.js +6 -9
  34. package/lib/transaction/tracer/index.js +11 -8
  35. package/lib/utilization/index.js +46 -17
  36. package/package.json +1 -5
@@ -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', 'default']),
31
- default: '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', 'default']),
46
- default: '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', 'default']),
61
- default: '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 for distributed tracing sampler configuration structure
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.incomingConfig The internal configuration object being modified
71
+ * @param {object} params.configToUpdate - The internal configuration object being modified
72
72
  * @param {Logger} params.logger - The logger instance
73
73
  */
74
- function buildTraceIdRatioSamplers({ config, key, incomingConfig, logger }) {
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 multiple samplers
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
- incomingConfig[key][sampler] = {
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', 'full_granularity', 'partial_granularity'].includes(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
- buildTraceIdRatioSamplers,
195
- setTraceIdRatioSamplerFromEnv
249
+ buildSamplers,
250
+ setSamplersFromEnv
196
251
  }
@@ -32,7 +32,7 @@ exports.released = [
32
32
  'langchain_instrumentation',
33
33
  'native_metrics',
34
34
  'new_promise_tracking',
35
- 'opentelemetry_bridge',
35
+ 'opentelemetry',
36
36
  'protocol_17',
37
37
  'released',
38
38
  'send_request_uri_attribute',
@@ -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
- const proto = Router.prototype
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)
@@ -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('./normalize-timestamp.js')
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.opentelemetry_bridge.metrics.exportInterval
27
- const exportTimeout = config.opentelemetry_bridge.metrics.exportTimeout
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
- // It's unclear why upstream tries to support a `process.hrtime()` value.
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.opentelemetry_bridge.enabled !== true) {
22
+ if (agent.config.opentelemetry.enabled !== true) {
23
23
  logger.warn(
24
- '`opentelemetry_bridge` is not enabled, skipping setup of opentelemetry-bridge'
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.opentelemetry_bridge.traces.enabled === true) {
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('`opentelemetry_bridge.traces` is not enabled, skipping')
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.opentelemetry_bridge.metrics.enabled === true) {
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('`opentelemetry_bridge.metrics` is not enabled, skipping')
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.opentelemetry_bridge.logs.enabled === true) {
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('`opentelemetry_bridge.logs` is not enabled, skipping')
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?.opentelemetry_bridge?.enabled !== true) {
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
- return createConsumerSegment(this.agent, otelSpan, rule)
42
+ mapResult = createConsumerSegment(this.agent, otelSpan, rule)
43
+ break
40
44
  }
41
45
 
42
46
  case 'db': {
43
- return createDbSegment(this.agent, otelSpan, rule)
47
+ mapResult = createDbSegment(this.agent, otelSpan, rule)
48
+ break
44
49
  }
45
50
 
46
51
  case 'external': {
47
- return createHttpExternalSegment(this.agent, otelSpan, rule, this.logger)
52
+ mapResult = createHttpExternalSegment(this.agent, otelSpan, rule, this.logger)
53
+ break
48
54
  }
49
55
 
50
56
  case 'internal': {
51
- return createInternalSegment(this.agent, otelSpan, rule)
57
+ mapResult = createInternalSegment(this.agent, otelSpan, rule)
58
+ break
52
59
  }
53
60
 
54
61
  case 'producer': {
55
- return createProducerSegment(this.agent, otelSpan, rule)
62
+ mapResult = createProducerSegment(this.agent, otelSpan, rule)
63
+ break
56
64
  }
57
65
 
58
66
  case 'server': {
59
- return createServerSegment(this.agent, otelSpan, rule)
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 })
@@ -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
- return data[key] ?? 'unknown'
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
- class AdaptiveSampler {
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
- this._samplingTarget = opts.target
23
- this._maxSamples = 2 * opts.target
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
  */