Skip to content

Commit f281f19

Browse files
feat: Added configuration for ratio based sampling for distributed tracing (#3438)
Co-authored-by: James Sumners <jsumners@newrelic.com>
1 parent 2e38914 commit f281f19

File tree

5 files changed

+446
-5
lines changed

5 files changed

+446
-5
lines changed

lib/config/default.js

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,6 +1166,32 @@ defaultConfig.definition = () => {
11661166
formatter: int,
11671167
default: 10
11681168
},
1169+
// The root sampling options are defined in an unusual way compared to the rest of our configuration but this is done to
1170+
// allow compatibility with how OpenTelemetry sets their samplers and be backwards compatible as well. Root can either be
1171+
// a string or an object.
1172+
//
1173+
// Example setting root sampler via config to a string value -
1174+
// root: 'always_on'
1175+
//
1176+
// Example setting root sampler via config to trace id ratio based -
1177+
// root: {
1178+
// trace_id_ratio_based: {
1179+
// ratio: 0.5
1180+
// }
1181+
// }
1182+
//
1183+
root: {
1184+
formatter: allowList.bind(null, ['trace_id_ratio_based', 'adaptive', 'always_on', 'always_off', 'default']),
1185+
default: 'default',
1186+
// if setting root to trace_id_ratio_based via env vars, set NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT: 'trace_id_ratio_based'
1187+
// and then set NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT_TRACE_ID_RATIO_BASED_RATIO to the desired ratio
1188+
trace_id_ratio_based: {
1189+
ratio: {
1190+
formatter: float,
1191+
env: 'NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT_TRACE_ID_RATIO_BASED_RATIO'
1192+
}
1193+
}
1194+
},
11691195

11701196
/**
11711197
* When set to `always_on`, the sampled flag in the `traceparent` header
@@ -1178,8 +1204,16 @@ defaultConfig.definition = () => {
11781204
* setting.
11791205
*/
11801206
remote_parent_sampled: {
1181-
formatter: allowList.bind(null, ['always_on', 'always_off', 'default']),
1182-
default: 'default'
1207+
formatter: allowList.bind(null, ['trace_id_ratio_based', 'adaptive', 'always_on', 'always_off', 'default']),
1208+
default: 'default',
1209+
// if setting remote_parent_sampled to trace_id_ratio_based via env vars, set NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED: 'trace_id_ratio_based'
1210+
// and then set NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED_TRACE_ID_RATIO_BASED_RATIO to the desired ratio
1211+
trace_id_ratio_based: {
1212+
ratio: {
1213+
formatter: float,
1214+
env: 'NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED_TRACE_ID_RATIO_BASED_RATIO'
1215+
}
1216+
}
11831217
},
11841218

11851219
/**
@@ -1193,8 +1227,16 @@ defaultConfig.definition = () => {
11931227
* is set to 0.
11941228
*/
11951229
remote_parent_not_sampled: {
1196-
formatter: allowList.bind(null, ['always_on', 'always_off', 'default']),
1197-
default: 'default'
1230+
formatter: allowList.bind(null, ['trace_id_ratio_based', 'adaptive', 'always_on', 'always_off', 'default']),
1231+
default: 'default',
1232+
// if setting remote_parent_not_sampled to trace_id_ratio_based via env vars, set NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED: 'trace_id_ratio_based'
1233+
// and then set NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED_TRACE_ID_RATIO_BASED_RATIO to the desired ratio
1234+
trace_id_ratio_based: {
1235+
ratio: {
1236+
formatter: float,
1237+
env: 'NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED_TRACE_ID_RATIO_BASED_RATIO'
1238+
}
1239+
}
11981240
}
11991241
}
12001242
},

