Skip to content

Commit

Permalink
Add sampling rule glob pattern support
Browse files Browse the repository at this point in the history
Signed-off-by: Marco Costa <[email protected]>
  • Loading branch information
marcotc committed Apr 30, 2024
1 parent 370d7b1 commit eee7819
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 228 deletions.
4 changes: 2 additions & 2 deletions lib/datadog/tracing/sampling/ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ module Decision
# Single Span Sampled.
SPAN_SAMPLING_RATE = '-8'
# Dynamically configured rule, explicitly created by the user.
REMOTE_USER_RULE = '-10'
REMOTE_USER_RULE = '-11'
# Dynamically configured rule, automatically generated by Datadog.
REMOTE_DYNAMIC_RULE = '-11'
REMOTE_DYNAMIC_RULE = '-12'
end
end
end
Expand Down
91 changes: 60 additions & 31 deletions lib/datadog/tracing/sampling/matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,59 @@ module Sampling
# Checks if a trace conforms to a matching criteria.
# @abstract
class Matcher
# Pattern that matches any string
MATCH_ALL_PATTERN = '*'

# Returns `true` if the trace should conforms to this rule, `false` otherwise
#
# @param [TraceOperation] trace
# @return [Boolean]
def match?(trace)
raise NotImplementedError
end
end

# A {Datadog::Sampling::Matcher} that supports matching a trace by
# trace name and/or service name.
class SimpleMatcher < Matcher
# Returns `true` for case equality (===) with any object
# Converts a glob pattern String to a case-insensitive String matcher object.
# The match object will only return `true` if it matches the complete String.
#
# The following special characters are supported:
# - `?` matches any single character
# - `*` matches any substring
#
# @param glob [String]
# @return [#match?(String)]
def self.glob_to_regex(glob)
# Optimization for match-all case
return MATCH_ALL if glob == MATCH_ALL_PATTERN

# Ensure no undesired characters are treated as regex.
glob = Regexp.quote(glob)

# Our valid special characters, `?` and `*`, were just escaped
# by `Regexp.quote` above. We need to unescape them:
glob.gsub!('\?', '.') # Any single character
glob.gsub!('\*', '.*') # Any substring

# Patterns have to match the whole input string
glob = "\\A#{glob}\\z"

Regexp.new(glob, Regexp::IGNORECASE)
end

# Returns `true` for any input
MATCH_ALL = Class.new do
# DEV: A class that implements `#===` is ~20% faster than
# DEV: a `Proc` that always returns `true`.
def ===(other)
def match?(_other)
true
end

def inspect
"MATCH_ALL:Matcher('*')"
end
end.new
end

# A {Datadog::Sampling::Matcher} that supports matching a trace by
# trace name and/or service name.
class SimpleMatcher < Matcher
attr_reader :name, :service, :resource, :tags

# @param name [String,Regexp,Proc] Matcher for case equality (===) with the trace name,
Expand All @@ -35,16 +67,30 @@ def ===(other)
# defaults to always match
# @param resource [String,Regexp,Proc] Matcher for case equality (===) with the resource name,
# defaults to always match
def initialize(name: MATCH_ALL, service: MATCH_ALL, resource: MATCH_ALL, tags: {})
def initialize(
name: MATCH_ALL_PATTERN,
service: MATCH_ALL_PATTERN,
resource: MATCH_ALL_PATTERN,
tags: {}
)
super()
@name = name
@service = service
@resource = resource

name = Matcher.glob_to_regex(name)
service = Matcher.glob_to_regex(service)
resource = Matcher.glob_to_regex(resource)
tags = tags.transform_values { |matcher| Matcher.glob_to_regex(matcher) }

@name = name || Datadog::Tracing::Sampling::Matcher::MATCH_ALL
@service = service || Datadog::Tracing::Sampling::Matcher::MATCH_ALL
@resource = resource || Datadog::Tracing::Sampling::Matcher::MATCH_ALL
@tags = tags
end

def match?(trace)
name === trace.name && service === trace.service && resource === trace.resource && tags_match?(trace)
@name.match?(trace.name) &&
@service.match?(trace.service) &&
@resource.match?(trace.resource) &&
tags_match?(trace)
end

private
Expand All @@ -59,27 +105,10 @@ def tags_match?(trace)
# can affect exact string matching (e.g. '400' matching '400.0').
tag = format('%g', tag) if tag.is_a?(Numeric)

matcher === tag
matcher.match?(tag)
end
end
end

# A {Datadog::Tracing::Sampling::Matcher} that allows for arbitrary trace matching
# based on the return value of a provided block.
class ProcMatcher < Matcher
attr_reader :block

# @yield [name, service] Provides trace name and service to the block
# @yieldreturn [Boolean] Whether the trace conforms to this matcher
def initialize(&block)
super()
@block = block
end

