aws-xray-sdk 0.10.2 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/aws-xray-sdk/configuration.rb +10 -7
- data/lib/aws-xray-sdk/daemon_config.rb +59 -0
- data/lib/aws-xray-sdk/emitter/default_emitter.rb +10 -21
- data/lib/aws-xray-sdk/emitter/emitter.rb +1 -3
- data/lib/aws-xray-sdk/facets/aws_sdk.rb +14 -6
- data/lib/aws-xray-sdk/facets/helper.rb +4 -11
- data/lib/aws-xray-sdk/facets/net_http.rb +4 -0
- data/lib/aws-xray-sdk/facets/rack.rb +8 -6
- data/lib/aws-xray-sdk/facets/rails/railtie.rb +1 -1
- data/lib/aws-xray-sdk/model/cause.rb +2 -2
- data/lib/aws-xray-sdk/model/dummy_entities.rb +4 -0
- data/lib/aws-xray-sdk/model/entity.rb +2 -2
- data/lib/aws-xray-sdk/model/metadata.rb +2 -2
- data/lib/aws-xray-sdk/model/segment.rb +8 -1
- data/lib/aws-xray-sdk/plugins/elastic_beanstalk.rb +2 -2
- data/lib/aws-xray-sdk/recorder.rb +7 -4
- data/lib/aws-xray-sdk/sampling/connector.rb +72 -0
- data/lib/aws-xray-sdk/sampling/default_sampler.rb +72 -78
- data/lib/aws-xray-sdk/sampling/lead_poller.rb +72 -0
- data/lib/aws-xray-sdk/sampling/local/reservoir.rb +35 -0
- data/lib/aws-xray-sdk/sampling/local/sampler.rb +110 -0
- data/lib/aws-xray-sdk/sampling/local/sampling_rule.rb +63 -0
- data/lib/aws-xray-sdk/sampling/reservoir.rb +68 -24
- data/lib/aws-xray-sdk/sampling/rule_cache.rb +86 -0
- data/lib/aws-xray-sdk/sampling/rule_poller.rb +39 -0
- data/lib/aws-xray-sdk/sampling/sampler.rb +3 -3
- data/lib/aws-xray-sdk/sampling/sampling_decision.rb +8 -0
- data/lib/aws-xray-sdk/sampling/sampling_rule.rb +102 -35
- data/lib/aws-xray-sdk/version.rb +1 -1
- 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/
|
7
|
+
require 'aws-xray-sdk/sampling/sampling_decision'
|
4
8
|
|
5
9
|
module XRay
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
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
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
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
|
-
#
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
62
|
+
sample_request? nil
|
68
63
|
end
|
69
64
|
|
70
|
-
# @param [Hash] v
|
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
|
-
|
70
|
+
@local_sampler.sampling_rules = v
|
73
71
|
end
|
74
72
|
|
75
|
-
|
76
|
-
|
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
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
if
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|