Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dynamically configurable sampling rules #3598

Merged
merged 1 commit into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion lib/datadog/tracing/configuration/dynamic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,46 @@ def configuration_object
end
end

# Dynamic configuration for `DD_TRACE_SAMPLING_RULES`.
class TracingSamplingRules < SimpleOption
def initialize
super('tracing_sampling_rules', 'DD_TRACE_SAMPLING_RULES', :rules)
end

# Ensures sampler is rebuilt and new configuration is applied
def call(tracing_sampling_rules)
# Modify the remote configuration value that it matches the
# local environment variable it configures.
if tracing_sampling_rules
tracing_sampling_rules.each do |rule|
next unless (tags = rule['tags'])

# Tag maps come in as arrays of 'key' and `value_glob`.
# We need to convert them into a hash for local use.
tag_array = tags.map! do |tag|
[tag['key'], tag['value_glob']]
end

rule['tags'] = tag_array.to_h
end

# The configuration is stored as JSON, so we need to convert it back
tracing_sampling_rules = tracing_sampling_rules.to_json
end

super(tracing_sampling_rules)
Datadog.send(:components).reconfigure_live_sampler
end

protected

def configuration_object
Datadog.configuration.tracing.sampling
end
end

# List of all tracing dynamic configuration options supported.
OPTIONS = [LogInjectionEnabled, TracingHeaderTags, TracingSamplingRate].map do |option_class|
OPTIONS = [LogInjectionEnabled, TracingHeaderTags, TracingSamplingRate, TracingSamplingRules].map do |option_class|
option = option_class.new
[option.name, option.env_var, option]
end
Expand Down
1 change: 1 addition & 0 deletions lib/datadog/tracing/configuration/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ def self.extended(base)
# @return [String,nil]
# @public_api
option :rules do |o|
o.type :string, nilable: true
o.default { ENV.fetch(Configuration::Ext::Sampling::ENV_RULES, nil) }
end

Expand Down
6 changes: 5 additions & 1 deletion lib/datadog/tracing/remote.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ class ReadError < StandardError; end
class << self
PRODUCT = 'APM_TRACING'

CAPABILITIES = [
1 << 29 # APM_TRACING_SAMPLE_RULES: Dynamic trace sampling rules configuration
].freeze

def products
[PRODUCT]
end

def capabilities
[] # No capabilities advertised
CAPABILITIES
end

def process_config(config, content)
Expand Down
6 changes: 5 additions & 1 deletion lib/datadog/tracing/sampling/ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ module Decision
DEFAULT = '-0'
# The sampling rate received in the agent's http response.
AGENT_RATE = '-1'
# Sampling rule or sampling rate based on tracer config.
# Locally configured rule.
TRACE_SAMPLING_RULE = '-3'
# User directly sets sampling priority via {Tracing.reject!} or {Tracing.keep!},
# or by a custom sampler implementation.
Expand All @@ -49,6 +49,10 @@ module Decision
ASM = '-5'
# Single Span Sampled.
SPAN_SAMPLING_RATE = '-8'
# Dynamically configured rule, explicitly created by the user.
REMOTE_USER_RULE = '-10'
# Dynamically configured rule, automatically generated by Datadog.
REMOTE_DYNAMIC_RULE = '-11'
end
end
end
Expand Down
15 changes: 11 additions & 4 deletions lib/datadog/tracing/sampling/rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ module Sampling
# apply in case of a positive match.
# @public_api
class Rule
attr_reader :matcher, :sampler
PROVENANCE_LOCAL = :local
PROVENANCE_REMOTE_USER = :customer
PROVENANCE_REMOTE_DYNAMIC = :dynamic

attr_reader :matcher, :sampler, :provenance

# @param [Matcher] matcher A matcher to verify trace conformity against
# @param [Sampler] sampler A sampler to be consulted on a positive match
def initialize(matcher, sampler)
def initialize(matcher, sampler, provenance)
@matcher = matcher
@sampler = sampler
@provenance = provenance
end