def match?(trace)
block.call(trace.name, trace.service)
end
end
end
end
end
4 changes: 2 additions & 2 deletions lib/datadog/tracing/sampling/rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ class SimpleRule < Rule
# defaults to always match
# @param sample_rate [Float] Sampling rate between +[0,1]+
def initialize(
name: SimpleMatcher::MATCH_ALL, service: SimpleMatcher::MATCH_ALL,
resource: SimpleMatcher::MATCH_ALL, tags: {},
name: SimpleMatcher::MATCH_ALL_PATTERN, service: SimpleMatcher::MATCH_ALL_PATTERN,
resource: SimpleMatcher::MATCH_ALL_PATTERN, tags: {},
provenance: Rule::PROVENANCE_LOCAL,
sample_rate: 1.0
)
Expand Down
8 changes: 7 additions & 1 deletion lib/datadog/tracing/sampling/rule_sampler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,13 @@ def update(*args, **kwargs)
private

def sample_trace(trace)
rule = @rules.find { |r| r.match?(trace) }
puts "sample_trace: #{caller}"
rule = @rules.find do |r|
Datadog.logger.debug do
"Matching #{r.inspect}\n against #{trace.inspect}:\n#{r.match?(trace)}"
end
r.match?(trace)
end

return yield(trace) if rule.nil?

Expand Down
54 changes: 13 additions & 41 deletions lib/datadog/tracing/sampling/span/matcher.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require_relative '../matcher'

module Datadog
module Tracing
module Sampling
Expand Down Expand Up @@ -31,29 +33,19 @@ class Matcher
# @param name_pattern [String] a pattern to be matched against {SpanOperation#name}
# @param service_pattern [String] a pattern to be matched against {SpanOperation#service}
def initialize(name_pattern: MATCH_ALL_PATTERN, service_pattern: MATCH_ALL_PATTERN)
@name = pattern_to_regex(name_pattern)
@service = pattern_to_regex(service_pattern)
@name = Sampling::Matcher.glob_to_regex(name_pattern)
@service = Sampling::Matcher.glob_to_regex(service_pattern)
end

# {Regexp#match?} was added in Ruby 2.4, and it's measurably
# the least costly way to get a boolean result for a Regexp match.
# @see https://www.ruby-lang.org/en/news/2016/12/25/ruby-2-4-0-released/
if Regexp.method_defined?(:match?)
# Returns `true` if the span conforms to the configured patterns,
# `false` otherwise
#
# @param [SpanOperation] span
# @return [Boolean]
def match?(span)
# Matching is performed at the end of the lifecycle of a Span,
# thus both `name` and `service` are guaranteed to be not `nil`.
@name.match?(span.name) && @service.match?(span.service)
end
else
# DEV: Remove when support for Ruby 2.3 and older is removed.
def match?(span)
@name === span.name && @service === span.service
end
# Returns `true` if the span conforms to the configured patterns,
# `false` otherwise
#
# @param [SpanOperation] span
# @return [Boolean]
def match?(span)
# Matching is performed at the end of the lifecycle of a Span,
# thus both `name` and `service` are guaranteed to be not `nil`.
@name.match?(span.name) && @service.match?(span.service)
end

def ==(other)
Expand All @@ -62,26 +54,6 @@ def ==(other)
name == other.name &&
service == other.service
end

private

# @param pattern [String]
# @return [Regexp]
def pattern_to_regex(pattern)
# Ensure no undesired characters are treated as regex.
# Our valid special characters, `?` and `*`,
# will be escaped so...
pattern = Regexp.quote(pattern)

# ...we account for that here:
pattern.gsub!('\?', '.') # Any single character
pattern.gsub!('\*', '.*') # Any substring

# Patterns have to match the whole input string
pattern = "\\A#{pattern}\\z"

Regexp.new(pattern)
end
end
end
end
Expand Down
24 changes: 14 additions & 10 deletions sig/datadog/tracing/sampling/matcher.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,25 @@ module Datadog
module Tracing
module Sampling
class Matcher
def match?: (untyped trace) -> untyped
end
class SimpleMatcher < Matcher
MATCH_ALL: untyped
MATCH_ALL_PATTERN: String

attr_reader name: untyped
interface _Matcher
def match?: (::String) -> bool
end

attr_reader service: untyped
def initialize: (?name: untyped, ?service: untyped) -> void
MATCH_ALL: _Matcher

def self.glob_to_regex: (::String) -> _Matcher

def match?: (untyped trace) -> untyped
end
class ProcMatcher < Matcher
attr_reader block: untyped
def initialize: () ?{ () -> untyped } -> void
class SimpleMatcher < Matcher
attr_reader name: Matcher::_Matcher
attr_reader service: Matcher::_Matcher
attr_reader resource: Matcher::_Matcher
attr_reader tags: Matcher::_Matcher

def initialize: (?name: untyped, ?service: untyped) -> void

def match?: (untyped trace) -> untyped
end
Expand Down
Loading

0 comments on commit eee7819

Please sign in to comment.