newrelic
Version:
New Relic agent
272 lines (244 loc) • 12.9 kB
JavaScript
/*
* Copyright 2025 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
'use strict'
const Sampler = require('./sampler')
const AdaptiveSampler = require('./adaptive-sampler')
const AlwaysOffSampler = require('./always-off-sampler')
const AlwaysOnSampler = require('./always-on-sampler')
const TraceIdRatioBasedSampler = require('./ratio-based-sampler')
const logger = require('../logger').child({ component: 'samplers' })
const { PARTIAL_TYPES } = require('../transaction')
/**
* Manages the different samplers used for distributed tracing sampling decisions.
* Selecting the appropriate sampler based on the configuration and context is done via the `applySamplingDecision` and `applyDTSamplingDecision` methods.
*
* @typedef {object} Samplers
* @property {boolean} fullEnabled Whether full granularity sampling is enabled.
* @property {boolean} partialEnabled Whether partial granularity sampling is enabled.
* @property {AdaptiveSampler|null} adaptiveSampler The global adaptive sampler instance, this is shared if `sampling_target` is not defined for a sampler type of `adaptive`.
* @property {Sampler} root The root sampler for traces originating in application.
* @property {Sampler} remoteParentSampled The sampler for traces with a remote parent that is sampled.
* @property {Sampler} remoteParentNotSampled The sampler for traces with a remote parent that is not sampled.
* @property {Sampler} partialRoot The partial granularity root sampler for traces originating in application.
* @property {Sampler} partialRemoteParentSampled The partial granularity sampler for traces with a remote parent that is sampled.
* @property {Sampler} partialRemoteParentNotSampled The partial granularity sampler for traces with a remote parent that is not sampled.
*/
class Samplers {
constructor(agent) {
this.fullEnabled = agent.config.distributed_tracing.enabled && agent.config.distributed_tracing.sampler.full_granularity.enabled
this.partialEnabled = agent.config.distributed_tracing.enabled && agent.config.distributed_tracing.sampler.partial_granularity.enabled
this.partialType = PARTIAL_TYPES[agent.config.distributed_tracing.sampler.partial_granularity.type.toUpperCase()]
this.adaptiveSampler = null
this.root = this.determineSampler({ agent, sampler: 'root' })
this.remoteParentSampled = this.determineSampler({ agent, sampler: 'remote_parent_sampled' })
this.remoteParentNotSampled = this.determineSampler({ agent, sampler: 'remote_parent_not_sampled' })
this.partialRoot = this.determineSampler({ agent, sampler: 'root', isPartial: true })
this.partialRemoteParentSampled = this.determineSampler({ agent, sampler: 'remote_parent_sampled', isPartial: true })
this.partialRemoteParentNotSampled = this.determineSampler({ agent, sampler: 'remote_parent_not_sampled', isPartial: true })
}
/**
* Fallback sampling decision. This should only be called if DT is disabled or both full and partial granularity are disabled
* @param {object} params to function
* @param {Transaction} params.transaction The transaction to apply the sampling decision to.
*/
applyDefaultDecision({ transaction }) {
logger.trace('Both full and partial granularity samplers are disabled. Applying default sampling decision of not sampled and and a random priority between 0 and 1 for transaction %s', transaction.id)
transaction.sampled = false
transaction.priority = Sampler.generatePriority()
}
/**
* Determines if the partial granularity sampler should run based on the transaction's sampled value and if partial granularity is enabled.
* @param {Transaction} transaction The transaction to check.
* @returns {boolean} True if the partial granularity sampler should run, false otherwise.
*/
shouldRunPartialSampling(transaction) {
return transaction.sampled !== true && this.partialEnabled
}
/**
* Applies the root sampler's sampling decision to the transaction
* if priority has not already been set. If both full and partial granularity
* are in use, the full granularity sampler runs first then the partial granularity sampler.
*
* @param {Transaction} transaction The transaction to apply the sampling decision to.
*/
applySamplingDecision({ transaction }) {
if (!transaction) {
logger.trace('No transaction provided to applySamplingDecision, not applying sampler.')
return
}
if (transaction.priority !== null) {
logger.trace('Transaction %s already has a sampling decision, not applying sampler.', transaction.id)
return
}
if (this.fullEnabled === false && this.partialEnabled === false) {
this.applyDefaultDecision({ transaction })
return
}
if (this.fullEnabled) {
this.root.applySamplingDecision({ transaction })
logger.trace('Ran full granularity applySamplingDecision %s sampler for transaction %s, decision: { sampled: %s, priority: %d }', this.root.toString(), transaction.id, transaction.sampled, transaction.priority)
}
if (this.shouldRunPartialSampling(transaction)) {
this.partialRoot.applySamplingDecision({ transaction, partialType: this.partialType })
logger.trace('Ran partial granularity applySamplingDecision %s sampler for transaction %s, decision: { sampled: %s, priority: %d }', this.partialRoot.toString(), transaction.id, transaction.sampled, transaction.priority)
}
}
/**
* Applies the appropriate remote parent sampler's sampling decision
* based on if the traceparent is sampled or not.
* In the case of adaptive sampler it relies on the tracestate sampling flag
*
* @param {object} params to function
* @param {Transaction} params.transaction The transaction to apply the sampling decision to.
* @param {TraceParent} params.traceparent The W3C traceparent object.
* @param {TraceState} params.tracestate The W3C tracestate object.
*/
applyDTSamplingDecision({ transaction, traceparent, tracestate }) {
if (!transaction) {
logger.trace('No transaction provided to applyDTSamplingDecision, not applying sampler.')
return
}
if (this.fullEnabled === false && this.partialEnabled === false) {
this.applyDefaultDecision({ transaction })
return
}
if (this.fullEnabled) {
const sampler = traceparent?.isSampled ? this.remoteParentSampled : this.remoteParentNotSampled
sampler.applySamplingDecision({ transaction, tracestate })
logger.trace('Ran full granularity applyDTSamplingDecision %s sampler for transaction %s, decision: { sampled: %s, priority: %d }', sampler.toString(), transaction.id, transaction.sampled, transaction.priority)
}
if (this.shouldRunPartialSampling(transaction)) {
const partialSampler = traceparent?.isSampled ? this.partialRemoteParentSampled : this.partialRemoteParentNotSampled
partialSampler.applySamplingDecision({ transaction, tracestate, partialType: this.partialType })
logger.trace('Ran partial granularity applyDTSamplingDecision %s sampler for transaction %s, decision: { sampled: %s, priority: %d }', partialSampler.toString(), transaction.id, transaction.sampled, transaction.priority)
}
}
/**
* Even though New Relic headers are deprecated,
* we still have to apply our sampling decision on top
* of the priority and sampled values we receive.
* However, this only applies if the sampler is NOT
* the default sampler (adaptive). In that case,
* we leave it alone. ¯\_(ツ)_/¯
*
* @param {object} params to function
* @param {Transaction} params.transaction The transaction to apply the sampling decision to.
* @param {boolean} params.isSampled The sampled value from the legacy New Relic headers.
*/
applyLegacyDTSamplingDecision({ transaction, isSampled }) {
if (!transaction) {
logger.trace('No transaction provided to applyLegacyDTSamplingDecision, not applying sampler.')
return
}
if (this.fullEnabled === false && this.partialEnabled === false) {
this.applyDefaultDecision({ transaction })
return
}
if (this.fullEnabled) {
const sampler = isSampled ? this.remoteParentSampled : this.remoteParentNotSampled
if (sampler.toString() === 'AdaptiveSampler') {
logger.trace('Not running full granularity applyLegacyDTSamplingDecision for transaction %s because sampler is AdaptiveSampler', transaction.id)
} else {
sampler.applySamplingDecision({ transaction })
logger.trace('Ran full granularity applyLegacyDTSamplingDecision %s sampler for transaction %s, decision: { sampled: %s, priority: %d }', sampler.toString(), transaction.id, transaction.sampled, transaction.priority)
}
}
if (this.shouldRunPartialSampling(transaction)) {
const partialSampler = isSampled ? this.partialRemoteParentSampled : this.partialRemoteParentNotSampled
if (partialSampler.toString() === 'AdaptiveSampler') {
logger.trace('Not running partial granularity applyLegacyDTSamplingDecision for transaction %s because sampler is AdaptiveSampler', transaction.id)
} else {
partialSampler.applySamplingDecision({ transaction, partialType: this.partialType })
logger.trace('Ran partial granularity applyLegacyDTSamplingDecision %s sampler for transaction %s, decision: { sampled: %s, priority: %d }', partialSampler.toString(), transaction.id, transaction.sampled, transaction.priority)
}
}
}
/**
* Updates the adaptive sampler's target if it exists when server side config sends a new value down
* @param {number} target The new target value to set
*/
updateAdaptiveTarget(target) {
if (this.adaptiveSampler) {
this.adaptiveSampler.samplingTarget = target
}
}
/**
* Updates the adaptive sampler's period if it exists when server side config sends a new value down
* @param {number} period The new period value to set in seconds
*/
updateAdaptivePeriod(period) {
if (this.adaptiveSampler) {
this.adaptiveSampler.samplingPeriod = period * 1000
}
}
/**
* Returns the global adaptive sampler, creating it if it doesn't exist yet
* @param {Agent} agent The New Relic agent instance.
* @returns {AdaptiveSampler} The global AdaptiveSampler instance.
*/
getAdaptiveSampler(agent) {
if (this.adaptiveSampler !== null) {
return this.adaptiveSampler
}
const config = agent.config
this.adaptiveSampler = new AdaptiveSampler({
agent,
serverless: config.serverless_mode.enabled,
period: config.sampling_target_period_in_seconds * 1000,
target: config.sampling_target
})
return this.adaptiveSampler
}
/**
* Determines which sampler to use and will log messages about the chosen sampler.
* @param {object} params to function
* @param {Agent} params.agent The New Relic agent instance.
* @param {string} params.sampler The sampler type to use: 'root', 'remote_parent_sampled', or 'remote_parent_not_sampled'.
* @param {boolean} params.isPartial Whether to determine the partial granularity sampler.
* @returns {Sampler} A Sampler e.g. AdaptiveSampler
*/
determineSampler({ agent, sampler, isPartial }) {
const config = agent.config
let samplerDefinition = null
if (isPartial) {
samplerDefinition = config.distributed_tracing.sampler.partial_granularity[sampler]
} else {
samplerDefinition = config.distributed_tracing.sampler[sampler]
}
// Always on?
if (samplerDefinition === 'always_on') {
return new AlwaysOnSampler()
}
// Always off?
if (samplerDefinition === 'always_off') {
return new AlwaysOffSampler()
}
// Is it TraceIdRatioBased?
if (samplerDefinition?.trace_id_ratio_based) {
let ratio = samplerDefinition.trace_id_ratio_based?.ratio
// If both partial and full granularity for a particular section are both set to trace ratio, agent **MUST** set the partial granularity ratio = full granularity ratio + partial granularity ratio
if (isPartial && this.fullEnabled && config.distributed_tracing.sampler[sampler]?.trace_id_ratio_based?.ratio) {
ratio += config.distributed_tracing.sampler[sampler].trace_id_ratio_based.ratio
}
return new TraceIdRatioBasedSampler({
agent,
ratio
})
}
// If adaptive.sampling_target set, create a new AdaptiveSampler,
// else use the global AdaptiveSampler.
if (samplerDefinition?.adaptive?.sampling_target) {
return new AdaptiveSampler({
agent,
serverless: config.serverless_mode.enabled,
period: config.sampling_target_period_in_seconds * 1000,
target: samplerDefinition.adaptive.sampling_target
})
} else {
return this.getAdaptiveSampler(agent)
}
}
}
module.exports = Samplers