Skip to content

Commit 44f51dd

Browse files
feat: adaptive.sampling_target can be set under sampler sections (#3532)
1 parent f300bd5 commit 44f51dd

File tree

10 files changed

+608
-82
lines changed

10 files changed

+608
-82
lines changed

lib/agent.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -290,8 +290,10 @@ function Agent(config) {
290290

291291
// Instantiate sampling decisions.
292292
this.sampler = {}
293-
// TODO: This could create duplicate AdaptiveSampler instances of the same
294-
// `sampling_target`, fix will be addressed in https://github.com/newrelic/node-newrelic/issues/3519.
293+
// This is the global adaptive sampler instance used across
294+
// different sampling modes and not intended to be used directly.
295+
this.sampler._globalAdaptiveSampler = null
296+
// The samplers to use for a particular sampler mode.
295297
this.sampler.root = determineSampler({ agent: this, config, sampler: 'root' })
296298
this.sampler.remoteParentSampled = determineSampler({ agent: this, config, sampler: 'remote_parent_sampled' })
297299
this.sampler.remoteParentNotSampled = determineSampler({ agent: this, config, sampler: 'remote_parent_not_sampled' })
@@ -850,10 +852,17 @@ Agent.prototype._listenForConfigChanges = function _listenForConfigChanges() {
850852
self.txSegmentNormalizer.load.apply(self.txSegmentNormalizer, arguments)
851853
})
852854
this.config.on('sampling_target', function updateSamplingTarget(target) {
853-
self.sampler.root.samplingTarget = target
855+
// Remember config.sampling_target is an alias for
856+
// config.distributed_tracing.sampler.adaptive_sampling_target,
857+
// which is the sampling target for the global adaptive sampler.
858+
if (self.sampler._globalAdaptiveSampler) {
859+
self.sampler._globalAdaptiveSampler.samplingTarget = target
860+
}
854861
})
855862
this.config.on('sampling_target_period_in_seconds', function updateSamplePeriod(period) {
856-
self.sampler.root.samplingPeriod = period * 1000
863+
if (self.sampler._globalAdaptiveSampler) {
864+
self.sampler._globalAdaptiveSampler.samplingPeriod = period * 1000
865+
}
857866
})
858867
this.config.on('event_harvest_config', function onHarvestConfigReceived(harvestConfig) {
859868
if (harvestConfig) {

lib/config/index.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const mergeServerConfig = new MergeServerConfig()
2525
const { boolean: isTruthular } = require('./formatters')
2626
const configDefinition = definition()
2727
const parseLabels = require('../util/label-parser')
28-
const { buildTraceIdRatioSamplers, setTraceIdRatioSamplerFromEnv } = require('./samplers')
28+
const { buildSamplers, setSamplersFromEnv } = require('./samplers')
2929

3030
/**
3131
* CONSTANTS -- we gotta lotta 'em
@@ -125,6 +125,8 @@ function Config(config) {
125125
this.trusted_account_ids = null
126126
this.trusted_account_key = null
127127

128+
// The global sampling target for AdaptiveSamplers that do not
129+
// specify their own sampling_target
128130
this.sampling_target = this.distributed_tracing.sampler.adaptive_sampling_target
129131
this.sampling_target_period_in_seconds = 60
130132
this.max_payload_size_in_bytes = DEFAULT_MAX_PAYLOAD_SIZE_IN_BYTES
@@ -1065,8 +1067,8 @@ Config.prototype._fromPassed = function _fromPassed(external, internal, arbitrar
10651067
continue
10661068
}
10671069

1068-
// Handle trace_id_ratio_based for distributed tracing sampler configuration
1069-
buildTraceIdRatioSamplers({ config: external, key, incomingConfig: internal, logger })
1070+
// Handle object-based distributed tracing sampler configurations
1071+
buildSamplers({ config: external, key, configToUpdate: internal, logger })
10701072

10711073
if (key === 'ssl' && !isTruthular(external.ssl)) {
10721074
logger.warn(SSL_WARNING)
@@ -1207,10 +1209,11 @@ Config.prototype._fromEnvironment = function _fromEnvironment(
12071209
setFromEnv({ config, key, paths, envVar, formatter: value.formatter })
12081210

12091211
// Handle custom configuration of the sampler if it's set to trace_id_ratio_based
1210-
// since sampler was set to a string but now has to be converted to an object.
1211-
// This is called here because we need set the sampler to a value first in config before setting
1212-
// trace id ratio sampler (if the user set that)
1213-
setTraceIdRatioSamplerFromEnv({
1212+
// or adaptive with adaptive.sampling_target defined, since sampler was set to a
1213+
// string but now has to be converted to an object.
1214+
// This is called here because we need to set the sampler to a value first in config
1215+
// before setting the samplers.
1216+
setSamplersFromEnv({
12141217
key,
12151218
config,
12161219
paths,

lib/config/samplers.js

Lines changed: 105 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
'use strict'
7-
const { allowList, float } = require('./formatters')
7+
const { allowList, float, int } = require('./formatters')
88

99
/**
1010
* This configuration is currently the same for `distributed_tracing.sampler` and `distributed_tracing.sampler.partial_granularity`
@@ -62,28 +62,46 @@ const config = {
6262
}
6363

6464
/**
65-
* 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.
6667
*
6768
* @param {object} params to function
6869
* @param {object} params.config - The full configuration object
6970
* @param {string} params.key - The configuration key being processed
70-
* @param {object} params.incomingConfig The internal configuration object being modified
71+
* @param {object} params.configToUpdate - The internal configuration object being modified
7172
* @param {Logger} params.logger - The logger instance
7273
*/
73-
function buildTraceIdRatioSamplers({ config, key, incomingConfig, logger }) {
74+
function buildSamplers({ config, key, configToUpdate, logger }) {
7475
if (!isValidDTSampler(key)) {
7576
return
7677
}
7778

7879
const samplerConfig = config[key]
7980
const samplers = Object.keys(samplerConfig)
8081

81-
// 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`
8285
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+
83101
if (isTraceIdRatioBasedConfig(sampler, samplerConfig)) {
84102
const ratioValue = samplerConfig[sampler].trace_id_ratio_based.ratio
85-
if (ratioValue) {
86-
incomingConfig[key][sampler] = {
103+
if (typeof ratioValue === 'number') {
104+
configToUpdate[key][sampler] = {
87105
trace_id_ratio_based: {
88106
ratio: ratioValue
89107
}
@@ -96,6 +114,69 @@ function buildTraceIdRatioSamplers({ config, key, incomingConfig, logger }) {
96114
}
97115
}
98116

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 `default`', envVar, paths.join('.'), key)
159+
setNestedKey(config, [...paths, key], 'default')
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+
const formattedSetting = int(setting)
167+
168+
if (formattedSetting >= 1 && formattedSetting <= 120) {
169+
setNestedKey(config, [...paths, key], { adaptive: { sampling_target: formattedSetting } })
170+
logger.trace('Setting %s environment variable to %s', envVar, formattedSetting)
171+
} else {
172+
logger.trace('Not setting %s environment variable; value not in range [1,120]. Setting %s.%s as `adaptive` with no `sampling_target',
173+
envVar,
174+
paths.join('.'),
175+
key)
176+
setNestedKey(config, [...paths, key], 'adaptive')
177+
}
178+
}
179+
99180
/**
100181
* Checks if the external config contains a trace_id_ratio_based for distributed tracing
101182
* sampler configuration
@@ -111,7 +192,21 @@ function isTraceIdRatioBasedConfig(sampler, samplerConfig) {
111192
}
112193

113194
/**
114-
* Check if the sampler is a valid value of either 'sampler' or 'partial_granularity'
195+
* Checks if the external config contains a adaptive object
196+
* for distributed tracing sampler configuration
197+
*
198+
* @param {string} sampler - The selected sampler key
199+
* @param {object} samplerConfig - The sampler configuration object
200+
* @returns {boolean} true if it's a adaptive config
201+
*/
202+
function isAdaptiveSamplingTargetConfig(sampler, samplerConfig) {
203+
return isValidDTSamplerType(sampler) &&
204+
typeof samplerConfig[sampler] === 'object' &&
205+
'adaptive' in samplerConfig[sampler]
206+
}
207+
208+
/**
209+
* Check if the sampler is a valid value of either 'sampler', 'full_granularity', or 'partial_granularity'
115210
* @param {string} sampler - The sampler key to validate
116211
* @returns {boolean} true if valid, false otherwise
117212
*/
@@ -128,47 +223,6 @@ function isValidDTSamplerType(samplerType) {
128223
return ['root', 'remote_parent_sampled', 'remote_parent_not_sampled'].includes(samplerType)
129224
}
130225

131-
/**
132-
* Assigns the value of the distributed tracing samplers env var as an trace_id_ratio_based
133-
* object to the sampler in the config.
134-
*
135-
* @param {object} params object passed to fn
136-
* @param {string} params.key key of the sampler
137-
* Example: 'root' or 'remote_parent_sampled'
138-
* @param {object} params.config agent config
139-
* @param {Array} params.paths list of leaf nodes leading to the sampling configuration value
140-
* Example: ['distributed_tracing', 'sampler']
141-
* @param {Function} params.setNestedKey function to set nested key in config
142-
* @param {Logger} params.logger logger instance
143-
*/
144-
function setTraceIdRatioSamplerFromEnv({ key, config, paths, setNestedKey, logger }) {
145-
// trace id ratio based sampler is nested in leaf nodes under distributed_tracing > samplers > key
146-
// so don't continue if path is empty
147-
if (paths.length === 0) {
148-
return
149-
}
150-
151-
const lastPath = paths[paths.length - 1]
152-
if (isValidDTSampler(lastPath) && isValidDTSamplerType(key)) {
153-
// Get the value of `config.distributed_tracing.sampler[key]` or `config.distributed_tracing.sampler.partial_granularity[key]`
154-
const nestedValue = getNestedValue(config, [...paths, key])
155-
if (nestedValue !== 'trace_id_ratio_based') {
156-
return
157-
}
158-
159-
// set the trace id based ratio
160-
const envVar = `NEW_RELIC_${[...paths, key, 'trace_id_ratio_based', 'ratio'].join('_').toUpperCase()}`
161-
const setting = process.env[envVar]
162-
if (setting) {
163-
const formattedSetting = float(setting)
164-
setNestedKey(config, [...paths, key], { trace_id_ratio_based: { ratio: formattedSetting } })
165-
logger.trace('Setting %s environment variable to %s', envVar, formattedSetting)
166-
} else {
167-
logger.trace('Not setting %s environment variable. Setting %s.%s to `default`', envVar, paths.join('.'), key)
168-
setNestedKey(config, [...paths, key], 'default')
169-
}
170-
}
171-
}
172226
/**
173227
* Retrieves a value from a nested object by providing the list of parent keys.
174228
* @param {object} obj object to assign value to
@@ -190,6 +244,6 @@ function getNestedValue(obj, keys) {
190244

191245
module.exports = {
192246
config,
193-
buildTraceIdRatioSamplers,
194-
setTraceIdRatioSamplerFromEnv
247+
buildSamplers,
248+
setSamplersFromEnv
195249
}

lib/samplers/README.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ The New Relic agent supports a robust sampling decision making interface. This i
44

55
## Config
66

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`.
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`.
88

99
### Types
1010

@@ -13,10 +13,13 @@ Customers configure how they would like their transactions to be sampled under o
1313
- `root`: This is the main sampler for traces originating from the current service.
1414
- `remote_parent_sampled`: The sampler for when the upstream service has sampled the trace.
1515
- `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.
1618

1719
NOTE: `distributed_tracing.sampler` will be used as the setting for the full granularity samplers.
1820

1921
### Full Config in Accordance to Spec
22+
Full Config in Accordance to Pending Spec Changes (as of agent team discussions, 11/21/2025):
2023

2124
```yaml
2225
...
@@ -30,12 +33,12 @@ distributed_tracing:
3033
exclude_newrelic_header: boolean (default false)
3134
enable_success_metrics (OPTIONAL): boolean (default true, set to false to disable supportability metrics)
3235
sampler: (section for sampling config options for different scenarios)
33-
adaptive_sampling_target (see note on Sampling Target below)
36+
adaptive_sampling_target (see note on Sampling Target above)
3437
root: (when the trace originates from the current service)
35-
${SAMPLER_TYPE} (See `Sampler Options` below)
38+
${SAMPLER_TYPE} (See `Sampler Options` above)
3639
${SAMPLER_SUBOPTION}
3740
remote_parent_sampled: (when the upstream service has sampled the trace)
38-
${SAMPLER_TYPE} (See `Sampler Options` below)
41+
${SAMPLER_TYPE})
3942
${SAMPLER_SUBOPTION}
4043
remote_parent_not_sampled: (when the upstream service has not sampled the trace)
4144
${SAMPLER_TYPE}
@@ -46,10 +49,10 @@ distributed_tracing:
4649
enabled
4750
type ("reduced", "essential", "compact")
4851
root: (when the trace originates from the current service)
49-
${SAMPLER_TYPE} (See `Sampler Options` below)
52+
${SAMPLER_TYPE}
5053
${SAMPLER_SUBOPTION}
5154
remote_parent_sampled: (when the upstream service has sampled the trace)
52-
${SAMPLER_TYPE} (See `Sampler Options` below)
55+
${SAMPLER_TYPE}
5356
${SAMPLER_SUBOPTION}
5457
remote_parent_not_sampled: (when the upstream service has not sampled the trace)
5558
${SAMPLER_TYPE}
@@ -59,9 +62,9 @@ distributed_tracing:
5962

6063
## Solution
6164

62-
There are three sampler modes, each with three sampler sections, resulting in potentially nine 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.
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.
6366

64-
`agent.sampler` would be defined as:
67+
`agent.sampler` is defined as:
6568

6669
* `agent.sampler.root`
6770
* `agent.sampler.remoteParentSampled`
@@ -72,4 +75,4 @@ There are three sampler modes, each with three sampler sections, resulting in po
7275

7376
These samplers have a `applySamplingDecision({transaction})` function, which `Transaction` calls (in `lib/transaction/index.js`) to update its `sampled` field and therefore its `priority`.
7477

75-
Unlike the other samplers, the `AdaptiveSampler` must share state with other `AdaptiveSamplers` with the same `sampling_target`, which complicates our seperate sampler instances approach. This will be fixed shortly, and this document will be updated to describe that solution.
78+
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.

lib/samplers/adaptive-sampler.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ class AdaptiveSampler extends Sampler {
2222
this._seen = 0
2323
this._sampled = 0
2424
this._samplingPeriod = 0
25-
this._samplingTarget = opts.target
26-
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
2730
this._samplingThreshold = 0
2831
this._resetCount = 0
2932
this._resetInterval = null

0 commit comments

Comments
 (0)