aws-xray-sdk 0.10.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/lib/aws-xray-sdk/configuration.rb +10 -7
  3. data/lib/aws-xray-sdk/daemon_config.rb +59 -0
  4. data/lib/aws-xray-sdk/emitter/default_emitter.rb +10 -21
  5. data/lib/aws-xray-sdk/emitter/emitter.rb +1 -3
  6. data/lib/aws-xray-sdk/facets/aws_sdk.rb +14 -6
  7. data/lib/aws-xray-sdk/facets/helper.rb +4 -11
  8. data/lib/aws-xray-sdk/facets/net_http.rb +4 -0
  9. data/lib/aws-xray-sdk/facets/rack.rb +8 -6
  10. data/lib/aws-xray-sdk/facets/rails/railtie.rb +1 -1
  11. data/lib/aws-xray-sdk/model/cause.rb +2 -2
  12. data/lib/aws-xray-sdk/model/dummy_entities.rb +4 -0
  13. data/lib/aws-xray-sdk/model/entity.rb +2 -2
  14. data/lib/aws-xray-sdk/model/metadata.rb +2 -2
  15. data/lib/aws-xray-sdk/model/segment.rb +8 -1
  16. data/lib/aws-xray-sdk/plugins/elastic_beanstalk.rb +2 -2
  17. data/lib/aws-xray-sdk/recorder.rb +7 -4
  18. data/lib/aws-xray-sdk/sampling/connector.rb +72 -0
  19. data/lib/aws-xray-sdk/sampling/default_sampler.rb +72 -78
  20. data/lib/aws-xray-sdk/sampling/lead_poller.rb +72 -0
  21. data/lib/aws-xray-sdk/sampling/local/reservoir.rb +35 -0
  22. data/lib/aws-xray-sdk/sampling/local/sampler.rb +110 -0
  23. data/lib/aws-xray-sdk/sampling/local/sampling_rule.rb +63 -0
  24. data/lib/aws-xray-sdk/sampling/reservoir.rb +68 -24
  25. data/lib/aws-xray-sdk/sampling/rule_cache.rb +86 -0
  26. data/lib/aws-xray-sdk/sampling/rule_poller.rb +39 -0
  27. data/lib/aws-xray-sdk/sampling/sampler.rb +3 -3
  28. data/lib/aws-xray-sdk/sampling/sampling_decision.rb +8 -0
  29. data/lib/aws-xray-sdk/sampling/sampling_rule.rb +102 -35
  30. data/lib/aws-xray-sdk/version.rb +1 -1
  31. metadata +34 -11
