Skip to content

Commit

Permalink
Merge branch 'master' into 523-update-changelog
Browse files Browse the repository at this point in the history
  • Loading branch information
mitchh456 authored Jan 14, 2025
2 parents 83f0160 + b01c43f commit 339dd2a
Show file tree
Hide file tree
Showing 7 changed files with 359 additions and 2 deletions.
1 change: 1 addition & 0 deletions lib/scout_apm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ module ScoutApm
require 'scout_apm/app_server_load'

require 'scout_apm/ignored_uris.rb'
require 'scout_apm/sampling.rb'
require 'scout_apm/utils/active_record_metric_name'
require 'scout_apm/utils/backtrace_parser'
require 'scout_apm/utils/installed_gems'
Expand Down
4 changes: 4 additions & 0 deletions lib/scout_apm/agent_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ def logger
@logger ||= LoggerFactory.build(config, environment)
end

def sampling
@sampling ||= ScoutApm::Sampling.new(config)
end

def ignored_uris
@ignored_uris ||= ScoutApm::IgnoredUris.new(config.value('ignore'))
end
Expand Down
10 changes: 9 additions & 1 deletion lib/scout_apm/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@
# instruments listed in this array. Default: []
# ignore_endpoints - An array of endpoints to ignore. These are matched as regular expressions. (supercedes 'ignore')
# ignore_jobs - An array of job names to ignore.
# sample_rate - An integer between 0 and 100. 0 means no traces are sent, 100 means all traces are sent.
# sample_rate - Rate to sample entire application. An integer between 0 and 100. 0 means no traces are sent, 100 means all traces are sent.
# sample_endpoints - An array of endpoints to sample. These are matched as regular expressions with individual sample rate of 0 to 100.
# sample_jobs - An array of job names with individual sample rate of 0 to 100.
# endpoint_sample_rate - Rate to sample all endpoints. An integer between 0 and 100. 0 means no traces are sent, 100 means all traces are sent. (supercedes 'sample_rate')
# job_sample_rate - Rate to sample all jobs. An integer between 0 and 100. 0 means no traces are sent, 100 means all traces are sent. (supercedes 'sample_rate')
#
# Any of these config settings can be set with an environment variable prefixed
# by SCOUT_ and uppercasing the key: SCOUT_LOG_LEVEL for instance.
Expand Down Expand Up @@ -93,6 +95,8 @@ class Config
'sample_rate',
'sample_endpoints',
'sample_jobs',
'endpoint_sample_rate',
'job_sample_rate',
'scm_subdirectory',
'start_resque_server_instrument',
'ssl_cert_file',
Expand Down Expand Up @@ -209,6 +213,8 @@ def coerce(val)
'sample_rate' => IntegerCoercion.new,
'sample_endpoints' => JsonCoercion.new,
'sample_jobs' => JsonCoercion.new,
'endpoint_sample_rate' => IntegerCoercion.new,
'job_sample_rate' => IntegerCoercion.new,
'start_resque_server_instrument' => BooleanCoercion.new,
'timeline_traces' => BooleanCoercion.new,
'auto_instruments' => BooleanCoercion.new,
Expand Down Expand Up @@ -331,6 +337,8 @@ class ConfigDefaults
'sample_rate' => 100,
'sample_endpoints' => [],
'sample_jobs' => [],
'endpoint_sample_rate' => 100,
'job_sample_rate' => 100,
'start_resque_server_instrument' => true, # still only starts if Resque is detected
'collect_remote_ip' => true,
'record_queue_time' => true,
Expand Down
96 changes: 96 additions & 0 deletions lib/scout_apm/sampling.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
module ScoutApm
class Sampling
attr_reader :global_sample_rate, :sample_endpoints, :sample_uri_regex, :sample_jobs, :ignore_uri_regex, :ignore_jobs

def initialize(config)
@global_sample_rate = config.value('sample_rate')
# web endpoints matched prefix by regex
# jobs matched explicitly by name

# for now still support old config key ('ignore') for backwards compatibility
@ignore_endpoints = config.value('ignore').present? ? config.value('ignore') : config.value('ignore_endpoints')
@sample_endpoints = individual_sample_to_hash(config.value('sample_endpoints'))
@endpoint_sample_rate = config.value('endpoint_sample_rate')

