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.
- data/.document +5 -0
- data/.gitignore +22 -0
- data/LICENSE +20 -0
- data/README.rdoc +18 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/flare.gemspec +66 -0
- data/lib/flare/active_record.rb +102 -0
- data/lib/flare/collection.rb +47 -0
- data/lib/flare/configuration.rb +64 -123
- data/lib/flare/index_builder.rb +24 -0
- data/lib/flare/session.rb +142 -0
- data/lib/flare/tasks.rb +18 -0
- data/lib/flare.rb +19 -537
- data/test/helper.rb +10 -0
- data/test/test_flare.rb +7 -0
- metadata +89 -242
- checksums.yaml +0 -7
- data/CHANGELOG.md +0 -3
- data/LICENSE.txt +0 -21
- data/README.md +0 -148
- data/app/controllers/flare/application_controller.rb +0 -30
- data/app/controllers/flare/jobs_controller.rb +0 -55
- data/app/controllers/flare/requests_controller.rb +0 -73
- data/app/controllers/flare/spans_controller.rb +0 -101
- data/app/helpers/flare/application_helper.rb +0 -168
- data/app/views/flare/jobs/index.html.erb +0 -69
- data/app/views/flare/jobs/show.html.erb +0 -323
- data/app/views/flare/requests/index.html.erb +0 -120
- data/app/views/flare/requests/show.html.erb +0 -498
- data/app/views/flare/spans/index.html.erb +0 -112
- data/app/views/flare/spans/show.html.erb +0 -184
- data/app/views/layouts/flare/application.html.erb +0 -126
- data/config/routes.rb +0 -20
- data/exe/flare +0 -9
- data/lib/flare/backoff_policy.rb +0 -73
- data/lib/flare/cli/doctor_command.rb +0 -129
- data/lib/flare/cli/output.rb +0 -45
- data/lib/flare/cli/setup_command.rb +0 -404
- data/lib/flare/cli/status_command.rb +0 -47
- data/lib/flare/cli.rb +0 -50
- data/lib/flare/client_headers.rb +0 -39
- data/lib/flare/deadline.rb +0 -27
- data/lib/flare/engine.rb +0 -45
- data/lib/flare/filtering_span_processor.rb +0 -429
- data/lib/flare/http_metrics_config.rb +0 -101
- data/lib/flare/http_transport.rb +0 -76
- data/lib/flare/lifecycle.rb +0 -39
- data/lib/flare/marker.rb +0 -106
- data/lib/flare/metric_counter.rb +0 -51
- data/lib/flare/metric_flusher.rb +0 -279
- data/lib/flare/metric_key.rb +0 -42
- data/lib/flare/metric_span_processor.rb +0 -470
- data/lib/flare/metric_storage.rb +0 -67
- data/lib/flare/metric_submitter.rb +0 -239
- data/lib/flare/recording_batch_span_processor.rb +0 -238
- data/lib/flare/rule_manager.rb +0 -141
- data/lib/flare/sampler.rb +0 -130
- data/lib/flare/source_location.rb +0 -113
- data/lib/flare/sqlite_exporter.rb +0 -334
- data/lib/flare/storage/sqlite.rb +0 -794
- data/lib/flare/storage.rb +0 -54
- data/lib/flare/trace_blob.rb +0 -116
- data/lib/flare/trace_exporter.rb +0 -182
- data/lib/flare/trace_health_reporter.rb +0 -74
- data/lib/flare/upload_url_pool.rb +0 -108
- data/lib/flare/version.rb +0 -5
- data/lib/flare/web_marker_subscriber.rb +0 -76
- data/public/flare-assets/flare.css +0 -1245
- 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
|
data/lib/flare/rule_manager.rb
DELETED
|
@@ -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
|