lib/config/index.js

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1060,10 +1060,13 @@ Config.prototype._fromPassed = function _fromPassed(external, internal, arbitrar
10601060

10611061
for (const key of Object.keys(external)) {
10621062
// if it's not in the defaults, it doesn't exist
1063-
if (!arbitrary && !(key in internal)) {
1063+
if (!arbitrary && Object.hasOwn(internal, key) === false) {
10641064
continue
10651065
}
10661066

1067+
// Handle trace_id_ratio_based for distributed tracing sampler configuration
1068+
this._buildTraceIdRatioSamplers(key, internal, external[key])
1069+
10671070
if (key === 'ssl' && !isTruthular(external.ssl)) {
10681071
logger.warn(SSL_WARNING)
10691072
continue
@@ -1087,6 +1090,54 @@ Config.prototype._fromPassed = function _fromPassed(external, internal, arbitrar
10871090
}
10881091
}
10891092

1093+
/**
1094+
* Builds the trace_id_ratio_based for distributed tracing sampler configuration structure
1095+
*
1096+
* @param {string} key - The configuration key being processed
1097+
* @param {object} internal - The internal configuration object being modified
1098+
* @param {object} samplerConfig - Sampler configuration object from defaultsConfig
1099+
*/
1100+
Config.prototype._buildTraceIdRatioSamplers = function _buildTraceIdRatioSamplers(key, internal, samplerConfig) {
1101+
if (key !== 'sampler') {
1102+
return
1103+
}
1104+
1105+
const samplers = Object.keys(samplerConfig)
1106+
1107+
// user can set multiple samplers
1108+
for (const sampler of samplers) {
1109+
if (this._isTraceIdRatioBasedConfig(sampler, samplerConfig)) {
1110+
const ratioValue = samplerConfig[sampler].trace_id_ratio_based.ratio
1111+
if (ratioValue) {
1112+
internal[key][sampler] = {
1113+
trace_id_ratio_based: {
1114+
ratio: ratioValue
1115+
}
1116+
}
1117+
logger.trace('Setting trace id ratio based sampler on %s', samplers)
1118+
} else {
1119+
logger.error('Not setting trace id ratio based sampler on %s.', samplers)
1120+
}
1121+
}
1122+
}
1123+
}
1124+
1125+
/**
1126+
* Checks if the external config contains a trace_id_ratio_based for distributed tracing
1127+
* sampler configuration
1128+
*
1129+
* @param {string} selectedSampler - The selected sampler key
1130+
* @param {object} samplerConfig - The sampler configuration object
1131+
* @returns {boolean} true if it's a trace_id_ratio_based config
1132+
*/
1133+
Config.prototype._isTraceIdRatioBasedConfig = function _isTraceIdRatioBasedConfig(selectedSampler, samplerConfig) {
1134+
const samplers = ['root', 'remote_parent_sampled', 'remote_parent_not_sampled']
1135+
1136+
return samplers.includes(selectedSampler) &&
1137+
typeof samplerConfig[selectedSampler] === 'object' &&
1138+
'trace_id_ratio_based' in samplerConfig[selectedSampler]
1139+
}
1140+
10901141
/**
10911142
* Some values should be picked up only if they're not otherwise set, like
10921143
* the Windows / Azure application name. Don't set it if there's already
@@ -1163,6 +1214,53 @@ function setFromEnv({ config, key, envVar, formatter, paths }) {
11631214
}
11641215
}
11651216

1217+
/**
1218+
* Assigns the value of the distributed tracing sampler env var as an trace_id_ratio_based
1219+
* object to the sampler in the config.
1220+
*
1221+
* @param {object} params object passed to fn
1222+
* @param {string} params.key key of the sampler
1223+
* Example: 'root' or 'remote_parent_sampled'
1224+
* @param {object} params.value value of the sampler
1225+
* Example: { default: 'default', formatter: someFunc, trace_id_ratio_based: {ratio} }
1226+
* @param {object} params.config agent config
1227+
* @param {Array} params.paths list of leaf nodes leading to the sampling configuration value
1228+
* Example: ['distributed_tracing', 'sampler']
1229+
*/
1230+
function setTraceIDRatioSampler({ key, value, config, paths }) {
1231+
// trace id ratio based sampler is nested in leaf nodes under distributed_tracing > samplers > key
1232+
// so don't continue if path is empty
1233+
if (paths.length === 0) {
1234+
return
1235+
}
1236+
1237+
const samplers = ['root', 'remote_parent_sampled', 'remote_parent_not_sampled']
1238+
if (samplers.includes(key) && paths[paths.length - 1] === 'sampler') {
1239+
// config.distributed_tracing.sampler.[TYPE OF SAMPLER] needs to be set to trace_id_ratio_based
1240+
// in order to continue
1241+
if (config.distributed_tracing.sampler[key] !== 'trace_id_ratio_based') {
1242+
return
1243+
}
1244+
1245+
// set the trace id based ratio
1246+
const envVar = value.trace_id_ratio_based.ratio.env
1247+
const formatter = value.trace_id_ratio_based.ratio.formatter
1248+
const setting = process.env[envVar]
1249+
if (setting) {
1250+
const formattedSetting = formatter(setting)
1251+
config.distributed_tracing.sampler[key] = {
1252+
trace_id_ratio_based: {
1253+
ratio: formattedSetting
1254+
}
1255+
}
1256+
logger.trace('Setting %s environment variable', envVar)
1257+
} else {
1258+
logger.error('Not setting %s environment variable. Setting sampler root to default.', envVar)
1259+
config.distributed_tracing.sampler.root = 'default'
1260+
}
1261+
}
1262+
}
1263+
11661264
/**
11671265
* Recursively visit the nodes of the config definition and look for environment variable names, overriding any configuration values that are found.
11681266
*
@@ -1201,6 +1299,17 @@ Config.prototype._fromEnvironment = function _fromEnvironment(
12011299
if (Object.prototype.hasOwnProperty.call(value, 'default') === true) {
12021300
const envVar = deriveEnvVar(key, paths)
12031301
setFromEnv({ config, key, paths, envVar, formatter: value.formatter })
1302+
1303+
// Handle custom configuration of the sampler if it's set to trace_id_ratio_based
1304+
// since sampler was set to a string but now has to be converted to an object.
1305+
// This is called here because we need set the sampler to a value first in config before setting
1306+
// trace id ratio sampler (if the user set that)
1307+
setTraceIDRatioSampler({
1308+
key,
1309+
value,
1310+
config,
1311+
paths
1312+
})
12041313
continue
12051314
}
12061315

test/unit/config/config-defaults.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,18 @@ test('with default properties', async (t) => {
333333
assert.equal(configuration.sampling_target, 10)
334334
})
335335

336+
await t.test('root sampler should default to default', () => {
337+
assert.equal(configuration.distributed_tracing.sampler.root, 'default')
338+
})
339+
340+
await t.test('remote parent sampled sampler should default to default', () => {
341+
assert.equal(configuration.distributed_tracing.sampler.remote_parent_sampled, 'default')
342+
})
343+
344+
await t.test('remote parent not sampled sampler should default to default', () => {
345+
assert.equal(configuration.distributed_tracing.sampler.remote_parent_not_sampled, 'default')
346+
})
347+
336348
await t.test('opentelemetry', () => {
337349
const otel = configuration.opentelemetry_bridge
338350
assert.equal(otel.enabled, false)

test/unit/config/config-env.test.js

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,139 @@ test('when overriding configuration values via environment variables', async (t)
188188
})
189189
})
190190

191+
await t.test('should set root sampler to always_on', (t, end) => {
192+
const env = {
193+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT: 'always_on',
194+
}
195+
196+
idempotentEnv(env, (tc) => {
197+
assert.equal(tc.distributed_tracing.sampler.root, 'always_on')
198+
end()
199+
})
200+
})
201+
202+
await t.test('should set root sampler to trace id based ratio', (t, end) => {
203+
const env = {
204+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT: 'trace_id_ratio_based',
205+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT_TRACE_ID_RATIO_BASED_RATIO: '0.5'
206+
}
207+
208+
idempotentEnv(env, (tc) => {
209+
assert.equal(tc.distributed_tracing.sampler.root.trace_id_ratio_based.ratio, 0.5)
210+
end()
211+
})
212+
})
213+
214+
await t.test('should set remote_parent_sampled sampler to trace id based ratio', (t, end) => {
215+
const env = {
216+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED: 'trace_id_ratio_based',
217+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED_TRACE_ID_RATIO_BASED_RATIO: '0.5'
218+
}
219+
220+
idempotentEnv(env, (tc) => {
221+
assert.equal(tc.distributed_tracing.sampler.remote_parent_sampled.trace_id_ratio_based.ratio, 0.5)
222+
end()
223+
})
224+
})
225+
226+
await t.test('should set remote_parent_not_sampled sampler to trace id based ratio', (t, end) => {
227+
const env = {
228+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED: 'trace_id_ratio_based',
229+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED_TRACE_ID_RATIO_BASED_RATIO: '0.5'
230+
}
231+
232+
idempotentEnv(env, (tc) => {
233+
assert.equal(tc.distributed_tracing.sampler.remote_parent_not_sampled.trace_id_ratio_based.ratio, 0.5)
234+
end()
235+
})
236+
})
237+
238+
await t.test('should fall back to default root sampler when trace id ratio based is misconfigured', (t, end) => {
239+
const env = {
240+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT: 'trace_id_ratio_based',
241+
// Missing "RATIO" key at the end of this env var
242+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT_TRACE_ID_RATIO_BASED: '0.5'
243+
}
244+
245+
idempotentEnv(env, (tc) => {
246+
assert.equal(tc.distributed_tracing.sampler.root, 'default')
247+
end()
248+
})
249+
})
250+
251+
await t.test('should fall back to default root sampler when trace id ratio is missing sampler root', (t, end) => {
252+
const env = {
253+
// NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT: 'trace_id_ratio_based' is needed to enable this sampler
254+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT_TRACE_ID_RATIO_BASED: '0.5'
255+
}
256+
257+
idempotentEnv(env, (tc) => {
258+
assert.equal(tc.distributed_tracing.sampler.root, 'default')
259+
end()
260+
})
261+
})
262+
263+
await t.test('should ignore trace id ratio based ratio env var when sampler root is set to a different value', (t, end) => {
264+
const env = {
265+
// NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT: 'trace_id_ratio_based' is needed to enable this sampler
266+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT: 'always_on',
267+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT_TRACE_ID_RATIO_BASED: '0.5'
268+
}
269+
270+
idempotentEnv(env, (tc) => {
271+
assert.equal(tc.distributed_tracing.sampler.root, 'always_on')
272+
assert.equal(tc.distributed_tracing.sampler.root.trace_id_ratio_based, undefined)
273+
end()
274+
})
275+
})
276+
277+
await t.test('should set root and remote parent sampled but leave remote parent not sampled as default', (t, end) => {
278+
const env = {
279+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT: 'trace_id_ratio_based',
280+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT_TRACE_ID_RATIO_BASED_RATIO: '0.5',
281+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED: 'always_on'
282+
}
283+
284+
idempotentEnv(env, (tc) => {
285+
assert.equal(tc.distributed_tracing.sampler.root.trace_id_ratio_based.ratio, 0.5)
286+
assert.equal(tc.distributed_tracing.sampler.remote_parent_sampled, 'always_on')
287+
end()
288+
})
289+
})
290+
291+
await t.test('should set all samplers to trace id ratio based', (t, end) => {
292+
const env = {
293+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT: 'trace_id_ratio_based',
294+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT_TRACE_ID_RATIO_BASED_RATIO: '0.5',
295+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED: 'trace_id_ratio_based',
296+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED_TRACE_ID_RATIO_BASED_RATIO: '0.6',
297+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED: 'trace_id_ratio_based',
298+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED_TRACE_ID_RATIO_BASED_RATIO: '0.85'
299+
}
300+
301+
idempotentEnv(env, (tc) => {
302+
assert.equal(tc.distributed_tracing.sampler.root.trace_id_ratio_based.ratio, 0.5)
303+
assert.equal(tc.distributed_tracing.sampler.remote_parent_sampled.trace_id_ratio_based.ratio, 0.6)
304+
assert.equal(tc.distributed_tracing.sampler.remote_parent_not_sampled.trace_id_ratio_based.ratio, 0.85)
305+
end()
306+
})
307+
})
308+
309+
await t.test('should set all samplers to a string', (t, end) => {
310+
const env = {
311+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT: 'always_on',
312+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED: 'default',
313+
NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED: 'adaptive',
314+
}
315+
316+
idempotentEnv(env, (tc) => {
317+
assert.equal(tc.distributed_tracing.sampler.root, 'always_on')
318+
assert.equal(tc.distributed_tracing.sampler.remote_parent_sampled, 'default')
319+
assert.equal(tc.distributed_tracing.sampler.remote_parent_not_sampled, 'adaptive')
320+
end()
321+
})
322+
})
323+
191324
await t.test('should pick up on the span events env vars', (t, end) => {
192325
const env = {
193326
NEW_RELIC_SPAN_EVENTS_ENABLED: true,

0 commit comments

Comments
 (0)