Skip to content

Commit ad63441

Browse files
refactor: Break samplers into classes and store them on agent.sampler.* (#3527)
1 parent d89aae9 commit ad63441

File tree

16 files changed

+445
-123
lines changed

16 files changed

+445
-123
lines changed

lib/agent.js

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
'use strict'
77

8-
const AdaptiveSampler = require('./samplers/adaptive-sampler')
8+
const determineSampler = require('./samplers/determine-sampler')
99
const CollectorAPI = require('./collector/api')
1010
const ServerlessCollector = require('./collector/serverless')
1111
const DESTINATIONS = require('./config/attribute-filter').DESTINATIONS
@@ -287,12 +287,14 @@ function Agent(config) {
287287
this.collector,
288288
this.harvester
289289
)
290-
this.transactionSampler = new AdaptiveSampler({
291-
agent: this,
292-
serverless: config.serverless_mode.enabled,
293-
period: config.sampling_target_period_in_seconds * 1000,
294-
target: config.sampling_target
295-
})
290+
291+
// Instantiate sampling decisions.
292+
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.
295+
this.sampler.root = determineSampler({ agent: this, config, sampler: 'root' })
296+
this.sampler.remoteParentSampled = determineSampler({ agent: this, config, sampler: 'remote_parent_sampled' })
297+
this.sampler.remoteParentNotSampled = determineSampler({ agent: this, config, sampler: 'remote_parent_not_sampled' })
296298

297299
this.queries = new QueryTraceAggregator(
298300
{
@@ -848,10 +850,10 @@ Agent.prototype._listenForConfigChanges = function _listenForConfigChanges() {
848850
self.txSegmentNormalizer.load.apply(self.txSegmentNormalizer, arguments)
849851
})
850852
this.config.on('sampling_target', function updateSamplingTarget(target) {
851-
self.transactionSampler.samplingTarget = target
853+
self.sampler.root.samplingTarget = target
852854
})
853855
this.config.on('sampling_target_period_in_seconds', function updateSamplePeriod(period) {
854-
self.transactionSampler.samplingPeriod = period * 1000
856+
self.sampler.root.samplingPeriod = period * 1000
855857
})
856858
this.config.on('event_harvest_config', function onHarvestConfigReceived(harvestConfig) {
857859
if (harvestConfig) {

lib/samplers/README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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`.
8+
9+
### Types
10+
11+
- A "sampler mode" refers to the following config sections: `distributed_tracing.sampler`, `distributed_tracing.sampler.full_granularity`, 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+
17+
NOTE: `distributed_tracing.sampler` only exists for backward compatiability and may be deprecated in favor of `distributed_tracing.sampler.full_granularity`. For now, `full_granularity` will take precedence over the old path.
18+
19+
### Full Config in Accordance to Spec
20+
21+
```yaml
22+
...
23+
(Serverless DT attributes; they may be defined under distributed_tracing instead)
24+
account_id: string (unset by default, only set when using Serverless Mode)
25+
trusted_account_key: string (unset by default, only set when using Serverless Mode)
26+
primary_application_id: string (unset by default, only set when using Serverless Mode)
27+
...
28+
distributed_tracing:
29+
enabled: boolean (default true)
30+
exclude_newrelic_header: boolean (default false)
31+
enable_success_metrics (OPTIONAL): boolean (default true, set to false to disable supportability metrics)
32+
sampler: (section for sampling config options for different scenarios)
33+
adaptive_sampling_target (see note on Sampling Target below)
34+
root: (when the trace originates from the current service)
35+
${SAMPLER_TYPE} (See `Sampler Options` below)
36+
${SAMPLER_SUBOPTION}
37+
remote_parent_sampled: (when the upstream service has sampled the trace)
38+
${SAMPLER_TYPE} (See `Sampler Options` below)
39+
${SAMPLER_SUBOPTION}
40+
remote_parent_not_sampled: (when the upstream service has not sampled the trace)
41+
${SAMPLER_TYPE}
42+
${SAMPLER_SUBOPTION}
43+
full_granularity:
44+
enabled
45+
root: (when the trace originates from the current service)
46+
${SAMPLER_TYPE} (See `Sampler Options` below)
47+
${SAMPLER_SUBOPTION}
48+
remote_parent_sampled: (when the upstream service has sampled the trace)
49+
${SAMPLER_TYPE} (See `Sampler Options` below)
50+
${SAMPLER_SUBOPTION}
51+
remote_parent_not_sampled: (when the upstream service has not sampled the trace)
52+
${SAMPLER_TYPE}
53+
${SAMPLER_SUBOPTION}
54+
partial_granularity:
55+
enabled
56+
type ("reduced", "essential", "compact")
57+
root: (when the trace originates from the current service)
58+
${SAMPLER_TYPE} (See `Sampler Options` below)
59+
${SAMPLER_SUBOPTION}
60+
remote_parent_sampled: (when the upstream service has sampled the trace)
61+
${SAMPLER_TYPE} (See `Sampler Options` below)
62+
${SAMPLER_SUBOPTION}
63+
remote_parent_not_sampled: (when the upstream service has not sampled the trace)
64+
${SAMPLER_TYPE}
65+
${SAMPLER_SUBOPTION}
66+
...
67+
```
68+
69+
## Solution
70+
71+
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.
72+
73+
`agent.sampler` would be defined as:
74+
75+
* `agent.sampler.fullGranularity.root`
76+
* `agent.sampler.fullGranularity.remoteParentSampled`
77+
* `agent.sampler.fullGranularity.remoteParentNotSampled`
78+
* `agent.sampler.partialGranularity.root`
79+
* `agent.sampler.partialGranularity.remoteParentSampled`
80+
* `agent.sampler.partialGranularity.remoteParentNotSampled`
81+
82+
These fields currently exist (before core tracing was implemented); `agent.sampler.fullGranularity.*` will take precedence over these fields:
83+
84+
* `agent.sampler.root`
85+
* `agent.sampler.remoteParentSampled`
86+
* `agent.sampler.remoteParentNotSampled`
87+
88+
These samplers have a `applySamplingDecision({transaction})` function, which `Transaction` calls (in `lib/transaction/index.js`) to update its `sampled` field and therefore its `priority`.
89+
90+
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.

lib/samplers/adaptive-sampler.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
'use strict'
77

8-
class AdaptiveSampler {
8+
const Sampler = require('./sampler')
9+
10+
class AdaptiveSampler extends Sampler {
911
/**
1012
*
1113
* @param {object} opts Sampler options.
@@ -15,6 +17,7 @@ class AdaptiveSampler {
1517
* @param {boolean} opts.serverless - Indicates if the environment is serverless.
1618
*/
1719
constructor(opts) {
20+
super() // no-op
1821
this._serverless = opts.serverless
1922
this._seen = 0
2023
this._sampled = 0
@@ -33,6 +36,10 @@ class AdaptiveSampler {
3336
}
3437
}
3538

39+
toString() {
40+
return 'AdaptiveSampler'
41+
}
42+
3643
get sampled() {
3744
return this._sampled
3845
}
@@ -99,6 +106,25 @@ class AdaptiveSampler {
99106
return false
100107
}
101108

109+
applySamplingDecision({ transaction, tracestate = null }) {
110+
if (!transaction) return
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+
// eslint-disable-next-line sonarjs/pseudo-random
120+
const initPriority = Math.random()
121+
transaction.sampled = this.shouldSample(initPriority)
122+
transaction.priority = transaction.sampled ? initPriority + 1 : initPriority
123+
// Truncate the priority after potentially modifying it to avoid floating
124+
// point errors.
125+
transaction.priority = ((transaction.priority * 1e6) | 0) / 1e6
126+
}
127+
102128
/**
103129
* Starts a new sample period after adjusting the sampling statistics.
104130
*/

lib/samplers/always-off-sampler.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
const Sampler = require('./sampler')
7+
8+
class AlwaysOffSampler extends Sampler {
9+
applySamplingDecision({ transaction }) {
10+
if (!transaction) return
11+
transaction.priority = 0
12+
transaction.sampled = false
13+
}
14+
}
15+
16+
module.exports = AlwaysOffSampler

lib/samplers/always-on-sampler.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
const Sampler = require('./sampler')
7+
8+
class AlwaysOnSampler extends Sampler {
9+
applySamplingDecision({ transaction }) {
10+
if (!transaction) return
11+
transaction.priority = 2.0
12+
transaction.sampled = true
13+
}
14+
}
15+
16+
module.exports = AlwaysOnSampler

lib/samplers/determine-sampler.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
const AdaptiveSampler = require('./adaptive-sampler')
7+
const AlwaysOffSampler = require('./always-off-sampler')
8+
const AlwaysOnSampler = require('./always-on-sampler')
9+
10+
/**
11+
* A helper function for the Agent to determine what sampler
12+
* to use and will log messages about the chosen sampler.
13+
* @param {*} params Function parameters.
14+
* @param {Agent} params.agent The New Relic agent instance.
15+
* @param {object} params.config The entire agent config.
16+
* @param {string} params.sampler The sampler type to use: 'root', 'remote_parent_sampled', or 'remote_parent_not_sampled'.
17+
* @returns {Sampler} A Sampler e.g. AdaptiveSampler
18+
*/
19+
function determineSampler({ agent, config, sampler = 'root' }) {
20+
const samplerDefinition = config?.distributed_tracing?.sampler?.[sampler]
21+
22+
// Always on?
23+
if (samplerDefinition === 'always_on') {
24+
return new AlwaysOnSampler()
25+
}
26+
27+
// Always off?
28+
if (samplerDefinition === 'always_off') {
29+
return new AlwaysOffSampler()
30+
}
31+
32+
// Our default ^.^
33+
// We'll ignore https://github.com/newrelic/node-newrelic/issues/3519 for now 0.o
34+
return new AdaptiveSampler({
35+
agent,
36+
serverless: config.serverless_mode.enabled,
37+
period: config.sampling_target_period_in_seconds * 1000,
38+
target: config.sampling_target // getter/setter for distributed_tracing.sampler.adaptive_sampling_target
39+
})
40+
}
41+
42+
module.exports = determineSampler

lib/samplers/sampler.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
/**
7+
* @private
8+
* @interface
9+
*/
10+
class Sampler {
11+
/**
12+
* Sets `priority` and `sampled` on the transaction
13+
* in respect to this sampler's decision.
14+
* @param {object} params to make the sampling decision with
15+
* @param {Transaction} params.transaction the transaction to update
16+
*/
17+
applySamplingDecision({ transaction }) {
18+
throw new Error('must implement applySamplingDecision for %s', transaction)
19+
}
20+
}
21+
22+
module.exports = Sampler

0 commit comments

Comments
 (0)