@@ -0,0 +1,72 @@
1
+ require 'securerandom'
2
+ require 'aws-xray-sdk/sampling/sampling_rule'
3
+ require 'aws-xray-sdk/logger'
4
+
5
+ module XRay
6
+ # Connector class that translates Sampling poller functions to
7
+ # actual X-Ray back-end APIs and communicates with X-Ray daemon
8
+ # as the signing proxy.
9
+ class ServiceConnector
10
+ include Logging
11
+ attr_accessor :xray_client
12
+ @@client_id = SecureRandom.hex(12)
13
+
14
+ def initialize
15
+ update_xray_client
16
+ end
17
+
18
+ def fetch_sampling_rules
19
+ rules = []
20
+ records = @xray_client.get_sampling_rules.sampling_rule_records
21
+ records.each { |record| rules << SamplingRule.new(record.sampling_rule) if rule_valid?(record.sampling_rule) }
22
+ rules
23
+ end
24
+
25
+ def fetch_sampling_targets(rules)
26
+ now = Time.now.to_i
27
+ reports = generate_reports(rules, now)
28
+ resp = @xray_client.get_sampling_targets({ sampling_statistics_documents: reports })
29
+ {
30
+ last_modified: resp.last_rule_modification,
31
+ documents: resp.sampling_target_documents
32
+ }
33
+ end
34
+
35
+ def update_xray_client(ip: '127.0.0.1', port: 2000)
36
+ require 'aws-sdk-xray'
37
+ @xray_client = Aws::XRay::Client.new(
38
+ endpoint: %(http://#{ip}:#{port}),
39
+ access_key_id: 'dummy', # AWS Ruby SDK doesn't support unsigned request
40
+ secret_access_key: 'dummy',
41
+ region: 'us-west-2' # not used
42
+ )
43
+ end
44
+
45
+ def daemon_config=(v)
46
+ update_xray_client ip: v.tcp_ip, port: v.tcp_port
47
+ end
48
+
49
+ private
50
+
51
+ def generate_reports(rules, now)
52
+ reports = []
53
+ rules.each do |rule|
54
+ report = rule.snapshot_statistics
55
+ report[:rule_name] = rule.name
56
+ report[:timestamp] = now
57
+ report[:client_id] = @@client_id
58
+ reports << report
59
+ end
60
+ reports
61
+ end
62
+
63
+ def rule_valid?(rule)
64
+ return false if rule.version != 1
65
+ # rules has resource ARN and attributes configured
66
+ # doesn't apply to this SDK
67
+ return false unless rule.resource_arn == '*'
68
+ return false unless rule.attributes.nil? || rule.attributes.empty?
69
+ true
70
+ end
71
+ end
72
+ end
@@ -1,105 +1,99 @@
1
+ require 'aws-xray-sdk/logger'
2
+ require 'aws-xray-sdk/sampling/local/sampler'
3
+ require 'aws-xray-sdk/sampling/lead_poller'
4
+ require 'aws-xray-sdk/sampling/rule_cache'
1
5
  require 'aws-xray-sdk/sampling/sampler'
2
6
  require 'aws-xray-sdk/sampling/sampling_rule'
3
- require 'aws-xray-sdk/exceptions'
7
+ require 'aws-xray-sdk/sampling/sampling_decision'
4
8
 
5
9
  module XRay
6
- # The default sampler that uses internally defined
7
- # sampling rule and reservoir models to decide sampling decision.
8
- # It also uses the default sampling rule.
9
- # An example definition:
10
- # {
11
- # version: 1,
12
- # rules: [
13
- # {
14
- # description: 'Player moves.',
15
- # service_name: '*',
16
- # http_method: '*',
17
- # url_path: '/api/move/*',
18
- # fixed_target: 0,
19
- # rate: 0.05
20
- # }
21
- # ],
22
- # default: {
23
- # fixed_target: 1,
24
- # rate: 0.1
25
- # }
26
- # }
27
- # This example defines one custom rule and a default rule.
28
- # The custom rule applies a five-percent sampling rate with no minimum
29
- # number of requests to trace for paths under /api/move/. The default
30
- # rule traces the first request each second and 10 percent of additional requests.
31
- # The SDK applies custom rules in the order in which they are defined.
32
- # If a request matches multiple custom rules, the SDK applies only the first rule.
10
+ # Making sampling decisions based on service sampling rules defined
11
+ # by X-Ray control plane APIs. It will fall back to local sampling rules
12
+ # if service sampling rules are not available or expired.
33
13
  class DefaultSampler
34
14
  include Sampler
35
- DEFAULT_RULES = {
36
- version: 1,
37
- default: {
38
- fixed_target: 1,
39
- rate: 0.05
40
- },
41
- rules: []
42
- }.freeze
15
+ include Logging
16
+ attr_reader :cache, :local_sampler, :poller
17
+ attr_accessor :origin
43
18
 
44
19
  def initialize
45
- load_sampling_rules(DEFAULT_RULES)
20
+ @local_sampler = LocalSampler.new
21
+ @cache = RuleCache.new
22
+ @poller = LeadPoller.new(@cache)
23
+
24
+ @started = false
25
+ @origin = nil
26
+ @lock = Mutex.new
46
27
  end
47
28
 
48
- # Return True if the sampler decide to sample based on input
49
- # information and sampling rules. It will first check if any
50
- # custom rule should be applied, if not it falls back to the
51
- # default sampling rule.
52
- # All arugments are extracted from incoming requests by
53
- # X-Ray middleware to perform path based sampling.
54
- def sample_request?(service_name:, url_path:, http_method:)
55
- # directly fallback to non-path-based if all arguments are nil
56
- return sample? unless service_name || url_path || http_method
57
- @custom_rules ||= []
58
- @custom_rules.each do |c|
59
- return should_sample?(c) if c.applies?(target_name: service_name, target_path: url_path, target_method: http_method)
29
+ # Start background threads to poll sampling rules
30
+ def start
31
+ @lock.synchronize do
32
+ unless @started
33
+ @poller.start
34
+ @started = true
35
+ end
36
+ end
37
+ end
38
+
39
+ # Return the rule name if it decides to sample based on
40
+ # a service sampling rule matching. If there is no match
41
+ # it will fallback to local defined sampling rules.
42
+ def sample_request?(sampling_req)
43
+ start unless @started
44
+ now = Time.now.to_i
45
+ if sampling_req.nil?
46
+ sampling_req = { service_type: @origin } if @origin
47
+ elsif !sampling_req.key?(:service_type)
48
+ sampling_req[:service_type] = @origin if @origin
49
+ end
50
+
51
+ matched_rule = @cache.get_matched_rule(sampling_req, now: now)
52
+ if !matched_rule.nil?
53
+ logger.debug %(Rule #{matched_rule.name} is selected to make a sampling decision.')
54
+ process_matched_rule(matched_rule, now)
55
+ else
56
+ logger.warn %(No effective centralized sampling rule match. Fallback to local rules.)
57
+ @local_sampler.sample_request?(sampling_req)
60
58
  end
61
- sample?
62
59
  end
63
60
 
64
- # Decides if should sample based on non-path-based rule.
65
- # Currently only the default rule is not path-based.
66
61
  def sample?
67
- should_sample?(@default_rule)
62
+ sample_request? nil
68
63
  end
69
64
 
70
- # @param [Hash] v The sampling rules definition.
65
+ # @param [Hash] v Local sampling rules definition.
66
+ # This configuration has lower priority than service
67
+ # sampling rules and only has effect when those rules
68
+ # are not available or expired.
71
69
  def sampling_rules=(v)
72
- load_sampling_rules(v)
70
+ @local_sampler.sampling_rules = v
73
71
  end
74
72
 
75
- # @return [Array] An array of [SamplingRule]
76
- def sampling_rules
77
- all_rules = []
78
- all_rules << @default_rule
79
- all_rules << @custom_rules unless @custom_rules.empty?
80
- all_rules
73
+ def daemon_config=(v)
74
+ @poller.connector.daemon_config = v
81
75
  end
82
76
 
83
77
  private
84
78
 
85
- def should_sample?(rule)
86
- return true if rule.reservoir.take
87
- Random.rand <= rule.rate
88
- end
89
-
90
- def load_sampling_rules(v)
91
- version = v[:version]
92
- if version != 1
93
- raise InvalidSamplingConfigError, %('Sampling rule version #{version} is not supported.')
94
- end
95
- unless v[:default]
96
- raise InvalidSamplingConfigError, 'A default rule must be provided.'
97
- end
98
- @default_rule = SamplingRule.new rule_definition: v[:default], default: true
99
- @custom_rules = []
100
- v[:rules].each do |d|
101
- @custom_rules << SamplingRule.new(rule_definition: d)
79
+ def process_matched_rule(rule, now)
80
+ # As long as a rule is matched we increment request counter.
81
+ rule.increment_request_count
82
+ reservoir = rule.reservoir
83
+ sample = true
84
+ # We check if we can borrow or take from reservoir first.
85
+ decision = reservoir.borrow_or_take(now, rule.borrowable?)
86
+ if decision == SamplingDecision::BORROW
87
+ rule.increment_borrow_count
88
+ elsif decision == SamplingDecision::TAKE
89
+ rule.increment_sampled_count
90
+ # Otherwise we compute based on fixed rate of this sampling rule.
91
+ elsif rand <= rule.rate
92
+ rule.increment_sampled_count
93
+ else
94
+ sample = false
102
95
  end
96
+ sample ? rule.name : false
103
97
  end
104
98
  end
105
99
  end
@@ -0,0 +1,72 @@
1
+ require 'aws-xray-sdk/logger'
2
+ require 'aws-xray-sdk/sampling/connector'
3
+ require 'aws-xray-sdk/sampling/rule_poller'
4
+
5
+ module XRay
6
+ # The poller to report the current statistics of all
7
+ # sampling rules and retrieve the new allocated
8
+ # sampling quota and TTL from X-Ray service. It also
9
+ # controls the rule poller.
10
+ class LeadPoller
11
+ include Logging
12
+ attr_reader :connector
13
+ @@interval = 10 # working frequency of the lead poller
14
+ @@rule_interval = 5 * 60 # 5 minutes on polling rules
15
+
16
+ def initialize(cache)
17
+ @cache = cache
18
+ @connector = ServiceConnector.new
19
+ @rule_poller = RulePoller.new cache: @cache, connector: @connector
20
+ @rule_poller_elapsed = 0
21
+ end
22
+
23
+ def start
24
+ @rule_poller.run
25
+ Thread.new { worker }
26
+ end
27
+
28
+ def worker
29
+ loop do
30
+ sleep_time = @@interval + rand
31
+ sleep sleep_time
32
+ @rule_poller_elapsed += sleep_time
33
+ refresh_cache
34
+ if @rule_poller_elapsed >= @@rule_interval
35
+ @rule_poller.run
36
+ @rule_poller_elapsed = 0
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def refresh_cache
44
+ candidates = get_candidates(@cache.rules)
45
+ if candidates.empty?
46
+ logger.debug %(No X-Ray sampling rules to report statistics. Skipping.)
47
+ return
48
+ end
49
+
50
+ result = @connector.fetch_sampling_targets(candidates)
51
+ targets = {}
52
+ result[:documents].each { |doc| targets[doc.rule_name] = doc }
53
+ @cache.load_targets(targets)
54
+
55
+ return unless @cache.last_updated && result[:last_modified].to_i > @cache.last_updated
56
+ logger.info 'Performing out-of-band sampling rule polling to fetch updated rules.'
57
+ @rule_poller.run
58
+ @rule_poller_elapsed = 0
59
+ rescue StandardError => e
60
+ logger.warn %(failed to fetch X-Ray sampling targets due to #{e.message})
61
+ end
62
+
63
+ # Don't report a rule statistics if any of the conditions is met:
64
+ # 1. The report time hasn't come(some rules might have larger report intervals).
65
+ # 2. The rule is never matched.
66
+ def get_candidates(rules)
67
+ candidates = []
68
+ rules.each { |rule| candidates << rule if rule.ever_matched? && rule.time_to_report? }
69
+ candidates
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,35 @@
1
+ module XRay
2
+ # Keeps track of the number of sampled segments within
3
+ # a single second in the local process. This class is
4
+ # implemented to be thread-safe to achieve accurate sampling.
5
+ class LocalReservoir
6
+ # @param [Integer] traces_per_sec The number of guranteed sampled
7
+ # segments per second.
8
+ def initialize(traces_per_sec: 0)
9
+ @traces_per_sec = traces_per_sec
10
+ @used_this_sec = 0
11
+ @this_sec = Time.now.to_i
12
+ @lock = Mutex.new
13
+ end
14
+
15
+ # Returns `true` if there are quota left within the
16
+ # current second, otherwise returns `false`.
17
+ def take
18
+ # nothing to provide if reserved is set to 0
19
+ return false if @traces_per_sec.zero?
20
+ @lock.synchronize do
21
+ now = Time.now.to_i
22
+ # refresh time frame
23
+ if now != @this_sec
24
+ @used_this_sec = 0
25
+ @this_sec = now
26
+ end
27
+ # return false if reserved item ran out
28
+ return false unless @used_this_sec < @traces_per_sec
29
+ # otherwise increment used counter and return true
30
+ @used_this_sec += 1
31
+ return true
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,110 @@
1
+ require 'aws-xray-sdk/sampling/sampler'
2
+ require 'aws-xray-sdk/sampling/local/sampling_rule'
3
+ require 'aws-xray-sdk/exceptions'
4
+
5
+ module XRay
6
+ # The local sampler that uses locally defined
7
+ # sampling rule and reservoir models to decide sampling decision.
8
+ # It also uses the default sampling rule.
9
+ # An example definition:
10
+ # {
11
+ # version: 2,
12
+ # rules: [
13
+ # {
14
+ # description: 'Player moves.',
15
+ # host: '*',
16
+ # http_method: '*',
17
+ # url_path: '/api/move/*',
18
+ # fixed_target: 0,
19
+ # rate: 0.05
20
+ # }
21
+ # ],
22
+ # default: {
23
+ # fixed_target: 1,
24
+ # rate: 0.1
25
+ # }
26
+ # }
27
+ # This example defines one custom rule and a default rule.
28
+ # The custom rule applies a five-percent sampling rate with no minimum
29
+ # number of requests to trace for paths under /api/move/. The default
30
+ # rule traces the first request each second and 10 percent of additional requests.
31
+ # The SDK applies custom rules in the order in which they are defined.
32
+ # If a request matches multiple custom rules, the SDK applies only the first rule.
33
+ class LocalSampler
34
+ include Sampler
35
+ DEFAULT_RULES = {
36
+ version: 2,
37
+ default: {
38
+ fixed_target: 1,
39
+ rate: 0.05
40
+ },
41
+ rules: []
42
+ }.freeze
43
+
44
+ SUPPORTED_VERSION = [1, 2].freeze
45
+
46
+ def initialize
47
+ load_sampling_rules(DEFAULT_RULES)
48
+ end
49
+
50
+ # Return True if the sampler decide to sample based on input
51
+ # information and sampling rules. It will first check if any
52
+ # custom rule should be applied, if not it falls back to the
53
+ # default sampling rule.
54
+ # All arugments are extracted from incoming requests by
55
+ # X-Ray middleware to perform path based sampling.
56
+ def sample_request?(sampling_req)
57
+ sample = sample?
58
+ return sample if sampling_req.nil? || sampling_req.empty?
59
+ @custom_rules ||= []
60
+ @custom_rules.each do |c|
61
+ return should_sample?(c) if c.applies?(sampling_req: sampling_req)
62
+ end
63
+ # use previously made decision based on default rule
64
+ # if no path-based rule has been matched
65
+ sample
66
+ end
67
+
68
+ # Decides if should sample based on non-path-based rule.
69
+ # Currently only the default rule is non-path-based.
70
+ def sample?
71
+ should_sample?(@default_rule)
72
+ end
73
+
74
+ # @param [Hash] v The sampling rules definition.
75
+ def sampling_rules=(v)
76
+ load_sampling_rules(v)
77
+ end
78
+
79
+ # @return [Array] An array of [SamplingRule]
80
+ def sampling_rules
81
+ all_rules = []
82
+ all_rules << @default_rule
83
+ all_rules << @custom_rules unless @custom_rules.empty?
84
+ all_rules
85
+ end
86
+
87
+ private
88
+
89
+ def should_sample?(rule)
90
+ return true if rule.reservoir.take
91
+ Random.rand <= rule.rate
92
+ end
93
+
94
+ def load_sampling_rules(v)
95
+ version = v[:version]
96
+ unless SUPPORTED_VERSION.include?(version)
97
+ raise InvalidSamplingConfigError, %('Sampling rule version #{version} is not supported.')
98
+ end
99
+ unless v[:default]
100
+ raise InvalidSamplingConfigError, 'A default rule must be provided.'
101
+ end
102
+ @default_rule = LocalSamplingRule.new rule_definition: v[:default], default: true
103
+ @custom_rules = []
104
+ v[:rules].each do |d|
105
+ d[:host] = d[:service_name] if version == 1
106
+ @custom_rules << LocalSamplingRule.new(rule_definition: d)
107
+ end
108
+ end
109
+ end
110
+ end