jaeger-client 0.7.1 → 1.1.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.
- checksums.yaml +4 -4
- data/.gitmodules +3 -0
- data/.rubocop.yml +3 -0
- data/.travis.yml +11 -2
- data/Makefile +1 -0
- data/README.md +127 -13
- data/crossdock/Dockerfile +29 -0
- data/crossdock/Gemfile +6 -0
- data/crossdock/Gemfile.lock +35 -0
- data/crossdock/docker-compose.yml +68 -0
- data/crossdock/jaeger-docker-compose.yml +48 -0
- data/crossdock/rules.mk +35 -0
- data/crossdock/server +173 -0
- data/jaeger-client.gemspec +4 -2
- data/lib/jaeger/client.rb +42 -18
- data/lib/jaeger/client/version.rb +1 -1
- data/lib/jaeger/encoders/thrift_encoder.rb +142 -0
- data/lib/jaeger/extractors.rb +173 -0
- data/lib/jaeger/http_sender.rb +28 -0
- data/lib/jaeger/injectors.rb +83 -0
- data/lib/jaeger/rate_limiter.rb +61 -0
- data/lib/jaeger/recurring_executor.rb +35 -0
- data/lib/jaeger/reporters.rb +7 -0
- data/lib/jaeger/reporters/composite_reporter.rb +17 -0
- data/lib/jaeger/reporters/in_memory_reporter.rb +30 -0
- data/lib/jaeger/reporters/logging_reporter.rb +22 -0
- data/lib/jaeger/reporters/null_reporter.rb +11 -0
- data/lib/jaeger/{client/async_reporter.rb → reporters/remote_reporter.rb} +21 -20
- data/lib/jaeger/{client/async_reporter → reporters/remote_reporter}/buffer.rb +2 -2
- data/lib/jaeger/samplers.rb +8 -0
- data/lib/jaeger/samplers/const.rb +24 -0
- data/lib/jaeger/samplers/guaranteed_throughput_probabilistic.rb +47 -0
- data/lib/jaeger/samplers/per_operation.rb +79 -0
- data/lib/jaeger/samplers/probabilistic.rb +38 -0
- data/lib/jaeger/samplers/rate_limiting.rb +51 -0
- data/lib/jaeger/samplers/remote_controlled.rb +119 -0
- data/lib/jaeger/samplers/remote_controlled/instructions_fetcher.rb +34 -0
- data/lib/jaeger/scope.rb +38 -0
- data/lib/jaeger/scope_manager.rb +47 -0
- data/lib/jaeger/scope_manager/scope_identifier.rb +13 -0
- data/lib/jaeger/scope_manager/scope_stack.rb +33 -0
- data/lib/jaeger/span.rb +97 -0
- data/lib/jaeger/span/thrift_log_builder.rb +18 -0
- data/lib/jaeger/span_context.rb +57 -0
- data/lib/jaeger/thrift_tag_builder.rb +41 -0
- data/lib/jaeger/trace_id.rb +48 -0
- data/lib/jaeger/tracer.rb +213 -0
- data/lib/jaeger/udp_sender.rb +26 -0
- data/lib/jaeger/udp_sender/transport.rb +40 -0
- metadata +78 -31
- data/lib/jaeger/client/carrier.rb +0 -26
- data/lib/jaeger/client/encoders/thrift_encoder.rb +0 -94
- data/lib/jaeger/client/extractors.rb +0 -88
- data/lib/jaeger/client/http_sender.rb +0 -30
- data/lib/jaeger/client/injectors.rb +0 -54
- data/lib/jaeger/client/samplers.rb +0 -4
- data/lib/jaeger/client/samplers/const.rb +0 -29
- data/lib/jaeger/client/samplers/probabilistic.rb +0 -30
- data/lib/jaeger/client/scope.rb +0 -40
- data/lib/jaeger/client/scope_manager.rb +0 -49
- data/lib/jaeger/client/scope_manager/scope_identifier.rb +0 -15
- data/lib/jaeger/client/scope_manager/scope_stack.rb +0 -35
- data/lib/jaeger/client/span.rb +0 -84
- data/lib/jaeger/client/span/thrift_log_builder.rb +0 -20
- data/lib/jaeger/client/span/thrift_tag_builder.rb +0 -45
- data/lib/jaeger/client/span_context.rb +0 -59
- data/lib/jaeger/client/trace_id.rb +0 -41
- data/lib/jaeger/client/tracer.rb +0 -189
- data/lib/jaeger/client/udp_sender.rb +0 -27
- data/lib/jaeger/client/udp_sender/transport.rb +0 -42
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jaeger
|
4
|
+
module Samplers
|
5
|
+
# A sampler that leverages both Probabilistic sampler and RateLimiting
|
6
|
+
# sampler via the GuaranteedThroughputProbabilistic sampler. This sampler
|
7
|
+
# keeps track of all operations and delegates calls the the respective
|
8
|
+
# GuaranteedThroughputProbabilistic sampler.
|
9
|
+
class PerOperation
|
10
|
+
DEFAULT_SAMPLING_PROBABILITY = 0.001
|
11
|
+
DEFAULT_LOWER_BOUND = 1.0 / (10.0 * 60.0) # sample once every 10 minutes'
|
12
|
+
|
13
|
+
attr_reader :default_sampling_probability, :lower_bound, :samplers
|
14
|
+
|
15
|
+
def initialize(strategies:, max_operations:)
|
16
|
+
@max_operations = max_operations
|
17
|
+
@samplers = {}
|
18
|
+
update(strategies: strategies)
|
19
|
+
end
|
20
|
+
|
21
|
+
def update(strategies:)
|
22
|
+
is_updated = false
|
23
|
+
|
24
|
+
@default_sampling_probability =
|
25
|
+
strategies[:default_sampling_probability] || DEFAULT_SAMPLING_PROBABILITY
|
26
|
+
@lower_bound =
|
27
|
+
strategies[:default_lower_bound_traces_per_second] || DEFAULT_LOWER_BOUND
|
28
|
+
|
29
|
+
if @default_sampler
|
30
|
+
is_updated = @default_sampler.update(rate: @default_sampling_probability)
|
31
|
+
else
|
32
|
+
@default_sampler = Probabilistic.new(rate: @default_sampling_probability)
|
33
|
+
end
|
34
|
+
|
35
|
+
is_updated = update_operation_strategies(strategies) || is_updated
|
36
|
+
|
37
|
+
is_updated
|
38
|
+
end
|
39
|
+
|
40
|
+
def sample(opts)
|
41
|
+
operation_name = opts.fetch(:operation_name)
|
42
|
+
sampler = @samplers[operation_name]
|
43
|
+
return sampler.sample(opts) if sampler
|
44
|
+
|
45
|
+
return @default_sampler.sample(opts) if @samplers.length >= @max_operations
|
46
|
+
|
47
|
+
sampler = GuaranteedThroughputProbabilistic.new(
|
48
|
+
lower_bound: @lower_bound,
|
49
|
+
rate: @default_sampling_probability
|
50
|
+
)
|
51
|
+
@samplers[operation_name] = sampler
|
52
|
+
sampler.sample(opts)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def update_operation_strategies(strategies)
|
58
|
+
is_updated = false
|
59
|
+
|
60
|
+
(strategies[:per_operation_strategies] || []).each do |strategy|
|
61
|
+
operation = strategy.fetch(:operation)
|
62
|
+
rate = strategy.fetch(:probabilistic_sampling).fetch(:sampling_rate)
|
63
|
+
|
64
|
+
if (sampler = @samplers[operation])
|
65
|
+
is_updated = sampler.update(lower_bound: @lower_bound, rate: rate) || is_updated
|
66
|
+
else
|
67
|
+
@samplers[operation] = GuaranteedThroughputProbabilistic.new(
|
68
|
+
lower_bound: @lower_bound,
|
69
|
+
rate: rate
|
70
|
+
)
|
71
|
+
is_updated = true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
is_updated
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jaeger
|
4
|
+
module Samplers
|
5
|
+
# Probabilistic sampler
|
6
|
+
#
|
7
|
+
# Sample a portion of traces using trace_id as the random decision
|
8
|
+
class Probabilistic
|
9
|
+
attr_reader :rate
|
10
|
+
|
11
|
+
def initialize(rate: 0.001)
|
12
|
+
update(rate: rate)
|
13
|
+
end
|
14
|
+
|
15
|
+
def update(rate:)
|
16
|
+
if rate < 0.0 || rate > 1.0
|
17
|
+
raise "Sampling rate must be between 0.0 and 1.0, got #{rate.inspect}"
|
18
|
+
end
|
19
|
+
|
20
|
+
new_boundary = TraceId::TRACE_ID_UPPER_BOUND * rate
|
21
|
+
return false if @boundary == new_boundary
|
22
|
+
|
23
|
+
@rate = rate
|
24
|
+
@boundary = TraceId::TRACE_ID_UPPER_BOUND * rate
|
25
|
+
@tags = {
|
26
|
+
'sampler.type' => 'probabilistic',
|
27
|
+
'sampler.param' => rate
|
28
|
+
}
|
29
|
+
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
def sample(trace_id:, **)
|
34
|
+
[@boundary >= trace_id, @tags]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jaeger
|
4
|
+
module Samplers
|
5
|
+
# Samples at most max_traces_per_second. The distribution of sampled
|
6
|
+
# traces follows burstiness of the service, i.e. a service with uniformly
|
7
|
+
# distributed requests will have those requests sampled uniformly as
|
8
|
+
# well, but if requests are bursty, especially sub-second, then a number
|
9
|
+
# of sequential requests can be sampled each second.
|
10
|
+
class RateLimiting
|
11
|
+
attr_reader :tags, :max_traces_per_second
|
12
|
+
|
13
|
+
def initialize(max_traces_per_second: 10)
|
14
|
+
update(max_traces_per_second: max_traces_per_second)
|
15
|
+
end
|
16
|
+
|
17
|
+
def update(max_traces_per_second:)
|
18
|
+
if max_traces_per_second < 0.0
|
19
|
+
raise "max_traces_per_second must not be negative, got #{max_traces_per_second}"
|
20
|
+
end
|
21
|
+
|
22
|
+
return false if max_traces_per_second == @max_traces_per_second
|
23
|
+
|
24
|
+
@tags = {
|
25
|
+
'sampler.type' => 'ratelimiting',
|
26
|
+
'sampler.param' => max_traces_per_second
|
27
|
+
}
|
28
|
+
@max_traces_per_second = max_traces_per_second
|
29
|
+
max_balance = [max_traces_per_second, 1.0].max
|
30
|
+
|
31
|
+
if @rate_limiter
|
32
|
+
@rate_limiter.update(
|
33
|
+
credits_per_second: max_traces_per_second,
|
34
|
+
max_balance: max_balance
|
35
|
+
)
|
36
|
+
else
|
37
|
+
@rate_limiter = RateLimiter.new(
|
38
|
+
credits_per_second: max_traces_per_second,
|
39
|
+
max_balance: [max_traces_per_second, 1.0].max
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def sample(*)
|
47
|
+
[@rate_limiter.check_credit(1.0), @tags]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'remote_controlled/instructions_fetcher'
|
4
|
+
|
5
|
+
module Jaeger
|
6
|
+
module Samplers
|
7
|
+
class RemoteControlled
|
8
|
+
DEFAULT_REFRESH_INTERVAL = 60
|
9
|
+
DEFAULT_SAMPLING_HOST = 'localhost'.freeze
|
10
|
+
DEFAULT_SAMPLING_PORT = 5778
|
11
|
+
|
12
|
+
attr_reader :sampler
|
13
|
+
|
14
|
+
def initialize(opts = {})
|
15
|
+
@sampler = opts.fetch(:sampler, Probabilistic.new)
|
16
|
+
@logger = opts.fetch(:logger, Logger.new($stdout))
|
17
|
+
|
18
|
+
@poll_executor = opts[:poll_executor] || begin
|
19
|
+
refresh_interval = opts.fetch(:refresh_interval, DEFAULT_REFRESH_INTERVAL)
|
20
|
+
RecurringExecutor.new(interval: refresh_interval)
|
21
|
+
end
|
22
|
+
|
23
|
+
@instructions_fetcher = opts[:instructions_fetcher] || begin
|
24
|
+
service_name = opts.fetch(:service_name)
|
25
|
+
host = opts.fetch(:host, DEFAULT_SAMPLING_HOST)
|
26
|
+
port = opts.fetch(:port, DEFAULT_SAMPLING_PORT)
|
27
|
+
InstructionsFetcher.new(host: host, port: port, service_name: service_name)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def sample(*args)
|
32
|
+
@poll_executor.start(&method(:poll)) unless @poll_executor.running?
|
33
|
+
|
34
|
+
@sampler.sample(*args)
|
35
|
+
end
|
36
|
+
|
37
|
+
def poll
|
38
|
+
@logger.debug 'Fetching sampling strategy'
|
39
|
+
|
40
|
+
instructions = @instructions_fetcher.fetch
|
41
|
+
handle_instructions(instructions)
|
42
|
+
rescue InstructionsFetcher::FetchFailed => e
|
43
|
+
@logger.warn "Fetching sampling strategy failed: #{e.message}"
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def handle_instructions(instructions)
|
49
|
+
if instructions['operationSampling']
|
50
|
+
update_per_operation_sampler(instructions['operationSampling'])
|
51
|
+
else
|
52
|
+
update_rate_limiting_or_probabilistic_sampler(instructions['strategyType'], instructions)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def update_per_operation_sampler(instructions)
|
57
|
+
strategies = normalize(instructions)
|
58
|
+
|
59
|
+
if @sampler.is_a?(PerOperation)
|
60
|
+
@sampler.update(strategies: strategies)
|
61
|
+
else
|
62
|
+
@sampler = PerOperation.new(strategies: strategies, max_operations: 2000)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def normalize(instructions)
|
67
|
+
{
|
68
|
+
default_sampling_probability: instructions['defaultSamplingProbability'],
|
69
|
+
default_lower_bound_traces_per_second: instructions['defaultLowerBoundTracesPerSecond'],
|
70
|
+
per_operation_strategies: instructions['perOperationStrategies'].map do |strategy|
|
71
|
+
{
|
72
|
+
operation: strategy['operation'],
|
73
|
+
probabilistic_sampling: {
|
74
|
+
sampling_rate: strategy['probabilisticSampling']['samplingRate']
|
75
|
+
}
|
76
|
+
}
|
77
|
+
end
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
def update_rate_limiting_or_probabilistic_sampler(strategy, instructions)
|
82
|
+
case strategy
|
83
|
+
when 'PROBABILISTIC'
|
84
|
+
update_probabilistic_strategy(instructions['probabilisticSampling'])
|
85
|
+
when 'RATE_LIMITING'
|
86
|
+
update_rate_limiting_strategy(instructions['rateLimitingSampling'])
|
87
|
+
else
|
88
|
+
@logger.warn "Unknown sampling strategy #{strategy}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def update_probabilistic_strategy(instructions)
|
93
|
+
rate = instructions['samplingRate']
|
94
|
+
return unless rate
|
95
|
+
|
96
|
+
if @sampler.is_a?(Probabilistic)
|
97
|
+
@sampler.update(rate: rate)
|
98
|
+
@logger.info "Updated Probabilistic sampler (rate=#{rate})"
|
99
|
+
else
|
100
|
+
@sampler = Probabilistic.new(rate: rate)
|
101
|
+
@logger.info "Updated sampler to Probabilistic (rate=#{rate})"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def update_rate_limiting_strategy(instructions)
|
106
|
+
max_traces_per_second = instructions['maxTracesPerSecond']
|
107
|
+
return unless max_traces_per_second
|
108
|
+
|
109
|
+
if @sampler.is_a?(RateLimiting)
|
110
|
+
@sampler.update(max_traces_per_second: max_traces_per_second)
|
111
|
+
@logger.info "Updated Ratelimiting sampler (max_traces_per_second=#{max_traces_per_second})"
|
112
|
+
else
|
113
|
+
@sampler = RateLimiting.new(max_traces_per_second: max_traces_per_second)
|
114
|
+
@logger.info "Updated sampler to Ratelimiting (max_traces_per_second=#{max_traces_per_second})"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jaeger
|
4
|
+
module Samplers
|
5
|
+
class RemoteControlled
|
6
|
+
class InstructionsFetcher
|
7
|
+
FetchFailed = Class.new(StandardError)
|
8
|
+
|
9
|
+
def initialize(host:, port:, service_name:)
|
10
|
+
@host = host
|
11
|
+
@port = port
|
12
|
+
@service_name = service_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def fetch
|
16
|
+
http = Net::HTTP.new(@host, @port)
|
17
|
+
path = "/sampling?service=#{CGI.escape(@service_name)}"
|
18
|
+
response =
|
19
|
+
begin
|
20
|
+
http.request(Net::HTTP::Get.new(path))
|
21
|
+
rescue StandardError => e
|
22
|
+
raise FetchFailed, e.inspect
|
23
|
+
end
|
24
|
+
|
25
|
+
unless response.is_a?(Net::HTTPSuccess)
|
26
|
+
raise FetchFailed, "Unsuccessful response (code=#{response.code})"
|
27
|
+
end
|
28
|
+
|
29
|
+
JSON.parse(response.body)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/jaeger/scope.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jaeger
|
4
|
+
# Scope represents an OpenTracing Scope
|
5
|
+
#
|
6
|
+
# See http://www.opentracing.io for more information.
|
7
|
+
class Scope
|
8
|
+
def initialize(span, scope_stack, finish_on_close:)
|
9
|
+
@span = span
|
10
|
+
@scope_stack = scope_stack
|
11
|
+
@finish_on_close = finish_on_close
|
12
|
+
@closed = false
|
13
|
+
end
|
14
|
+
|
15
|
+
# Return the Span scoped by this Scope
|
16
|
+
#
|
17
|
+
# @return [Span]
|
18
|
+
attr_reader :span
|
19
|
+
|
20
|
+
# Close scope
|
21
|
+
#
|
22
|
+
# Mark the end of the active period for the current thread and Scope,
|
23
|
+
# updating the ScopeManager#active in the process.
|
24
|
+
def close
|
25
|
+
raise "Tried to close already closed span: #{inspect}" if @closed
|
26
|
+
@closed = true
|
27
|
+
|
28
|
+
@span.finish if @finish_on_close
|
29
|
+
removed_scope = @scope_stack.pop
|
30
|
+
|
31
|
+
if removed_scope != self # rubocop:disable Style/GuardClause
|
32
|
+
raise 'Removed non-active scope, ' \
|
33
|
+
"removed: #{removed_scope.inspect}, "\
|
34
|
+
"expected: #{inspect}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'scope_manager/scope_stack'
|
4
|
+
require_relative 'scope_manager/scope_identifier'
|
5
|
+
|
6
|
+
module Jaeger
|
7
|
+
# ScopeManager represents an OpenTracing ScopeManager
|
8
|
+
#
|
9
|
+
# See http://www.opentracing.io for more information.
|
10
|
+
#
|
11
|
+
# The ScopeManager interface abstracts both the activation of Span instances
|
12
|
+
# via ScopeManager#activate and access to an active Span/Scope via
|
13
|
+
# ScopeManager#active
|
14
|
+
#
|
15
|
+
class ScopeManager
|
16
|
+
def initialize
|
17
|
+
@scope_stack = ScopeStack.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Make a span instance active
|
21
|
+
#
|
22
|
+
# @param span [Span] the Span that should become active
|
23
|
+
# @param finish_on_close [Boolean] whether the Span should automatically be
|
24
|
+
# finished when Scope#close is called
|
25
|
+
# @return [Scope] instance to control the end of the active period for the
|
26
|
+
# Span. It is a programming error to neglect to call Scope#close on the
|
27
|
+
# returned instance.
|
28
|
+
def activate(span, finish_on_close: true)
|
29
|
+
return active if active && active.span == span
|
30
|
+
scope = Scope.new(span, @scope_stack, finish_on_close: finish_on_close)
|
31
|
+
@scope_stack.push(scope)
|
32
|
+
scope
|
33
|
+
end
|
34
|
+
|
35
|
+
# Return active scope
|
36
|
+
#
|
37
|
+
# If there is a non-null Scope, its wrapped Span becomes an implicit parent
|
38
|
+
# (as Reference#CHILD_OF) of any newly-created Span at
|
39
|
+
# Tracer#start_active_span or Tracer#start_span time.
|
40
|
+
#
|
41
|
+
# @return [Scope] the currently active Scope which can be used to access the
|
42
|
+
# currently active Span.
|
43
|
+
def active
|
44
|
+
@scope_stack.peek
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jaeger
|
4
|
+
class ScopeManager
|
5
|
+
# @api private
|
6
|
+
class ScopeIdentifier
|
7
|
+
def self.generate
|
8
|
+
# 65..90.chr are characters between A and Z
|
9
|
+
"opentracing_#{(0...8).map { rand(65..90).chr }.join}".to_sym
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jaeger
|
4
|
+
class ScopeManager
|
5
|
+
# @api private
|
6
|
+
class ScopeStack
|
7
|
+
def initialize
|
8
|
+
# Generate a random identifier to use as the Thread.current key. This is
|
9
|
+
# needed so that it would be possible to create multiple tracers in one
|
10
|
+
# thread (mostly useful for testing purposes)
|
11
|
+
@scope_identifier = ScopeIdentifier.generate
|
12
|
+
end
|
13
|
+
|
14
|
+
def push(scope)
|
15
|
+
store << scope
|
16
|
+
end
|
17
|
+
|
18
|
+
def pop
|
19
|
+
store.pop
|
20
|
+
end
|
21
|
+
|
22
|
+
def peek
|
23
|
+
store.last
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def store
|
29
|
+
Thread.current[@scope_identifier] ||= []
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|