flare 0.4.0 → 1.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.

Potentially problematic release.


This version of flare might be problematic. Click here for more details.

Files changed (70) hide show
  1. data/.document +5 -0
  2. data/.gitignore +22 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +18 -0
  5. data/Rakefile +55 -0
  6. data/VERSION +1 -0
  7. data/flare.gemspec +66 -0
  8. data/lib/flare/active_record.rb +102 -0
  9. data/lib/flare/collection.rb +47 -0
  10. data/lib/flare/configuration.rb +64 -123
  11. data/lib/flare/index_builder.rb +24 -0
  12. data/lib/flare/session.rb +142 -0
  13. data/lib/flare/tasks.rb +18 -0
  14. data/lib/flare.rb +19 -537
  15. data/test/helper.rb +10 -0
  16. data/test/test_flare.rb +7 -0
  17. metadata +89 -242
  18. checksums.yaml +0 -7
  19. data/CHANGELOG.md +0 -3
  20. data/LICENSE.txt +0 -21
  21. data/README.md +0 -148
  22. data/app/controllers/flare/application_controller.rb +0 -30
  23. data/app/controllers/flare/jobs_controller.rb +0 -55
  24. data/app/controllers/flare/requests_controller.rb +0 -73
  25. data/app/controllers/flare/spans_controller.rb +0 -101
  26. data/app/helpers/flare/application_helper.rb +0 -168
  27. data/app/views/flare/jobs/index.html.erb +0 -69
  28. data/app/views/flare/jobs/show.html.erb +0 -323
  29. data/app/views/flare/requests/index.html.erb +0 -120
  30. data/app/views/flare/requests/show.html.erb +0 -498
  31. data/app/views/flare/spans/index.html.erb +0 -112
  32. data/app/views/flare/spans/show.html.erb +0 -184
  33. data/app/views/layouts/flare/application.html.erb +0 -126
  34. data/config/routes.rb +0 -20
  35. data/exe/flare +0 -9
  36. data/lib/flare/backoff_policy.rb +0 -73
  37. data/lib/flare/cli/doctor_command.rb +0 -129
  38. data/lib/flare/cli/output.rb +0 -45
  39. data/lib/flare/cli/setup_command.rb +0 -404
  40. data/lib/flare/cli/status_command.rb +0 -47
  41. data/lib/flare/cli.rb +0 -50
  42. data/lib/flare/client_headers.rb +0 -39
  43. data/lib/flare/deadline.rb +0 -27
  44. data/lib/flare/engine.rb +0 -45
  45. data/lib/flare/filtering_span_processor.rb +0 -429
  46. data/lib/flare/http_metrics_config.rb +0 -101
  47. data/lib/flare/http_transport.rb +0 -76
  48. data/lib/flare/lifecycle.rb +0 -39
  49. data/lib/flare/marker.rb +0 -106
  50. data/lib/flare/metric_counter.rb +0 -51
  51. data/lib/flare/metric_flusher.rb +0 -279
  52. data/lib/flare/metric_key.rb +0 -42
  53. data/lib/flare/metric_span_processor.rb +0 -470
  54. data/lib/flare/metric_storage.rb +0 -67
  55. data/lib/flare/metric_submitter.rb +0 -239
  56. data/lib/flare/recording_batch_span_processor.rb +0 -238
  57. data/lib/flare/rule_manager.rb +0 -141
  58. data/lib/flare/sampler.rb +0 -130
  59. data/lib/flare/source_location.rb +0 -113
  60. data/lib/flare/sqlite_exporter.rb +0 -334
  61. data/lib/flare/storage/sqlite.rb +0 -794
  62. data/lib/flare/storage.rb +0 -54
  63. data/lib/flare/trace_blob.rb +0 -116
  64. data/lib/flare/trace_exporter.rb +0 -182
  65. data/lib/flare/trace_health_reporter.rb +0 -74
  66. data/lib/flare/upload_url_pool.rb +0 -108
  67. data/lib/flare/version.rb +0 -5
  68. data/lib/flare/web_marker_subscriber.rb +0 -76
  69. data/public/flare-assets/flare.css +0 -1245
  70. data/public/flare-assets/images/flipper.png +0 -0
