flare 0.3.0 → 0.4.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.
- checksums.yaml +4 -4
- data/lib/flare/client_headers.rb +39 -0
- 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 +39 -0
- data/lib/flare/metric_flusher.rb +154 -12
- data/lib/flare/metric_storage.rb +20 -0
- data/lib/flare/metric_submitter.rb +38 -20
- data/lib/flare/recording_batch_span_processor.rb +238 -0
- data/lib/flare/rule_manager.rb +3 -2
- data/lib/flare/sqlite_exporter.rb +80 -25
- data/lib/flare/trace_exporter.rb +50 -11
- data/lib/flare/version.rb +1 -1
- data/lib/flare.rb +4 -1
- metadata +6 -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
|
|
@@ -96,22 +117,46 @@ module Flare
|
|
|
96
117
|
# after_fork hooks.
|
|
97
118
|
def after_fork
|
|
98
119
|
@pid = $$
|
|
99
|
-
|
|
120
|
+
@storage.after_fork if @storage.respond_to?(:after_fork)
|
|
121
|
+
initialize_synchronization
|
|
122
|
+
@timer = nil
|
|
123
|
+
@pool = nil
|
|
124
|
+
start
|
|
100
125
|
end
|
|
101
126
|
|
|
102
127
|
private
|
|
103
128
|
|
|
129
|
+
def detect_forking
|
|
130
|
+
after_fork if @pid != $$
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def initialize_synchronization
|
|
134
|
+
@submission_mutex = Mutex.new
|
|
135
|
+
@submission_condition = ConditionVariable.new
|
|
136
|
+
@pending_submissions = 0
|
|
137
|
+
@flush_owner = nil
|
|
138
|
+
end
|
|
139
|
+
|
|
104
140
|
def post_to_pool
|
|
141
|
+
return unless reserve_background_submission
|
|
142
|
+
|
|
105
143
|
record_health_metrics
|
|
106
144
|
drained = @storage.drain
|
|
107
145
|
if drained.empty?
|
|
108
146
|
log "No metrics to flush"
|
|
147
|
+
background_submission_finished
|
|
109
148
|
return
|
|
110
149
|
end
|
|
111
150
|
|
|
112
151
|
log "Drained #{drained.size} metric keys for submission"
|
|
113
|
-
@pool.post
|
|
152
|
+
posted = @pool.post do
|
|
153
|
+
submit_to_cloud(drained)
|
|
154
|
+
ensure
|
|
155
|
+
background_submission_finished
|
|
156
|
+
end
|
|
157
|
+
background_submission_finished unless posted
|
|
114
158
|
rescue => e
|
|
159
|
+
background_submission_finished
|
|
115
160
|
warn "[Flare] Metric drain error: #{e.message}"
|
|
116
161
|
end
|
|
117
162
|
|
|
@@ -124,6 +169,103 @@ module Flare
|
|
|
124
169
|
warn "[Flare] Metric submission error: #{e.message}"
|
|
125
170
|
end
|
|
126
171
|
|
|
172
|
+
def reserve_background_submission
|
|
173
|
+
@submission_mutex.synchronize do
|
|
174
|
+
return false if @flush_owner || @pending_submissions.positive?
|
|
175
|
+
|
|
176
|
+
@pending_submissions += 1
|
|
177
|
+
true
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def background_submission_finished
|
|
182
|
+
@submission_mutex.synchronize do
|
|
183
|
+
@pending_submissions -= 1 if @pending_submissions.positive?
|
|
184
|
+
@submission_condition.broadcast
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def flush_synchronously(deadline)
|
|
189
|
+
return [0, nil, true] unless begin_synchronous_flush(deadline)
|
|
190
|
+
|
|
191
|
+
record_health_metrics
|
|
192
|
+
drained = @storage.drain
|
|
193
|
+
return [0, nil, false] if drained.empty?
|
|
194
|
+
|
|
195
|
+
submit_with_deadline(drained, deadline)
|
|
196
|
+
ensure
|
|
197
|
+
finish_synchronous_flush if @flush_owner == Thread.current
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def begin_synchronous_flush(deadline)
|
|
201
|
+
@submission_mutex.synchronize do
|
|
202
|
+
while @flush_owner && @flush_owner != Thread.current
|
|
203
|
+
return false if deadline.expired?
|
|
204
|
+
|
|
205
|
+
@submission_condition.wait(@submission_mutex, deadline.remaining)
|
|
206
|
+
end
|
|
207
|
+
@flush_owner = Thread.current
|
|
208
|
+
|
|
209
|
+
while @pending_submissions.positive?
|
|
210
|
+
return false if deadline.expired?
|
|
211
|
+
|
|
212
|
+
@submission_condition.wait(@submission_mutex, deadline.remaining)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
true
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def finish_synchronous_flush
|
|
219
|
+
@submission_mutex.synchronize do
|
|
220
|
+
@flush_owner = nil
|
|
221
|
+
@submission_condition.broadcast
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def submit_metrics(drained, timeout:)
|
|
226
|
+
parameters = @submitter.method(:submit).parameters
|
|
227
|
+
accepts_timeout = parameters.any? do |type, name|
|
|
228
|
+
type == :keyrest || ([:key, :keyreq].include?(type) && name == :timeout)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
if accepts_timeout
|
|
232
|
+
@submitter.submit(drained, timeout: timeout)
|
|
233
|
+
else
|
|
234
|
+
@submitter.submit(drained)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def submit_with_deadline(drained, deadline)
|
|
239
|
+
operation = { done: false, count: 0, error: nil }
|
|
240
|
+
@submission_mutex.synchronize { @pending_submissions += 1 }
|
|
241
|
+
Thread.new do
|
|
242
|
+
operation[:count], operation[:error] = submit_metrics(drained, timeout: deadline.remaining)
|
|
243
|
+
rescue => e
|
|
244
|
+
operation[:error] = e
|
|
245
|
+
ensure
|
|
246
|
+
@submission_mutex.synchronize do
|
|
247
|
+
operation[:done] = true
|
|
248
|
+
@pending_submissions -= 1
|
|
249
|
+
@submission_condition.broadcast
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
@submission_mutex.synchronize do
|
|
254
|
+
until operation[:done]
|
|
255
|
+
return [0, nil, true] if deadline.expired?
|
|
256
|
+
|
|
257
|
+
@submission_condition.wait(@submission_mutex, deadline.remaining)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
timed_out = deadline.expired? || deadline_error?(operation[:error])
|
|
262
|
+
[operation[:count], operation[:error], timed_out]
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def deadline_error?(error)
|
|
266
|
+
defined?(MetricSubmitter::DeadlineExceeded) && error.is_a?(MetricSubmitter::DeadlineExceeded)
|
|
267
|
+
end
|
|
268
|
+
|
|
127
269
|
def record_health_metrics
|
|
128
270
|
@health_reporters.each { |reporter| reporter.record(@storage) }
|
|
129
271
|
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
|
|
@@ -5,7 +5,9 @@ require "json"
|
|
|
5
5
|
require "zlib"
|
|
6
6
|
require "stringio"
|
|
7
7
|
require "securerandom"
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
require_relative "client_headers"
|
|
10
|
+
require_relative "deadline"
|
|
9
11
|
|
|
10
12
|
module Flare
|
|
11
13
|
# Submits metrics to the Flare metrics service via HTTP.
|
|
@@ -13,7 +15,6 @@ module Flare
|
|
|
13
15
|
class MetricSubmitter
|
|
14
16
|
SCHEMA_VERSION = "V1"
|
|
15
17
|
GZIP_ENCODING = "gzip"
|
|
16
|
-
USER_AGENT = "Flare Ruby/#{Flare::VERSION}"
|
|
17
18
|
|
|
18
19
|
# Default timeouts (in seconds)
|
|
19
20
|
DEFAULT_OPEN_TIMEOUT = 2
|
|
@@ -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,28 +119,25 @@ 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)
|
|
123
133
|
request["Content-Type"] = "application/json"
|
|
124
134
|
request["Content-Encoding"] = GZIP_ENCODING
|
|
125
135
|
request["Authorization"] = "Bearer #{@api_key}"
|
|
126
|
-
request["User-Agent"] = USER_AGENT
|
|
127
136
|
request["X-Request-Id"] = request_id
|
|
128
137
|
request["X-Schema-Version"] = SCHEMA_VERSION
|
|
129
138
|
|
|
130
|
-
# Client
|
|
131
|
-
request[
|
|
132
|
-
request["X-Client-Language-Version"] = RUBY_VERSION
|
|
133
|
-
request["X-Client-Platform"] = RUBY_PLATFORM
|
|
134
|
-
request["X-Client-Pid"] = Process.pid.to_s
|
|
135
|
-
request["X-Client-Hostname"] = Socket.gethostname rescue "unknown"
|
|
139
|
+
# Client + version identifying headers, shared across every Flare-API request.
|
|
140
|
+
ClientHeaders.to_h.each { |name, value| request[name] = value }
|
|
136
141
|
|
|
137
142
|
request.body = body
|
|
138
143
|
response = http.request(request)
|
|
@@ -163,20 +168,27 @@ module Flare
|
|
|
163
168
|
)
|
|
164
169
|
end
|
|
165
170
|
|
|
166
|
-
def retry_with_backoff(max_attempts)
|
|
171
|
+
def retry_with_backoff(max_attempts, deadline:)
|
|
167
172
|
attempts_remaining = max_attempts
|
|
168
173
|
last_error = nil
|
|
169
174
|
|
|
170
175
|
while attempts_remaining > 0
|
|
171
176
|
begin
|
|
177
|
+
return [nil, DeadlineExceeded.new("metric submission deadline exceeded")] if deadline.expired?
|
|
178
|
+
|
|
172
179
|
result, should_retry = yield
|
|
180
|
+
return [nil, DeadlineExceeded.new("metric submission deadline exceeded")] if deadline.expired?
|
|
173
181
|
return [result, nil] unless should_retry
|
|
174
|
-
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
|
|
175
183
|
last_error = e
|
|
176
184
|
attempts_remaining -= 1
|
|
177
185
|
|
|
178
186
|
if attempts_remaining > 0
|
|
179
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
|
|
180
192
|
sleep(sleep_time)
|
|
181
193
|
end
|
|
182
194
|
next
|
|
@@ -189,6 +201,12 @@ module Flare
|
|
|
189
201
|
[nil, last_error]
|
|
190
202
|
end
|
|
191
203
|
|
|
204
|
+
def effective_timeout(configured_timeout, remaining)
|
|
205
|
+
return configured_timeout unless remaining
|
|
206
|
+
|
|
207
|
+
[configured_timeout, remaining].min
|
|
208
|
+
end
|
|
209
|
+
|
|
192
210
|
def gzip(string)
|
|
193
211
|
io = StringIO.new
|
|
194
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
|
@@ -5,6 +5,7 @@ require "logger"
|
|
|
5
5
|
require "concurrent/timer_task"
|
|
6
6
|
require "concurrent/atomic/atomic_fixnum"
|
|
7
7
|
|
|
8
|
+
require_relative "client_headers"
|
|
8
9
|
require_relative "http_transport"
|
|
9
10
|
|
|
10
11
|
module Flare
|
|
@@ -118,11 +119,11 @@ module Flare
|
|
|
118
119
|
end
|
|
119
120
|
|
|
120
121
|
def request_headers
|
|
121
|
-
headers =
|
|
122
|
+
headers = ClientHeaders.to_h.merge(
|
|
122
123
|
"Authorization" => "Bearer #{@api_key}",
|
|
123
124
|
"Flare-Project" => @project,
|
|
124
125
|
"Flare-Environment" => @environment
|
|
125
|
-
|
|
126
|
+
)
|
|
126
127
|
headers["If-None-Match"] = @etag if @etag
|
|
127
128
|
headers
|
|
128
129
|
end
|