dox-jaeger-client 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +33 -0
  3. data/.gitignore +12 -0
  4. data/.gitmodules +3 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +61 -0
  7. data/.rubocop_todo.yml +13 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE.txt +21 -0
  10. data/Makefile +1 -0
  11. data/README.md +210 -0
  12. data/Rakefile +9 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +8 -0
  15. data/crossdock/Dockerfile +31 -0
  16. data/crossdock/Gemfile +6 -0
  17. data/crossdock/Gemfile.lock +37 -0
  18. data/crossdock/docker-compose.yml +68 -0
  19. data/crossdock/jaeger-docker-compose.yml +53 -0
  20. data/crossdock/rules.mk +35 -0
  21. data/crossdock/server +175 -0
  22. data/jaeger-client.gemspec +37 -0
  23. data/lib/jaeger/client/version.rb +7 -0
  24. data/lib/jaeger/client.rb +77 -0
  25. data/lib/jaeger/encoders/thrift_encoder.rb +173 -0
  26. data/lib/jaeger/extractors.rb +173 -0
  27. data/lib/jaeger/http_sender.rb +28 -0
  28. data/lib/jaeger/injectors.rb +83 -0
  29. data/lib/jaeger/rate_limiter.rb +61 -0
  30. data/lib/jaeger/recurring_executor.rb +35 -0
  31. data/lib/jaeger/reporters/composite_reporter.rb +17 -0
  32. data/lib/jaeger/reporters/in_memory_reporter.rb +30 -0
  33. data/lib/jaeger/reporters/logging_reporter.rb +22 -0
  34. data/lib/jaeger/reporters/null_reporter.rb +11 -0
  35. data/lib/jaeger/reporters/remote_reporter/buffer.rb +29 -0
  36. data/lib/jaeger/reporters/remote_reporter.rb +42 -0
  37. data/lib/jaeger/reporters.rb +7 -0
  38. data/lib/jaeger/samplers/const.rb +24 -0
  39. data/lib/jaeger/samplers/guaranteed_throughput_probabilistic.rb +47 -0
  40. data/lib/jaeger/samplers/per_operation.rb +77 -0
  41. data/lib/jaeger/samplers/probabilistic.rb +40 -0
  42. data/lib/jaeger/samplers/rate_limiting.rb +51 -0
  43. data/lib/jaeger/samplers/remote_controlled/instructions_fetcher.rb +34 -0
  44. data/lib/jaeger/samplers/remote_controlled.rb +119 -0
  45. data/lib/jaeger/samplers.rb +8 -0
  46. data/lib/jaeger/scope.rb +39 -0
  47. data/lib/jaeger/scope_manager/scope_identifier.rb +13 -0
  48. data/lib/jaeger/scope_manager/scope_stack.rb +33 -0
  49. data/lib/jaeger/scope_manager.rb +48 -0
  50. data/lib/jaeger/span/thrift_log_builder.rb +18 -0
  51. data/lib/jaeger/span.rb +97 -0
  52. data/lib/jaeger/span_context.rb +57 -0
  53. data/lib/jaeger/thrift_tag_builder.rb +42 -0
  54. data/lib/jaeger/trace_id.rb +48 -0
  55. data/lib/jaeger/tracer.rb +214 -0
  56. data/lib/jaeger/udp_sender/transport.rb +41 -0
  57. data/lib/jaeger/udp_sender.rb +26 -0
  58. data/script/create_follows_from_trace +51 -0
  59. data/script/create_trace +52 -0
  60. data/thrift/agent.thrift +32 -0
  61. data/thrift/gen-rb/jaeger/thrift/agent/agent.rb +118 -0
  62. data/thrift/gen-rb/jaeger/thrift/agent/agent_constants.rb +15 -0
  63. data/thrift/gen-rb/jaeger/thrift/agent/agent_types.rb +17 -0
  64. data/thrift/gen-rb/jaeger/thrift/agent.rb +116 -0
  65. data/thrift/gen-rb/jaeger/thrift/agent_constants.rb +13 -0
  66. data/thrift/gen-rb/jaeger/thrift/agent_types.rb +15 -0
  67. data/thrift/gen-rb/jaeger/thrift/collector.rb +82 -0
  68. data/thrift/gen-rb/jaeger/thrift/jaeger_constants.rb +13 -0
  69. data/thrift/gen-rb/jaeger/thrift/jaeger_types.rb +211 -0
  70. data/thrift/gen-rb/jaeger/thrift/zipkin/zipkin_collector.rb +84 -0
  71. data/thrift/gen-rb/jaeger/thrift/zipkin/zipkincore_constants.rb +41 -0
  72. data/thrift/gen-rb/jaeger/thrift/zipkin/zipkincore_types.rb +220 -0
  73. data/thrift/jaeger.thrift +88 -0
  74. data/thrift/zipkincore.thrift +300 -0
  75. metadata +260 -0
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jaeger
4
+ module Injectors
5
+ def self.context_as_jaeger_string(span_context)
6
+ [
7
+ span_context.trace_id.to_s(16),
8
+ span_context.span_id.to_s(16),
9
+ span_context.parent_id.to_s(16),
10
+ span_context.flags.to_s(16)
11
+ ].join(':')
12
+ end
13
+
14
+ class JaegerTextMapCodec
15
+ def self.inject(span_context, carrier)
16
+ carrier['uber-trace-id'] = Injectors.context_as_jaeger_string(span_context)
17
+ span_context.baggage.each do |key, value|
18
+ carrier["uberctx-#{key}"] = value
19
+ end
20
+ end
21
+ end
22
+
23
+ class JaegerRackCodec
24
+ def self.inject(span_context, carrier)
25
+ carrier['uber-trace-id'] =
26
+ CGI.escape(Injectors.context_as_jaeger_string(span_context))
27
+ span_context.baggage.each do |key, value|
28
+ carrier["uberctx-#{key}"] = CGI.escape(value)
29
+ end
30
+ end
31
+ end
32
+
33
+ class JaegerBinaryCodec
34
+ def self.inject(_span_context, _carrier)
35
+ warn 'Jaeger::Client with binary format is not supported yet'
36
+ end
37
+ end
38
+
39
+ class B3RackCodec
40
+ def self.inject(span_context, carrier)
41
+ carrier['x-b3-traceid'] = TraceId.to_hex(span_context.trace_id)
42
+ carrier['x-b3-spanid'] = TraceId.to_hex(span_context.span_id)
43
+ carrier['x-b3-parentspanid'] = TraceId.to_hex(span_context.parent_id)
44
+
45
+ # flags (for debug) and sampled headers are mutually exclusive
46
+ if span_context.flags == Jaeger::SpanContext::Flags::DEBUG
47
+ carrier['x-b3-flags'] = '1'
48
+ else
49
+ carrier['x-b3-sampled'] = span_context.flags.to_s(16)
50
+ end
51
+ end
52
+ end
53
+
54
+ class TraceContextRackCodec
55
+ def self.inject(span_context, carrier)
56
+ flags = span_context.sampled? || span_context.debug? ? 1 : 0
57
+
58
+ carrier['traceparent'] = format(
59
+ '%<version>s-%<trace_id>s-%<span_id>s-%<flags>s',
60
+ version: '00',
61
+ trace_id: span_context.trace_id.to_s(16).rjust(32, '0'),
62
+ span_id: span_context.span_id.to_s(16).rjust(16, '0'),
63
+ flags: flags.to_s(16).rjust(2, '0')
64
+ )
65
+ end
66
+ end
67
+
68
+ DEFAULT_INJECTORS = {
69
+ OpenTracing::FORMAT_TEXT_MAP => JaegerTextMapCodec,
70
+ OpenTracing::FORMAT_BINARY => JaegerBinaryCodec,
71
+ OpenTracing::FORMAT_RACK => JaegerRackCodec
72
+ }.freeze
73
+
74
+ def self.prepare(injectors)
75
+ DEFAULT_INJECTORS.reduce(injectors) do |acc, (format, default)|
76
+ provided_injectors = Array(injectors[format])
77
+ provided_injectors += [default] if provided_injectors.empty?
78
+
79
+ acc.merge(format => provided_injectors)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jaeger
4
+ # RateLimiter is based on leaky bucket algorithm, formulated in terms of a
5
+ # credits balance that is replenished every time check_credit() method is
6
+ # called (tick) by the amount proportional to the time elapsed since the
7
+ # last tick, up to the max_balance. A call to check_credit() takes a cost
8
+ # of an item we want to pay with the balance. If the balance exceeds the
9
+ # cost of the item, the item is "purchased" and the balance reduced,
10
+ # indicated by returned value of true. Otherwise the balance is unchanged
11
+ # and return false.
12
+ #
13
+ # This can be used to limit a rate of messages emitted by a service by
14
+ # instantiating the Rate Limiter with the max number of messages a service
15
+ # is allowed to emit per second, and calling check_credit(1.0) for each
16
+ # message to determine if the message is within the rate limit.
17
+ #
18
+ # It can also be used to limit the rate of traffic in bytes, by setting
19
+ # credits_per_second to desired throughput as bytes/second, and calling
20
+ # check_credit() with the actual message size.
21
+ class RateLimiter
22
+ def initialize(credits_per_second:, max_balance:)
23
+ @credits_per_second = credits_per_second
24
+ @max_balance = max_balance
25
+ @balance = max_balance
26
+ @last_tick = Time.now
27
+ end
28
+
29
+ def check_credit(item_cost)
30
+ update_balance
31
+
32
+ return false if @balance < item_cost
33
+
34
+ @balance -= item_cost
35
+ true
36
+ end
37
+
38
+ def update(credits_per_second:, max_balance:)
39
+ update_balance
40
+
41
+ @credits_per_second = credits_per_second
42
+
43
+ # The new balance should be proportional to the old balance
44
+ @balance = max_balance * @balance / @max_balance
45
+ @max_balance = max_balance
46
+ end
47
+
48
+ private
49
+
50
+ def update_balance
51
+ current_time = Time.now
52
+ elapsed_time = current_time - @last_tick
53
+ @last_tick = current_time
54
+
55
+ @balance += elapsed_time * @credits_per_second
56
+ return if @balance <= @max_balance
57
+
58
+ @balance = @max_balance
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jaeger
4
+ # Executes a given block periodically. The block will be executed only once
5
+ # when interval is set to 0.
6
+ class RecurringExecutor
7
+ def initialize(interval:)
8
+ @interval = interval
9
+ end
10
+
11
+ def start(&block)
12
+ raise 'Already running' if @thread
13
+
14
+ @thread = Thread.new do
15
+ if @interval <= 0
16
+ yield
17
+ else
18
+ loop do
19
+ yield
20
+ sleep @interval
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ def running?
27
+ @thread&.alive?
28
+ end
29
+
30
+ def stop
31
+ @thread.kill
32
+ @thread = nil
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jaeger
4
+ module Reporters
5
+ class CompositeReporter
6
+ def initialize(reporters:)
7
+ @reporters = reporters
8
+ end
9
+
10
+ def report(span)
11
+ @reporters.each do |reporter|
12
+ reporter.report(span)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jaeger
4
+ module Reporters
5
+ class InMemoryReporter
6
+ def initialize
7
+ @spans = []
8
+ @mutex = Mutex.new
9
+ end
10
+
11
+ def report(span)
12
+ @mutex.synchronize do
13
+ @spans << span
14
+ end
15
+ end
16
+
17
+ def spans
18
+ @mutex.synchronize do
19
+ @spans
20
+ end
21
+ end
22
+
23
+ def clear
24
+ @mutex.synchronize do
25
+ @spans.clear
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jaeger
4
+ module Reporters
5
+ class LoggingReporter
6
+ def initialize(logger: Logger.new($stdout))
7
+ @logger = logger
8
+ end
9
+
10
+ def report(span)
11
+ span_info = {
12
+ operation_name: span.operation_name,
13
+ start_time: span.start_time.iso8601,
14
+ end_time: span.end_time.iso8601,
15
+ trace_id: span.context.to_trace_id,
16
+ span_id: span.context.to_span_id
17
+ }
18
+ @logger.info "Span reported: #{span_info}"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jaeger
4
+ module Reporters
5
+ class NullReporter
6
+ def report(_span)
7
+ # no-op
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jaeger
4
+ module Reporters
5
+ class RemoteReporter
6
+ class Buffer
7
+ def initialize
8
+ @buffer = []
9
+ @mutex = Mutex.new
10
+ end
11
+
12
+ def <<(element)
13
+ @mutex.synchronize do
14
+ @buffer << element
15
+ true
16
+ end
17
+ end
18
+
19
+ def retrieve
20
+ @mutex.synchronize do
21
+ elements = @buffer.dup
22
+ @buffer.clear
23
+ elements
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './remote_reporter/buffer'
4
+
5
+ module Jaeger
6
+ module Reporters
7
+ class RemoteReporter
8
+ def initialize(sender:, flush_interval:)
9
+ @sender = sender
10
+ @flush_interval = flush_interval
11
+ @buffer = Buffer.new
12
+ end
13
+
14
+ def flush
15
+ spans = @buffer.retrieve
16
+ @sender.send_spans(spans) if spans.any?
17
+ spans
18
+ end
19
+
20
+ def report(span)
21
+ return if !span.context.sampled? && !span.context.debug?
22
+
23
+ init_reporter_thread
24
+ @buffer << span
25
+ end
26
+
27
+ private
28
+
29
+ def init_reporter_thread
30
+ return if @initializer_pid == Process.pid
31
+
32
+ @initializer_pid = Process.pid
33
+ Thread.new do
34
+ loop do
35
+ flush
36
+ sleep(@flush_interval)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'reporters/composite_reporter'
4
+ require_relative 'reporters/in_memory_reporter'
5
+ require_relative 'reporters/logging_reporter'
6
+ require_relative 'reporters/null_reporter'
7
+ require_relative 'reporters/remote_reporter'
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jaeger
4
+ module Samplers
5
+ # Const sampler
6
+ #
7
+ # A sampler that always makes the same decision for new traces depending
8
+ # on the initialization value. Use `Jaeger::Samplers::Const.new(true)`
9
+ # to mark all new traces as sampled.
10
+ class Const
11
+ def initialize(decision)
12
+ @decision = decision
13
+ @tags = {
14
+ 'sampler.type' => 'const',
15
+ 'sampler.param' => @decision ? 1 : 0
16
+ }
17
+ end
18
+
19
+ def sample(*)
20
+ [@decision, @tags]
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jaeger
4
+ module Samplers
5
+ # A sampler that leverages both Probabilistic sampler and RateLimiting
6
+ # sampler. The RateLimiting is used as a guaranteed lower bound sampler
7
+ # such that every operation is sampled at least once in a time interval
8
+ # defined by the lower_bound. ie a lower_bound of 1.0 / (60 * 10) will
9
+ # sample an operation at least once every 10 minutes.
10
+ #
11
+ # The Probabilistic sampler is given higher priority when tags are
12
+ # emitted, ie. if is_sampled() for both samplers return true, the tags
13
+ # for Probabilistic sampler will be used.
14
+ class GuaranteedThroughputProbabilistic
15
+ attr_reader :tags, :probabilistic_sampler, :lower_bound_sampler
16
+
17
+ def initialize(lower_bound:, rate:, lower_bound_sampler: nil)
18
+ @probabilistic_sampler = Probabilistic.new(rate: rate)
19
+ @lower_bound_sampler = lower_bound_sampler || RateLimiting.new(max_traces_per_second: lower_bound)
20
+ @lower_bound_tags = {
21
+ 'sampler.type' => 'lowerbound',
22
+ 'sampler.param' => rate
23
+ }
24
+ end
25
+
26
+ def update(lower_bound:, rate:)
27
+ is_updated = @probabilistic_sampler.update(rate: rate)
28
+ is_updated = @lower_bound_sampler.update(max_traces_per_second: lower_bound) || is_updated
29
+ @lower_bound_tags['sampler.param'] = rate
30
+ is_updated
31
+ end
32
+
33
+ def sample(...)
34
+ is_sampled, probabilistic_tags = @probabilistic_sampler.sample(...)
35
+ if is_sampled
36
+ # We still call lower_bound_sampler to update the rate limiter budget
37
+ @lower_bound_sampler.sample(...)
38
+
39
+ return [is_sampled, probabilistic_tags]
40
+ end
41
+
42
+ is_sampled, _tags = @lower_bound_sampler.sample(...)
43
+ [is_sampled, @lower_bound_tags]
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,77 @@
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
+ update_operation_strategies(strategies) || is_updated
36
+ end
37
+
38
+ def sample(opts)
39
+ operation_name = opts.fetch(:operation_name)
40
+ sampler = @samplers[operation_name]
41
+ return sampler.sample(opts) if sampler
42
+
43
+ return @default_sampler.sample(opts) if @samplers.length >= @max_operations
44
+
45
+ sampler = GuaranteedThroughputProbabilistic.new(
46
+ lower_bound: @lower_bound,
47
+ rate: @default_sampling_probability
48
+ )
49
+ @samplers[operation_name] = sampler
50
+ sampler.sample(opts)
51
+ end
52
+
53
+ private
54
+
55
+ def update_operation_strategies(strategies)
56
+ is_updated = false
57
+
58
+ (strategies[:per_operation_strategies] || []).each do |strategy|
59
+ operation = strategy.fetch(:operation)
60
+ rate = strategy.fetch(:probabilistic_sampling).fetch(:sampling_rate)
61
+
62
+ if (sampler = @samplers[operation])
63
+ is_updated = sampler.update(lower_bound: @lower_bound, rate: rate) || is_updated
64
+ else
65
+ @samplers[operation] = GuaranteedThroughputProbabilistic.new(
66
+ lower_bound: @lower_bound,
67
+ rate: rate
68
+ )
69
+ is_updated = true
70
+ end
71
+ end
72
+
73
+ is_updated
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,40 @@
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(opts)
34
+ trace_id = opts.fetch(:trace_id)
35
+
36
+ [@boundary >= trace_id, @tags]
37
+ end
38
+ end
39
+ end
40
+ 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,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