@@ -1,239 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "net/http"
4
- require "json"
5
- require "zlib"
6
- require "stringio"
7
- require "securerandom"
8
-
9
- require_relative "client_headers"
10
- require_relative "deadline"
11
-
12
- module Flare
13
- # Submits metrics to the Flare metrics service via HTTP.
14
- # Handles gzip compression, retries with exponential backoff, and error handling.
15
- class MetricSubmitter
16
- SCHEMA_VERSION = "V1"
17
- GZIP_ENCODING = "gzip"
18
-
19
- # Default timeouts (in seconds)
20
- DEFAULT_OPEN_TIMEOUT = 2
21
- DEFAULT_READ_TIMEOUT = 5
22
- DEFAULT_WRITE_TIMEOUT = 5
23
-
24
- # Max retries before giving up
25
- MAX_RETRIES = 3
26
-
27
- class SubmissionError < StandardError
28
- attr_reader :request_id, :response_code, :response_body
29
-
30
- def initialize(message, request_id:, response_code: nil, response_body: nil)
31
- @request_id = request_id
32
- @response_code = response_code
33
- @response_body = response_body
34
- super(message)
35
- end
36
- end
37
-
38
- class ClientError < StandardError
39
- attr_reader :request_id, :response_code, :response_body
40
-
41
- def initialize(message, request_id:, response_code: nil, response_body: nil)
42
- @request_id = request_id
43
- @response_code = response_code
44
- @response_body = response_body
45
- super(message)
46
- end
47
- end
48
-
49
- class DeadlineExceeded < StandardError; end
50
-
51
- attr_reader :endpoint, :api_key, :backoff_policy
52
-
53
- def initialize(endpoint:, api_key:, project: nil, environment: nil, backoff_policy: nil, open_timeout: nil, read_timeout: nil, write_timeout: nil)
54
- @endpoint = URI("#{endpoint.to_s.chomp('/')}/api/metrics")
55
- @api_key = api_key
56
- @project = project || default_project
57
- @environment = environment || default_environment
58
- @backoff_policy = backoff_policy || BackoffPolicy.new
59
- @open_timeout = open_timeout || DEFAULT_OPEN_TIMEOUT
60
- @read_timeout = read_timeout || DEFAULT_READ_TIMEOUT
61
- @write_timeout = write_timeout || DEFAULT_WRITE_TIMEOUT
62
- end
63
-
64
- # Submit drained metrics to the server.
65
- # Returns [success_count, error] where error may be nil on success.
66
- def submit(drained, timeout: nil)
67
- return [0, nil] if drained.empty?
68
-
69
- deadline = Deadline.new(timeout)
70
-
71
- request_id = SecureRandom.uuid
72
- Flare.log "Submitting #{drained.size} metrics to #{@endpoint} (request_id=#{request_id})" if Flare.respond_to?(:log)
73
-
74
- body = build_body(drained, request_id)
75
- return [0, nil] if body.nil?
76
- return [0, DeadlineExceeded.new("metric submission deadline exceeded")] if deadline.expired?
77
-
78
- @backoff_policy.reset
79
- response, error = retry_with_backoff(MAX_RETRIES, deadline: deadline) do
80
- post(body, request_id, timeout: deadline.remaining)
81
- end
82
-
83
- if error
84
- Flare.log "Submission failed: #{error.message} (request_id=#{request_id})" if Flare.respond_to?(:log)
85
- [0, error]
86
- else
87
- Flare.log "Submission succeeded: #{response.code} (request_id=#{request_id})" if Flare.respond_to?(:log)
88
- [drained.size, nil]
89
- end
90
- end
91
-
92
- private
93
-
94
- def build_body(drained, request_id)
95
- metrics = drained.map do |key, values|
96
- {
97
- bucket: format_time(key.bucket),
98
- namespace: key.namespace,
99
- service: key.service,
100
- target: key.target || "",
101
- operation: key.operation,
102
- count: values[:count],
103
- sum_ms: values[:sum_ms],
104
- error_count: values[:error_count]
105
- }
106
- end
107
-
108
- payload = {
109
- request_id: request_id,
110
- schema_version: SCHEMA_VERSION,
111
- project: @project,
112
- environment: @environment,
113
- metrics: metrics
114
- }
115
-
116
- gzip(JSON.generate(payload))
117
- rescue => e
118
- warn "[Flare] Failed to build submission body: #{e.message}"
119
- nil
120
- end
121
-
122
- def post(body, request_id, timeout: nil)
123
- raise DeadlineExceeded, "metric submission deadline exceeded" if timeout == 0
124
-
125
- http = Net::HTTP.new(@endpoint.host, @endpoint.port)
126
- http.use_ssl = @endpoint.scheme == "https"
127
- http.open_timeout = effective_timeout(@open_timeout, timeout)
128
- http.read_timeout = effective_timeout(@read_timeout, timeout)
129
- http.write_timeout = effective_timeout(@write_timeout, timeout) if http.respond_to?(:write_timeout=)
130
-
131
- request_uri = @endpoint.request_uri
132
- request = Net::HTTP::Post.new(request_uri == "" ? "/" : request_uri)
133
- request["Content-Type"] = "application/json"
134
- request["Content-Encoding"] = GZIP_ENCODING
135
- request["Authorization"] = "Bearer #{@api_key}"
136
- request["X-Request-Id"] = request_id
137
- request["X-Schema-Version"] = SCHEMA_VERSION
138
-
139
- # Client + version identifying headers, shared across every Flare-API request.
140
- ClientHeaders.to_h.each { |name, value| request[name] = value }
141
-
142
- request.body = body
143
- response = http.request(request)
144
-
145
- code = response.code.to_i
146
-
147
- # Success
148
- if code >= 200 && code < 300
149
- return [response, false] # [result, should_retry]
150
- end
151
-
152
- # Retriable errors: rate limiting, server errors
153
- if code == 429 || code >= 500
154
- raise SubmissionError.new(
155
- "Retriable error: #{code}",
156
- request_id: request_id,
157
- response_code: code,
158
- response_body: response.body
159
- )
160
- end
161
-
162
- # Non-retriable client errors (4xx except 429)
163
- raise ClientError.new(
164
- "Client error: #{code}",
165
- request_id: request_id,
166
- response_code: code,
167
- response_body: response.body
168
- )
169
- end
170
-
171
- def retry_with_backoff(max_attempts, deadline:)
172
- attempts_remaining = max_attempts
173
- last_error = nil
174
-
175
- while attempts_remaining > 0
176
- begin
177
- return [nil, DeadlineExceeded.new("metric submission deadline exceeded")] if deadline.expired?
178
-
179
- result, should_retry = yield
180
- return [nil, DeadlineExceeded.new("metric submission deadline exceeded")] if deadline.expired?
181
- return [result, nil] unless should_retry
182
- rescue SubmissionError, Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Errno::ECONNREFUSED, Errno::ECONNRESET => e
183
- last_error = e
184
- attempts_remaining -= 1
185
-
186
- if attempts_remaining > 0
187
- sleep_time = @backoff_policy.next_interval / 1000.0
188
- remaining = deadline.remaining
189
- if remaining && sleep_time >= remaining
190
- return [nil, DeadlineExceeded.new("metric submission deadline exceeded")]
191
- end
192
- sleep(sleep_time)
193
- end
194
- next
195
- rescue => e
196
- # Unexpected errors - don't retry
197
- return [nil, e]
198
- end
199
- end
200
-
201
- [nil, last_error]
202
- end
203
-
204
- def effective_timeout(configured_timeout, remaining)
205
- return configured_timeout unless remaining
206
-
207
- [configured_timeout, remaining].min
208
- end
209
-
210
- def gzip(string)
211
- io = StringIO.new
212
- io.set_encoding("BINARY")
213
- gz = Zlib::GzipWriter.new(io)
214
- gz.write(string)
215
- gz.close
216
- io.string
217
- end
218
-
219
- def format_time(time)
220
- time.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
221
- end
222
-
223
- def default_project
224
- if defined?(Rails) && Rails.application
225
- Rails.application.class.module_parent_name.underscore rescue "rails_app"
226
- else
227
- "app"
228
- end
229
- end
230
-
231
- def default_environment
232
- if defined?(Rails)
233
- Rails.env.to_s
234
- else
235
- ENV.fetch("RACK_ENV", "development")
236
- end
237
- end
238
- end
239
- end
@@ -1,238 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "logger"
4
- require "opentelemetry/sdk"
5
-
6
- require_relative "deadline"
7
-
8
- module Flare
9
- # An asynchronous, bounded span processor that exports every ended recording
10
- # span. OpenTelemetry's BatchSpanProcessor only accepts sampled spans, which
11
- # excludes RECORD_ONLY spans needed by Flare's local development dashboard.
12
- class RecordingBatchSpanProcessor
13
- SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS
14
- FAILURE = OpenTelemetry::SDK::Trace::Export::FAILURE
15
- TIMEOUT = OpenTelemetry::SDK::Trace::Export::TIMEOUT
16
-
17
- def initialize(exporter, exporter_timeout: 30_000, schedule_delay: 5_000,
18
- max_queue_size: 2_048, max_export_batch_size: 512, logger: nil)
19
- raise ArgumentError if max_export_batch_size > max_queue_size
20
-
21
- @exporter = exporter
22
- @exporter_timeout = exporter_timeout / 1_000.0
23
- @schedule_delay = schedule_delay / 1_000.0
24
- @max_queue_size = max_queue_size
25
- @max_export_batch_size = max_export_batch_size
26
- @logger = logger || Logger.new($stderr, level: Logger::WARN)
27
- @pid = $$
28
- initialize_synchronization
29
- start_worker
30
- end
31
-
32
- def on_start(_span, _parent_context); end
33
-
34
- def on_finish(span)
35
- detect_forking
36
-
37
- @mutex.synchronize do
38
- overflow = @queue.length + 1 - @max_queue_size
39
- @queue.shift(overflow) if overflow.positive?
40
- @queue << span
41
- @condition.signal if @queue.length >= @max_export_batch_size
42
- end
43
- end
44
-
45
- def force_flush(timeout: nil)
46
- detect_forking
47
- deadline = Deadline.new(timeout)
48
- return TIMEOUT unless begin_flush(deadline)
49
-
50
- snapshot = snapshot_for_flush
51
- operation = start_flush_export(snapshot, deadline)
52
- result = wait_for_flush_export(operation, deadline)
53
- [@flush_prior_result, result].max
54
- rescue StandardError => e
55
- log_export_error(e)
56
- FAILURE
57
- ensure
58
- finish_flush if @flush_owner == Thread.current
59
- end
60
-
61
- def shutdown(timeout: nil)
62
- detect_forking
63
- deadline = Deadline.new(timeout)
64
-
65
- worker = @mutex.synchronize do
66
- @stopped = true
67
- @condition.broadcast
68
- @worker
69
- end
70
- worker&.join(deadline.remaining)
71
- return TIMEOUT if worker&.alive? || deadline.expired?
72
-
73
- result = force_flush(timeout: deadline.remaining)
74
- return result unless result == SUCCESS
75
- return TIMEOUT if deadline.expired?
76
-
77
- exporter_result = @exporter.shutdown(timeout: deadline.remaining)
78
- deadline.expired? ? TIMEOUT : exporter_result
79
- rescue StandardError => e
80
- log_export_error(e)
81
- FAILURE
82
- end
83
-
84
- private
85
-
86
- def initialize_synchronization
87
- @mutex = Mutex.new
88
- @condition = ConditionVariable.new
89
- @queue = []
90
- @active_exports = 0
91
- @export_completion_sequence = 0
92
- @last_export_result = SUCCESS
93
- @flush_prior_result = SUCCESS
94
- @flush_owner = nil
95
- @stopped = false
96
- @worker = nil
97
- end
98
-
99
- def worker_loop
100
- loop do
101
- batch = @mutex.synchronize do
102
- while !@stopped && (@queue.empty? || @flush_owner || @active_exports.positive?)
103
- @condition.wait(@mutex, @schedule_delay)
104
- break if !@queue.empty? && !@flush_owner && @active_exports.zero?
105
- end
106
- return if @stopped
107
-
108
- @active_exports += 1
109
- @queue.shift(@max_export_batch_size)
110
- end
111
-
112
- result = export_batch(batch, timeout: @exporter_timeout)
113
- ensure
114
- export_finished(result || FAILURE) if batch
115
- end
116
- end
117
-
118
- def begin_flush(deadline)
119
- @mutex.synchronize do
120
- initial_sequence = @export_completion_sequence
121
- while @flush_owner && @flush_owner != Thread.current
122
- return false if deadline.expired?
123
-
124
- @condition.wait(@mutex, deadline.remaining)
125
- end
126
- @flush_owner = Thread.current
127
-
128
- while @active_exports.positive?
129
- return false if deadline.expired?
130
-
131
- @condition.wait(@mutex, deadline.remaining)
132
- end
133
- @flush_prior_result = if @export_completion_sequence > initial_sequence
134
- @last_export_result
135
- else
136
- SUCCESS
137
- end
138
- end
139
- true
140
- end
141
-
142
- def finish_flush
143
- @mutex.synchronize do
144
- @flush_owner = nil
145
- @condition.broadcast
146
- end
147
- end
148
-
149
- def snapshot_for_flush
150
- @mutex.synchronize { @queue.shift(@queue.length) }
151
- end
152
-
153
- def export_snapshot(snapshot, deadline)
154
- until snapshot.empty?
155
- return TIMEOUT if deadline.expired?
156
-
157
- batch = snapshot.shift(@max_export_batch_size)
158
- result = export_batch(batch, timeout: deadline.remaining)
159
- return result unless result == SUCCESS
160
- end
161
- SUCCESS
162
- ensure
163
- @mutex.synchronize { @queue.unshift(*snapshot) } if snapshot&.any?
164
- end
165
-
166
- def start_flush_export(snapshot, deadline)
167
- operation = { done: false, result: nil }
168
- @mutex.synchronize { @active_exports += 1 }
169
- Thread.new do
170
- result = export_snapshot(snapshot, deadline)
171
- if result == SUCCESS && !deadline.expired?
172
- result = @exporter.force_flush(timeout: deadline.remaining)
173
- end
174
- operation[:result] = deadline.expired? ? TIMEOUT : result
175
- rescue StandardError => e
176
- log_export_error(e)
177
- operation[:result] = FAILURE
178
- ensure
179
- @mutex.synchronize do
180
- operation[:done] = true
181
- complete_export(operation[:result])
182
- end
183
- end
184
- operation
185
- end
186
-
187
- def wait_for_flush_export(operation, deadline)
188
- @mutex.synchronize do
189
- until operation[:done]
190
- return TIMEOUT if deadline.expired?
191
-
192
- @condition.wait(@mutex, deadline.remaining)
193
- end
194
- end
195
- operation[:result]
196
- end
197
-
198
- def export_batch(spans, timeout:)
199
- span_data = spans.map { |span| span.respond_to?(:to_span_data) ? span.to_span_data : span }
200
- @exporter.export(span_data, timeout: timeout)
201
- rescue StandardError => e
202
- log_export_error(e)
203
- FAILURE
204
- end
205
-
206
- def export_finished(result)
207
- @mutex.synchronize do
208
- complete_export(result)
209
- end
210
- end
211
-
212
- def complete_export(result)
213
- @active_exports -= 1
214
- @export_completion_sequence += 1
215
- @last_export_result = result || FAILURE
216
- @condition.broadcast
217
- end
218
-
219
- def detect_forking
220
- return if @pid == $$
221
-
222
- # Only the forking thread survives. Replacing synchronization objects
223
- # avoids waiting on locks or in-flight state owned by vanished threads.
224
- @pid = $$
225
- initialize_synchronization
226
- start_worker
227
- end
228
-
229
- def start_worker
230
- @worker = Thread.new { worker_loop }
231
- @worker.name = "flare-recording-batch-span-processor"
232
- end
233
-
234
- def log_export_error(error)
235
- @logger.warn("[Flare::RecordingBatchSpanProcessor] export failed: #{error.class}: #{error.message}")
236
- end
237
- end
238
- end
@@ -1,141 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "logger"
5
- require "concurrent/timer_task"
6
- require "concurrent/atomic/atomic_fixnum"
7
-
8
- require_relative "client_headers"
9
- require_relative "http_transport"
10
-
11
- module Flare
12
- # The SDK's only poll. Every interval seconds (default 30) it does a
13
- # GET /api/rules; the 200 response carries the active TraceRules (with
14
- # server-computed sample_rate) plus a bag of presigned R2 PUT URLs.
15
- # We hand the rules to Sampler#update_rules and the URLs to
16
- # UploadUrlPool#replace, and sweep the Marker so stuck rack-span
17
- # entries don't linger.
18
- #
19
- # ETag-guarded: subsequent polls send If-None-Match. A 304 still gets
20
- # us a Marker.sweep but doesn't touch sampler or pool. 401/403 stops
21
- # the poller (misconfigured token shouldn't beat down the server).
22
- # 5xx and exceptions are logged + counted; the timer just tries again
23
- # on the next tick.
24
- #
25
- # Fork-safe: after_fork clears the pool and restarts the timer in the
26
- # child process so each child polls independently.
27
- class RuleManager
28
- DEFAULT_INTERVAL = 30
29
-
30
- attr_reader :poll_count, :etag, :stopped_due_to_auth, :last_error_count
31
-
32
- def initialize(sampler:, marker:, pool:, base_url:, api_key:, project:, environment:,
33
- interval: DEFAULT_INTERVAL, transport: nil, logger: nil)
34
- @sampler = sampler
35
- @marker = marker
36
- @pool = pool
37
- @rules_url = "#{base_url.to_s.chomp('/')}/api/rules"
38
- @api_key = api_key
39
- @project = project
40
- @environment = environment
41
- @interval = interval
42
- @transport = transport || HttpTransport.new
43
- @logger = logger || Logger.new($stderr, level: Logger::WARN)
44
-
45
- @etag = nil
46
- @poll_count = Concurrent::AtomicFixnum.new(0)
47
- @last_error_count = Concurrent::AtomicFixnum.new(0)
48
- @stopped_due_to_auth = false
49
- @pid = $$
50
- end
51
-
52
- def start
53
- return self if @timer || @stopped_due_to_auth
54
-
55
- @timer = Concurrent::TimerTask.execute(
56
- execution_interval: @interval,
57
- run_now: true,
58
- name: "flare-rule-manager-timer"
59
- ) { poll_safely }
60
- self
61
- end
62
-
63
- def stop
64
- if @timer
65
- @timer.shutdown
66
- @timer.wait_for_termination(1)
67
- @timer.kill unless @timer.shutdown?
68
- @timer = nil
69
- end
70
- self
71
- end
72
-
73
- def running?
74
- @timer ? @timer.running? : false
75
- end
76
-
77
- def after_fork
78
- @pid = $$
79
- @pool.after_fork
80
- stop
81
- start
82
- end
83
-
84
- # Public so callers can force a poll (tests + integration tests).
85
- def poll_now
86
- poll_safely
87
- end
88
-
89
- private
90
-
91
- def poll_safely
92
- poll
93
- rescue StandardError => e
94
- @last_error_count.increment
95
- @logger.warn("[Flare::RuleManager] poll exception: #{e.class}: #{e.message}")
96
- end
97
-
98
- def poll
99
- return if @stopped_due_to_auth
100
-
101
- response = @transport.get(@rules_url, request_headers)
102
- @poll_count.increment
103
-
104
- case response.code
105
- when "304"
106
- @marker.sweep
107
- when "200"
108
- @etag = response.header("ETag")
109
- apply(JSON.parse(response.body))
110
- @marker.sweep
111
- when "401", "403"
112
- @stopped_due_to_auth = true
113
- @logger.warn("[Flare::RuleManager] auth failed (#{response.code}); stopping poll")
114
- stop
115
- else
116
- @last_error_count.increment
117
- @logger.warn("[Flare::RuleManager] unexpected #{response.code}")
118
- end
119
- end
120
-
121
- def request_headers
122
- headers = ClientHeaders.to_h.merge(
123
- "Authorization" => "Bearer #{@api_key}",
124
- "Flare-Project" => @project,
125
- "Flare-Environment" => @environment
126
- )
127
- headers["If-None-Match"] = @etag if @etag
128
- headers
129
- end
130
-
131
- # Server payload shape (see tirana-v2 Api::RulesController):
132
- # { "trace_rules": [{ "id", "match_attributes", "rate", ..., "urls": [...] }] }
133
- def apply(payload)
134
- rules = payload["trace_rules"] || []
135
- @sampler.update_rules(rules)
136
-
137
- url_entries = rules.flat_map { |r| Array(r["urls"]) }
138
- @pool.replace(url_entries)
139
- end
140
- end
141
- end