# Evaluates if the provided `trace` conforms to the `matcher`.
Expand Down Expand Up @@ -56,7 +61,9 @@ class SimpleRule < Rule
# @param sample_rate [Float] Sampling rate between +[0,1]+
def initialize(
name: SimpleMatcher::MATCH_ALL, service: SimpleMatcher::MATCH_ALL,
resource: SimpleMatcher::MATCH_ALL, tags: {}, sample_rate: 1.0
resource: SimpleMatcher::MATCH_ALL, tags: {},
provenance: Rule::PROVENANCE_LOCAL,
sample_rate: 1.0
)
# We want to allow 0.0 to drop all traces, but {Datadog::Tracing::Sampling::RateSampler}
# considers 0.0 an invalid rate and falls back to 100% sampling.
Expand All @@ -69,7 +76,7 @@ def initialize(
sampler = RateSampler.new
sampler.sample_rate = sample_rate

super(SimpleMatcher.new(name: name, service: service, resource: resource, tags: tags), sampler)
super(SimpleMatcher.new(name: name, service: service, resource: resource, tags: tags), sampler, provenance)
end
end
end
Expand Down
18 changes: 17 additions & 1 deletion lib/datadog/tracing/sampling/rule_sampler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ def self.parse(rules, rate_limit, default_sample_rate)
resource: rule['resource'],
tags: rule['tags'],
sample_rate: sample_rate,
provenance: if (provenance = rule['provenance'])
# `Rule::PROVENANCE_*` values are symbols, so convert strings to match
provenance.to_sym
else
Rule::PROVENANCE_LOCAL
end,
}

Core::BackportFrom24.hash_compact!(kwargs)
Expand Down Expand Up @@ -127,7 +133,17 @@ def sample_trace(trace)
rate_limiter.allow?(1).tap do |allowed|
set_priority(trace, allowed)
set_limiter_metrics(trace, rate_limiter.effective_rate)
trace.set_tag(Tracing::Metadata::Ext::Distributed::TAG_DECISION_MAKER, Ext::Decision::TRACE_SAMPLING_RULE)

provenance = case rule.provenance
when Rule::PROVENANCE_REMOTE_USER
Ext::Decision::REMOTE_USER_RULE
when Rule::PROVENANCE_REMOTE_DYNAMIC
Ext::Decision::REMOTE_DYNAMIC_RULE
else
Ext::Decision::TRACE_SAMPLING_RULE
end

