dox-jaeger-client 2.0.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 (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