flare 0.3.1 → 0.4.1
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/lib/flare/deadline.rb +27 -0
- data/lib/flare/filtering_span_processor.rb +180 -30
- data/lib/flare/http_transport.rb +24 -10
- data/lib/flare/lifecycle.rb +74 -0
- data/lib/flare/metric_flusher.rb +156 -12
- data/lib/flare/metric_storage.rb +20 -0
- data/lib/flare/metric_submitter.rb +34 -11
- data/lib/flare/recording_batch_span_processor.rb +238 -0
- data/lib/flare/rule_manager.rb +2 -2
- data/lib/flare/sqlite_exporter.rb +80 -25
- data/lib/flare/trace_exporter.rb +44 -9
- data/lib/flare/version.rb +1 -1
- data/lib/flare.rb +6 -17
- metadata +5 -2
data/lib/flare/metric_flusher.rb
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require "concurrent/timer_task"
|
|
4
4
|
require "concurrent/executor/fixed_thread_pool"
|
|
5
|
+
require "opentelemetry/sdk"
|
|
6
|
+
|
|
7
|
+
require_relative "deadline"
|
|
5
8
|
|
|
6
9
|
module Flare
|
|
7
10
|
# Background threads that periodically drain in-memory metrics and submit
|
|
@@ -10,6 +13,9 @@ module Flare
|
|
|
10
13
|
#
|
|
11
14
|
# Fork-safe: detects forked processes and restarts automatically.
|
|
12
15
|
class MetricFlusher
|
|
16
|
+
SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS
|
|
17
|
+
FAILURE = OpenTelemetry::SDK::Trace::Export::FAILURE
|
|
18
|
+
TIMEOUT = OpenTelemetry::SDK::Trace::Export::TIMEOUT
|
|
13
19
|
DEFAULT_INTERVAL = 60 # seconds
|
|
14
20
|
DEFAULT_SHUTDOWN_TIMEOUT = 5 # seconds
|
|
15
21
|
|
|
@@ -23,6 +29,7 @@ module Flare
|
|
|
23
29
|
@health_reporters = Array(health_reporters)
|
|
24
30
|
@pid = $$
|
|
25
31
|
@stopped = false
|
|
32
|
+
initialize_synchronization
|
|
26
33
|
end
|
|
27
34
|
|
|
28
35
|
def start
|
|
@@ -40,23 +47,25 @@ module Flare
|
|
|
40
47
|
}) { post_to_pool }
|
|
41
48
|
end
|
|
42
49
|
|
|
43
|
-
def stop
|
|
50
|
+
def stop(timeout: @shutdown_timeout)
|
|
44
51
|
return if @stopped
|
|
45
52
|
|
|
53
|
+
deadline = Deadline.new(timeout)
|
|
46
54
|
@stopped = true
|
|
47
55
|
|
|
48
56
|
log "Shutting down metrics flusher, draining remaining metrics..."
|
|
49
57
|
|
|
50
58
|
if @timer
|
|
51
59
|
@timer.shutdown
|
|
52
|
-
@timer.wait_for_termination(1)
|
|
60
|
+
@timer.wait_for_termination([deadline.remaining || 1, 1].min)
|
|
53
61
|
@timer.kill unless @timer.shutdown?
|
|
54
62
|
end
|
|
55
63
|
|
|
64
|
+
force_flush(timeout: deadline.remaining)
|
|
65
|
+
|
|
56
66
|
if @pool
|
|
57
|
-
post_to_pool # one last drain
|
|
58
67
|
@pool.shutdown
|
|
59
|
-
pool_terminated = @pool.wait_for_termination(@shutdown_timeout)
|
|
68
|
+
pool_terminated = @pool.wait_for_termination(deadline.remaining || @shutdown_timeout)
|
|
60
69
|
@pool.kill unless pool_terminated
|
|
61
70
|
end
|
|
62
71
|
|
|
@@ -70,14 +79,11 @@ module Flare
|
|
|
70
79
|
end
|
|
71
80
|
|
|
72
81
|
# Manually trigger a flush (useful for testing or forced flushes).
|
|
73
|
-
def flush_now
|
|
82
|
+
def flush_now(timeout: nil)
|
|
74
83
|
return 0 unless @storage && @submitter
|
|
75
84
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return 0 if drained.empty?
|
|
79
|
-
|
|
80
|
-
count, error = @submitter.submit(drained)
|
|
85
|
+
detect_forking
|
|
86
|
+
count, error, = flush_synchronously(Deadline.new(timeout))
|
|
81
87
|
if error
|
|
82
88
|
warn "[Flare] Metric submission error: #{error.message}"
|
|
83
89
|
end
|
|
@@ -87,6 +93,21 @@ module Flare
|
|
|
87
93
|
0
|
|
88
94
|
end
|
|
89
95
|
|
|
96
|
+
def force_flush(timeout: nil)
|
|
97
|
+
return SUCCESS unless @storage && @submitter
|
|
98
|
+
|
|
99
|
+
detect_forking
|
|
100
|
+
deadline = Deadline.new(timeout)
|
|
101
|
+
_count, error, timed_out = flush_synchronously(deadline)
|
|
102
|
+
return TIMEOUT if timed_out || deadline.expired?
|
|
103
|
+
return FAILURE if error
|
|
104
|
+
|
|
105
|
+
SUCCESS
|
|
106
|
+
rescue => e
|
|
107
|
+
warn "[Flare] Metric flush error: #{e.message}"
|
|
108
|
+
FAILURE
|
|
109
|
+
end
|
|
110
|
+
|
|
90
111
|
def running?
|
|
91
112
|
@timer&.running? || false
|
|
92
113
|
end
|
|
@@ -95,23 +116,49 @@ module Flare
|
|
|
95
116
|
# on first span in the new process, or manually from Puma/Unicorn
|
|
96
117
|
# after_fork hooks.
|
|
97
118
|
def after_fork
|
|
119
|
+
return if @pid == $$
|
|
120
|
+
|
|
98
121
|
@pid = $$
|
|
99
|
-
|
|
122
|
+
@storage.after_fork if @storage.respond_to?(:after_fork)
|
|
123
|
+
initialize_synchronization
|
|
124
|
+
@timer = nil
|
|
125
|
+
@pool = nil
|
|
126
|
+
start
|
|
100
127
|
end
|
|
101
128
|
|
|
102
129
|
private
|
|
103
130
|
|
|
131
|
+
def detect_forking
|
|
132
|
+
after_fork if @pid != $$
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def initialize_synchronization
|
|
136
|
+
@submission_mutex = Mutex.new
|
|
137
|
+
@submission_condition = ConditionVariable.new
|
|
138
|
+
@pending_submissions = 0
|
|
139
|
+
@flush_owner = nil
|
|
140
|
+
end
|
|
141
|
+
|
|
104
142
|
def post_to_pool
|
|
143
|
+
return unless reserve_background_submission
|
|
144
|
+
|
|
105
145
|
record_health_metrics
|
|
106
146
|
drained = @storage.drain
|
|
107
147
|
if drained.empty?
|
|
108
148
|
log "No metrics to flush"
|
|
149
|
+
background_submission_finished
|
|
109
150
|
return
|
|
110
151
|
end
|
|
111
152
|
|
|
112
153
|
log "Drained #{drained.size} metric keys for submission"
|
|
113
|
-
@pool.post
|
|
154
|
+
posted = @pool.post do
|
|
155
|
+
submit_to_cloud(drained)
|
|
156
|
+
ensure
|
|
157
|
+
background_submission_finished
|
|
158
|
+
end
|
|
159
|
+
background_submission_finished unless posted
|
|
114
160
|
rescue => e
|
|
161
|
+
background_submission_finished
|
|
115
162
|
warn "[Flare] Metric drain error: #{e.message}"
|
|
116
163
|
end
|
|
117
164
|
|
|
@@ -124,6 +171,103 @@ module Flare
|
|
|
124
171
|
warn "[Flare] Metric submission error: #{e.message}"
|
|
125
172
|
end
|
|
126
173
|
|
|
174
|
+
def reserve_background_submission
|
|
175
|
+
@submission_mutex.synchronize do
|
|
176
|
+
return false if @flush_owner || @pending_submissions.positive?
|
|
177
|
+
|
|
178
|
+
@pending_submissions += 1
|
|
179
|
+
true
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def background_submission_finished
|
|
184
|
+
@submission_mutex.synchronize do
|
|
185
|
+
@pending_submissions -= 1 if @pending_submissions.positive?
|
|
186
|
+
@submission_condition.broadcast
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def flush_synchronously(deadline)
|
|
191
|
+
return [0, nil, true] unless begin_synchronous_flush(deadline)
|
|
192
|
+
|
|
193
|
+
record_health_metrics
|
|
194
|
+
drained = @storage.drain
|
|
195
|
+
return [0, nil, false] if drained.empty?
|
|
196
|
+
|
|
197
|
+
submit_with_deadline(drained, deadline)
|
|
198
|
+
ensure
|
|
199
|
+
finish_synchronous_flush if @flush_owner == Thread.current
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def begin_synchronous_flush(deadline)
|
|
203
|
+
@submission_mutex.synchronize do
|
|
204
|
+
while @flush_owner && @flush_owner != Thread.current
|
|
205
|
+
return false if deadline.expired?
|
|
206
|
+
|
|
207
|
+
@submission_condition.wait(@submission_mutex, deadline.remaining)
|
|
208
|
+
end
|
|
209
|
+
@flush_owner = Thread.current
|
|
210
|
+
|
|
211
|
+
while @pending_submissions.positive?
|
|
212
|
+
return false if deadline.expired?
|
|
213
|
+
|
|
214
|
+
@submission_condition.wait(@submission_mutex, deadline.remaining)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
true
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def finish_synchronous_flush
|
|
221
|
+
@submission_mutex.synchronize do
|
|
222
|
+
@flush_owner = nil
|
|
223
|
+
@submission_condition.broadcast
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def submit_metrics(drained, timeout:)
|
|
228
|
+
parameters = @submitter.method(:submit).parameters
|
|
229
|
+
accepts_timeout = parameters.any? do |type, name|
|
|
230
|
+
type == :keyrest || ([:key, :keyreq].include?(type) && name == :timeout)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
if accepts_timeout
|
|
234
|
+
@submitter.submit(drained, timeout: timeout)
|
|
235
|
+
else
|
|
236
|
+
@submitter.submit(drained)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def submit_with_deadline(drained, deadline)
|
|
241
|
+
operation = { done: false, count: 0, error: nil }
|
|
242
|
+
@submission_mutex.synchronize { @pending_submissions += 1 }
|
|
243
|
+
Thread.new do
|
|
244
|
+
operation[:count], operation[:error] = submit_metrics(drained, timeout: deadline.remaining)
|
|
245
|
+
rescue => e
|
|
246
|
+
operation[:error] = e
|
|
247
|
+
ensure
|
|
248
|
+
@submission_mutex.synchronize do
|
|
249
|
+
operation[:done] = true
|
|
250
|
+
@pending_submissions -= 1
|
|
251
|
+
@submission_condition.broadcast
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
@submission_mutex.synchronize do
|
|
256
|
+
until operation[:done]
|
|
257
|
+
return [0, nil, true] if deadline.expired?
|
|
258
|
+
|
|
259
|
+
@submission_condition.wait(@submission_mutex, deadline.remaining)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
timed_out = deadline.expired? || deadline_error?(operation[:error])
|
|
264
|
+
[operation[:count], operation[:error], timed_out]
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def deadline_error?(error)
|
|
268
|
+
defined?(MetricSubmitter::DeadlineExceeded) && error.is_a?(MetricSubmitter::DeadlineExceeded)
|
|
269
|
+
end
|
|
270
|
+
|
|
127
271
|
def record_health_metrics
|
|
128
272
|
@health_reporters.each { |reporter| reporter.record(@storage) }
|
|
129
273
|
rescue => e
|
data/lib/flare/metric_storage.rb
CHANGED
|
@@ -9,14 +9,17 @@ module Flare
|
|
|
9
9
|
class MetricStorage
|
|
10
10
|
def initialize
|
|
11
11
|
@storage = Concurrent::Map.new
|
|
12
|
+
@pid = $$
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def increment(key, duration_ms:, error: false)
|
|
16
|
+
detect_forking
|
|
15
17
|
counter = @storage.compute_if_absent(key) { MetricCounter.new }
|
|
16
18
|
counter.increment(duration_ms: duration_ms, error: error)
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
def add(key, count:, sum_ms:, error_count: 0)
|
|
22
|
+
detect_forking
|
|
20
23
|
counter = @storage.compute_if_absent(key) { MetricCounter.new }
|
|
21
24
|
counter.add(count: count, sum_ms: sum_ms, error_count: error_count)
|
|
22
25
|
end
|
|
@@ -24,6 +27,7 @@ module Flare
|
|
|
24
27
|
# Atomically retrieves and clears all metrics.
|
|
25
28
|
# Returns a frozen hash of MetricKey => counter data.
|
|
26
29
|
def drain
|
|
30
|
+
detect_forking
|
|
27
31
|
result = {}
|
|
28
32
|
@storage.keys.each do |key|
|
|
29
33
|
counter = @storage.delete(key)
|
|
@@ -33,15 +37,31 @@ module Flare
|
|
|
33
37
|
end
|
|
34
38
|
|
|
35
39
|
def size
|
|
40
|
+
detect_forking
|
|
36
41
|
@storage.size
|
|
37
42
|
end
|
|
38
43
|
|
|
39
44
|
def empty?
|
|
45
|
+
detect_forking
|
|
40
46
|
@storage.empty?
|
|
41
47
|
end
|
|
42
48
|
|
|
43
49
|
def [](key)
|
|
50
|
+
detect_forking
|
|
44
51
|
@storage[key]
|
|
45
52
|
end
|
|
53
|
+
|
|
54
|
+
def after_fork
|
|
55
|
+
return if @pid == $$
|
|
56
|
+
|
|
57
|
+
@pid = $$
|
|
58
|
+
@storage = Concurrent::Map.new
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def detect_forking
|
|
64
|
+
after_fork
|
|
65
|
+
end
|
|
46
66
|
end
|
|
47
67
|
end
|
|
@@ -7,6 +7,7 @@ require "stringio"
|
|
|
7
7
|
require "securerandom"
|
|
8
8
|
|
|
9
9
|
require_relative "client_headers"
|
|
10
|
+
require_relative "deadline"
|
|
10
11
|
|
|
11
12
|
module Flare
|
|
12
13
|
# Submits metrics to the Flare metrics service via HTTP.
|
|
@@ -45,6 +46,8 @@ module Flare
|
|
|
45
46
|
end
|
|
46
47
|
end
|
|
47
48
|
|
|
49
|
+
class DeadlineExceeded < StandardError; end
|
|
50
|
+
|
|
48
51
|
attr_reader :endpoint, :api_key, :backoff_policy
|
|
49
52
|
|
|
50
53
|
def initialize(endpoint:, api_key:, project: nil, environment: nil, backoff_policy: nil, open_timeout: nil, read_timeout: nil, write_timeout: nil)
|
|
@@ -60,23 +63,28 @@ module Flare
|
|
|
60
63
|
|
|
61
64
|
# Submit drained metrics to the server.
|
|
62
65
|
# Returns [success_count, error] where error may be nil on success.
|
|
63
|
-
def submit(drained)
|
|
66
|
+
def submit(drained, timeout: nil)
|
|
64
67
|
return [0, nil] if drained.empty?
|
|
65
68
|
|
|
69
|
+
deadline = Deadline.new(timeout)
|
|
70
|
+
|
|
66
71
|
request_id = SecureRandom.uuid
|
|
67
|
-
Flare.log "Submitting #{drained.size} metrics to #{@endpoint} (request_id=#{request_id})"
|
|
72
|
+
Flare.log "Submitting #{drained.size} metrics to #{@endpoint} (request_id=#{request_id})" if Flare.respond_to?(:log)
|
|
68
73
|
|
|
69
74
|
body = build_body(drained, request_id)
|
|
70
75
|
return [0, nil] if body.nil?
|
|
76
|
+
return [0, DeadlineExceeded.new("metric submission deadline exceeded")] if deadline.expired?
|
|
71
77
|
|
|
72
78
|
@backoff_policy.reset
|
|
73
|
-
response, error = retry_with_backoff(MAX_RETRIES
|
|
79
|
+
response, error = retry_with_backoff(MAX_RETRIES, deadline: deadline) do
|
|
80
|
+
post(body, request_id, timeout: deadline.remaining)
|
|
81
|
+
end
|
|
74
82
|
|
|
75
83
|
if error
|
|
76
|
-
Flare.log "Submission failed: #{error.message} (request_id=#{request_id})"
|
|
84
|
+
Flare.log "Submission failed: #{error.message} (request_id=#{request_id})" if Flare.respond_to?(:log)
|
|
77
85
|
[0, error]
|
|
78
86
|
else
|
|
79
|
-
Flare.log "Submission succeeded: #{response.code} (request_id=#{request_id})"
|
|
87
|
+
Flare.log "Submission succeeded: #{response.code} (request_id=#{request_id})" if Flare.respond_to?(:log)
|
|
80
88
|
[drained.size, nil]
|
|
81
89
|
end
|
|
82
90
|
end
|
|
@@ -111,12 +119,14 @@ module Flare
|
|
|
111
119
|
nil
|
|
112
120
|
end
|
|
113
121
|
|
|
114
|
-
def post(body, request_id)
|
|
122
|
+
def post(body, request_id, timeout: nil)
|
|
123
|
+
raise DeadlineExceeded, "metric submission deadline exceeded" if timeout == 0
|
|
124
|
+
|
|
115
125
|
http = Net::HTTP.new(@endpoint.host, @endpoint.port)
|
|
116
126
|
http.use_ssl = @endpoint.scheme == "https"
|
|
117
|
-
http.open_timeout = @open_timeout
|
|
118
|
-
http.read_timeout = @read_timeout
|
|
119
|
-
http.write_timeout = @write_timeout if http.respond_to?(:write_timeout=)
|
|
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=)
|
|
120
130
|
|
|
121
131
|
request_uri = @endpoint.request_uri
|
|
122
132
|
request = Net::HTTP::Post.new(request_uri == "" ? "/" : request_uri)
|
|
@@ -158,20 +168,27 @@ module Flare
|
|
|
158
168
|
)
|
|
159
169
|
end
|
|
160
170
|
|
|
161
|
-
def retry_with_backoff(max_attempts)
|
|
171
|
+
def retry_with_backoff(max_attempts, deadline:)
|
|
162
172
|
attempts_remaining = max_attempts
|
|
163
173
|
last_error = nil
|
|
164
174
|
|
|
165
175
|
while attempts_remaining > 0
|
|
166
176
|
begin
|
|
177
|
+
return [nil, DeadlineExceeded.new("metric submission deadline exceeded")] if deadline.expired?
|
|
178
|
+
|
|
167
179
|
result, should_retry = yield
|
|
180
|
+
return [nil, DeadlineExceeded.new("metric submission deadline exceeded")] if deadline.expired?
|
|
168
181
|
return [result, nil] unless should_retry
|
|
169
|
-
rescue SubmissionError, Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, Errno::ECONNRESET => e
|
|
182
|
+
rescue SubmissionError, Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Errno::ECONNREFUSED, Errno::ECONNRESET => e
|
|
170
183
|
last_error = e
|
|
171
184
|
attempts_remaining -= 1
|
|
172
185
|
|
|
173
186
|
if attempts_remaining > 0
|
|
174
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
|
|
175
192
|
sleep(sleep_time)
|
|
176
193
|
end
|
|
177
194
|
next
|
|
@@ -184,6 +201,12 @@ module Flare
|
|
|
184
201
|
[nil, last_error]
|
|
185
202
|
end
|
|
186
203
|
|
|
204
|
+
def effective_timeout(configured_timeout, remaining)
|
|
205
|
+
return configured_timeout unless remaining
|
|
206
|
+
|
|
207
|
+
[configured_timeout, remaining].min
|
|
208
|
+
end
|
|
209
|
+
|
|
187
210
|
def gzip(string)
|
|
188
211
|
io = StringIO.new
|
|
189
212
|
io.set_encoding("BINARY")
|
|
@@ -0,0 +1,238 @@
|
|
|
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
CHANGED