trace.set_tag(Tracing::Metadata::Ext::Distributed::TAG_DECISION_MAKER, provenance)
end
rescue StandardError => e
Datadog.logger.error(
Expand Down
4 changes: 3 additions & 1 deletion sig/datadog/core/configuration/options.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@ module Datadog
module InstanceMethods
def options: () -> untyped

def set_option: (untyped name, untyped value) -> untyped
def set_option: (untyped name, untyped value, precedence: Option::Precedence::Value) -> untyped

def get_option: (untyped name) -> untyped

def reset_option: (untyped name) -> untyped

def option_defined?: (untyped name) -> untyped

def unset_option: (untyped name, precedence: Option::Precedence::Value) -> void

def using_default?: (Symbol option) -> bool

def options_hash: () -> untyped
Expand Down
8 changes: 7 additions & 1 deletion sig/datadog/tracing/configuration/dynamic.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ module Datadog
def initialize: () -> void
def call: (untyped tracing_sampling_rate) -> untyped

def configuration_object: () -> untyped
def configuration_object: () -> Core::Configuration::Options::InstanceMethods
end

class TracingSamplingRules < SimpleOption
def initialize: () -> void
def call: (Array[Hash[String, untyped]] tracing_sampling_rules) -> void
def configuration_object: () -> Core::Configuration::Options::InstanceMethods
end

# Correct type is `OPTIONS: Array[[String, String, Option]]`
Expand Down
2 changes: 1 addition & 1 deletion sig/datadog/tracing/configuration/dynamic/option.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ module Datadog

def initialize: (String name, String env_var, Symbol setting_key) -> void
def call: (Object value) -> void
def configuration_object: () -> untyped
def configuration_object: () -> Core::Configuration::Options::InstanceMethods
end
end
end
Expand Down
1 change: 1 addition & 0 deletions sig/datadog/tracing/remote.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Datadog
end

PRODUCT: "APM_TRACING"
CAPABILITIES: Array[Integer]

def self.products: () -> ::Array[String]

Expand Down
2 changes: 2 additions & 0 deletions sig/datadog/tracing/sampling/ext.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ module Datadog
module Decision
DEFAULT: ::String
AGENT_RATE: ::String
REMOTE_DYNAMIC_RULE: ::String
REMOTE_USER_RULE: ::String
TRACE_SAMPLING_RULE: ::String
MANUAL: ::String
ASM: ::String
Expand Down
2 changes: 2 additions & 0 deletions sig/datadog/tracing/sampling/rule_sampler.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ module Datadog
def sample!: (untyped trace) -> untyped
def update: (*untyped args, **untyped kwargs) -> (false | untyped)

def self.parse: (Array[Hash[Symbol, untyped]] rules, Float rate_limit, Float default_sample_rate) -> RuleSampler?

private

def sample_trace: (untyped trace) { (untyped) -> untyped } -> untyped
Expand Down
10 changes: 5 additions & 5 deletions spec/datadog/core/remote/client/capabilities_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
end

describe '#base64_capabilities' do
it 'returns an empty string' do
expect(capabilities.base64_capabilities).to eq('')
it 'matches tracing capabilities only' do
expect(capabilities.base64_capabilities).to eq('IAAAAA==')
end
end
end
Expand All @@ -51,8 +51,8 @@
end

describe '#base64_capabilities' do
it 'returns an empty string' do
expect(capabilities.base64_capabilities).to eq('')
it 'matches tracing capabilities only' do
expect(capabilities.base64_capabilities).to eq('IAAAAA==')
end
end
end
Expand Down Expand Up @@ -84,7 +84,7 @@

context 'Tracing component' do
it 'register capabilities, products, and receivers' do
expect(capabilities.capabilities).to be_empty # Tracing does advertise capabilities today
expect(capabilities.capabilities).to eq([1 << 29])
expect(capabilities.products).to include('APM_TRACING')
expect(capabilities.receivers).to include(
lambda { |r|
Expand Down
33 changes: 33 additions & 0 deletions spec/datadog/tracing/configuration/dynamic_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,36 @@
option.call(new_value)
end
end

RSpec.describe Datadog::Tracing::Configuration::Dynamic::TracingSamplingRules do
let(:old_value) { nil }

include_examples 'tracing dynamic simple option',
name: 'tracing_sampling_rules',
env_var: 'DD_TRACE_SAMPLING_RULES',
config_key: :rules,
value: RSpec::Matchers::BuiltIn::Match.new(->(rules) { rules == '[{"sample_rate":1}]' }),
config_object: Datadog.configuration.tracing.sampling do
let(:new_value) { [{ sample_rate: 1 }] }
end

context 'with tags' do
include_examples 'tracing dynamic simple option',
name: 'tracing_sampling_rules',
env_var: 'DD_TRACE_SAMPLING_RULES',
config_key: :rules,
value: RSpec::Matchers::BuiltIn::Match.new(
lambda do |rules|
rules == '[{"sample_rate":1,"tags":[{"key":"k","value_glob":"v"}]}]'
end
),
config_object: Datadog.configuration.tracing.sampling do
let(:new_value) { [{ sample_rate: 1, tags: [{ key: 'k', value_glob: 'v' }] }] }
end
end

it 'reconfigures the live sampler' do
expect(Datadog.send(:components)).to receive(:reconfigure_live_sampler)
option.call(new_value)
end
end
10 changes: 6 additions & 4 deletions spec/datadog/tracing/remote_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
expect(remote.products).to contain_exactly('APM_TRACING')
end

it 'declares no capabilities' do
expect(remote.capabilities).to be_empty
it 'declares rule sampling capabilities' do
expect(remote.capabilities).to eq([1 << 29])
end

it 'declares matches that match APM_TRACING' do
Expand Down Expand Up @@ -46,7 +46,8 @@
.with(contain_exactly(
['DD_LOGS_INJECTION', nil],
['DD_TRACE_HEADER_TAGS', nil],
['DD_TRACE_SAMPLE_RATE', nil]
['DD_TRACE_SAMPLE_RATE', nil],
['DD_TRACE_SAMPLING_RULES', nil],
))

process_config
Expand All @@ -64,7 +65,8 @@
.with(contain_exactly(
['DD_LOGS_INJECTION', false],
['DD_TRACE_HEADER_TAGS', nil],
['DD_TRACE_SAMPLE_RATE', nil]
['DD_TRACE_SAMPLE_RATE', nil],
['DD_TRACE_SAMPLING_RULES', nil],
))

process_config
Expand Down
Loading
Loading