@ignore_jobs = config.value('ignore_jobs')
@sample_jobs = individual_sample_to_hash(config.value('sample_jobs'))
@job_sample_rate = config.value('job_sample_rate')

logger.info("Sampling initialized with config: global_sample_rate: #{@global_sample_rate}, endpoint_sample_rate: #{@endpoint_sample_rate}, sample_endpoints: #{@sample_endpoints}, ignore_endpoints: #{@ignore_endpoints}, job_sample_rate: #@job_sample_rate}, sample_jobs: #{@sample_jobs}, ignore_jobs: #{@ignore_jobs}")
end

def drop_request?(transaction)
# Individual endpoint/job sampling takes precedence over ignoring.
# Individual endpoint/job sample rate always takes precedence over general endpoint/job rate.
# General endpoint/job rate always takes precedence over global sample rate
if transaction.job?
job_name = transaction.layer_finder.job.name
rate = job_sample_rate(job_name)
return sample?(rate) unless rate.nil?
return true if ignore_job?(job_name)
return sample?(@job_sample_rate) unless @job_sample_rate.nil?
elsif transaction.web?
uri = transaction.annotations[:uri]
rate = web_sample_rate(uri)
return sample?(rate) unless rate.nil?
return true if ignore_uri?(uri)
return sample?(@endpoint_sample_rate) unless @endpoint_sample_rate.nil?
end

# global sample check
if @global_sample_rate
return sample?(@global_sample_rate)
end

false # don't drop the request
end

def individual_sample_to_hash(sampling_config)
return nil if sampling_config.blank?
# config looks like ['/foo:50','/bar:100']. parse it into hash of string: integer
sample_hash = {}
sampling_config.each do |sample|
path, rate = sample.split(':')
sample_hash[path] = rate.to_i
end
sample_hash
end

def ignore_uri?(uri)
return false if @ignore_endpoints.blank?
@ignore_endpoints.each do |prefix|
return true if uri.start_with?(prefix)
end
false
end

def web_sample_rate(uri)
return nil if @sample_endpoints.blank?
@sample_endpoints.each do |prefix, rate|
return rate if uri.start_with?(prefix)
end
nil
end

def ignore_job?(job_name)
return false if @ignore_jobs.blank?
@ignore_jobs.include?(job_name)
end

def job_sample_rate(job_name)
return nil if @sample_jobs.blank?
@sample_jobs.fetch(job_name, nil)
end

def sample?(rate)
rand * 100 > rate
end

private

def logger
ScoutApm::Agent.instance.logger
end

end
end
6 changes: 5 additions & 1 deletion lib/scout_apm/tracked_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,11 @@ def record!
restore_from_dump! if @agent_context.nil?

# Bail out early if the user asked us to ignore this uri
return if @agent_context.ignored_uris.ignore?(annotations[:uri])
# return if @agent_context.ignored_uris.ignore?(annotations[:uri])
if @agent_context.sampling.drop_request?(self)
logger.debug("Dropping request due to sampling")
return
end

apply_name_override

Expand Down
29 changes: 29 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ def value(key)
@values[key]
end

def values
@values
end

def has_key?(key)
@values.has_key?(key)
end
Expand Down Expand Up @@ -169,3 +173,28 @@ def assert_false(thing)
class Minitest::Test
include CustomAsserts
end

class FakeTrackedRequest
def self.new_web_request(uri)
context = ScoutApm::Agent.instance.context
fake_store = ScoutApm::FakeStore.new
req = ScoutApm::TrackedRequest.new(context, fake_store)

first_layer = ScoutApm::Layer.new("Controller", "index")
req.start_layer(first_layer)
req.annotate_request(:uri => uri)

req
end

def self.new_job_request(job_name)
context = ScoutApm::Agent.instance.context
fake_store = ScoutApm::FakeStore.new
req = ScoutApm::TrackedRequest.new(context, fake_store)

first_layer = ScoutApm::Layer.new("Job", job_name)
req.start_layer(first_layer)

req
end
end
Loading

0 comments on commit 339dd2a

Please sign in to comment.