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
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require "sqlite3"
|
|
4
4
|
require "json"
|
|
5
5
|
|
|
6
|
+
require_relative "deadline"
|
|
7
|
+
|
|
6
8
|
module Flare
|
|
7
9
|
class SQLiteExporter
|
|
8
10
|
SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS
|
|
@@ -16,45 +18,61 @@ module Flare
|
|
|
16
18
|
@database_path = database_path
|
|
17
19
|
@mutex = Mutex.new
|
|
18
20
|
@setup = false
|
|
21
|
+
@pid = $$
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
# Maximum number of retry attempts when the database is busy.
|
|
22
25
|
# Mirrors ActiveRecord's retry strategy for SQLite.
|
|
23
26
|
MAX_RETRIES = 3
|
|
27
|
+
ExportDeadlineExceeded = Class.new(StandardError)
|
|
24
28
|
|
|
25
29
|
def export(span_datas, timeout: nil)
|
|
26
|
-
|
|
30
|
+
detect_forking
|
|
31
|
+
deadline = Deadline.new(timeout)
|
|
27
32
|
|
|
28
33
|
retries = 0
|
|
29
34
|
exported = 0
|
|
30
35
|
|
|
31
36
|
begin
|
|
32
|
-
@
|
|
37
|
+
setup_database(deadline) unless @setup
|
|
38
|
+
raise ExportDeadlineExceeded if deadline.expired?
|
|
39
|
+
raise ExportDeadlineExceeded unless lock_before_deadline(deadline)
|
|
40
|
+
|
|
41
|
+
begin
|
|
42
|
+
apply_busy_timeout(deadline)
|
|
33
43
|
connection.transaction do
|
|
34
44
|
span_datas.each do |span_data|
|
|
45
|
+
raise ExportDeadlineExceeded if deadline.expired?
|
|
35
46
|
next if should_ignore_span?(span_data)
|
|
36
47
|
|
|
37
48
|
create_span(span_data)
|
|
38
49
|
exported += 1
|
|
39
50
|
end
|
|
40
51
|
end
|
|
52
|
+
ensure
|
|
53
|
+
@mutex.unlock
|
|
41
54
|
end
|
|
55
|
+
rescue ExportDeadlineExceeded
|
|
56
|
+
return TIMEOUT
|
|
42
57
|
rescue ::SQLite3::BusyException
|
|
43
58
|
retries += 1
|
|
44
59
|
if retries <= MAX_RETRIES
|
|
45
|
-
|
|
60
|
+
sleep_time = 0.1 * retries
|
|
61
|
+
return TIMEOUT if deadline.remaining && sleep_time >= deadline.remaining
|
|
62
|
+
|
|
63
|
+
sleep(sleep_time)
|
|
46
64
|
retry
|
|
47
65
|
end
|
|
48
66
|
warn "[Flare] SQLite export error: database is busy after #{MAX_RETRIES} retries"
|
|
49
67
|
return FAILURE
|
|
50
68
|
end
|
|
51
69
|
|
|
52
|
-
Flare.log "Exported #{exported} spans to SQLite" if exported > 0
|
|
70
|
+
Flare.log "Exported #{exported} spans to SQLite" if exported > 0 && Flare.respond_to?(:log)
|
|
53
71
|
|
|
54
72
|
# Periodically prune old data
|
|
55
|
-
maybe_prune
|
|
73
|
+
maybe_prune unless deadline.expired?
|
|
56
74
|
|
|
57
|
-
SUCCESS
|
|
75
|
+
deadline.expired? ? TIMEOUT : SUCCESS
|
|
58
76
|
rescue => e
|
|
59
77
|
warn "[Flare] SQLite export error: #{e.message}"
|
|
60
78
|
FAILURE
|
|
@@ -70,6 +88,31 @@ module Flare
|
|
|
70
88
|
|
|
71
89
|
private
|
|
72
90
|
|
|
91
|
+
def detect_forking
|
|
92
|
+
return if @pid == $$
|
|
93
|
+
|
|
94
|
+
@pid = $$
|
|
95
|
+
@mutex = Mutex.new
|
|
96
|
+
close_connection
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def lock_before_deadline(deadline)
|
|
100
|
+
return @mutex.lock unless deadline.remaining
|
|
101
|
+
|
|
102
|
+
until @mutex.try_lock
|
|
103
|
+
return false if deadline.expired?
|
|
104
|
+
|
|
105
|
+
sleep([deadline.remaining, 0.001].min)
|
|
106
|
+
end
|
|
107
|
+
true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def apply_busy_timeout(deadline)
|
|
111
|
+
remaining = deadline.remaining
|
|
112
|
+
timeout_ms = remaining ? [(remaining * 1_000).floor, 1].max : 5_000
|
|
113
|
+
connection.busy_timeout = [timeout_ms, 5_000].min
|
|
114
|
+
end
|
|
115
|
+
|
|
73
116
|
def maybe_prune
|
|
74
117
|
return unless rand < PRUNE_PROBABILITY
|
|
75
118
|
|
|
@@ -162,14 +205,16 @@ module Flare
|
|
|
162
205
|
end
|
|
163
206
|
end
|
|
164
207
|
|
|
165
|
-
def setup_database
|
|
166
|
-
|
|
208
|
+
def setup_database(deadline)
|
|
209
|
+
raise ExportDeadlineExceeded unless lock_before_deadline(deadline)
|
|
210
|
+
|
|
211
|
+
begin
|
|
167
212
|
return if @setup
|
|
168
213
|
|
|
169
214
|
db = connection
|
|
170
|
-
configure_pragmas(db)
|
|
215
|
+
configure_pragmas(db, deadline)
|
|
171
216
|
|
|
172
|
-
db
|
|
217
|
+
execute_setup(db, deadline, <<~SQL)
|
|
173
218
|
CREATE TABLE IF NOT EXISTS flare_spans (
|
|
174
219
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
175
220
|
name TEXT NOT NULL,
|
|
@@ -187,23 +232,23 @@ module Flare
|
|
|
187
232
|
)
|
|
188
233
|
SQL
|
|
189
234
|
|
|
190
|
-
db
|
|
235
|
+
execute_setup(db, deadline, <<~SQL)
|
|
191
236
|
CREATE INDEX IF NOT EXISTS idx_spans_span_id ON flare_spans(span_id)
|
|
192
237
|
SQL
|
|
193
238
|
|
|
194
|
-
db
|
|
239
|
+
execute_setup(db, deadline, <<~SQL)
|
|
195
240
|
CREATE INDEX IF NOT EXISTS idx_spans_trace_id ON flare_spans(trace_id)
|
|
196
241
|
SQL
|
|
197
242
|
|
|
198
|
-
db
|
|
243
|
+
execute_setup(db, deadline, <<~SQL)
|
|
199
244
|
CREATE INDEX IF NOT EXISTS idx_spans_parent_span_id ON flare_spans(parent_span_id)
|
|
200
245
|
SQL
|
|
201
246
|
|
|
202
|
-
db
|
|
247
|
+
execute_setup(db, deadline, <<~SQL)
|
|
203
248
|
CREATE INDEX IF NOT EXISTS idx_spans_created_at ON flare_spans(created_at)
|
|
204
249
|
SQL
|
|
205
250
|
|
|
206
|
-
db
|
|
251
|
+
execute_setup(db, deadline, <<~SQL)
|
|
207
252
|
CREATE TABLE IF NOT EXISTS flare_events (
|
|
208
253
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
209
254
|
span_id INTEGER NOT NULL,
|
|
@@ -214,11 +259,11 @@ module Flare
|
|
|
214
259
|
)
|
|
215
260
|
SQL
|
|
216
261
|
|
|
217
|
-
db
|
|
262
|
+
execute_setup(db, deadline, <<~SQL)
|
|
218
263
|
CREATE INDEX IF NOT EXISTS idx_events_span_id ON flare_events(span_id)
|
|
219
264
|
SQL
|
|
220
265
|
|
|
221
|
-
db
|
|
266
|
+
execute_setup(db, deadline, <<~SQL)
|
|
222
267
|
CREATE TABLE IF NOT EXISTS flare_properties (
|
|
223
268
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
224
269
|
key TEXT NOT NULL,
|
|
@@ -231,27 +276,37 @@ module Flare
|
|
|
231
276
|
)
|
|
232
277
|
SQL
|
|
233
278
|
|
|
234
|
-
db
|
|
279
|
+
execute_setup(db, deadline, <<~SQL)
|
|
235
280
|
CREATE INDEX IF NOT EXISTS idx_properties_owner ON flare_properties(owner_type, owner_id)
|
|
236
281
|
SQL
|
|
237
282
|
|
|
238
|
-
db
|
|
283
|
+
execute_setup(db, deadline, <<~SQL)
|
|
239
284
|
CREATE INDEX IF NOT EXISTS idx_properties_key ON flare_properties(key)
|
|
240
285
|
SQL
|
|
241
286
|
|
|
242
287
|
close_connection # avoid inheriting connection across fork
|
|
243
288
|
@setup = true
|
|
289
|
+
ensure
|
|
290
|
+
@mutex.unlock
|
|
244
291
|
end
|
|
245
292
|
end
|
|
246
293
|
|
|
247
294
|
# Applies the same SQLite pragmas that ActiveRecord uses for good
|
|
248
295
|
# concurrency and performance with threaded/multi-process access.
|
|
249
|
-
def configure_pragmas(db)
|
|
250
|
-
db
|
|
251
|
-
db
|
|
252
|
-
db
|
|
253
|
-
db
|
|
254
|
-
db
|
|
296
|
+
def configure_pragmas(db, deadline)
|
|
297
|
+
execute_setup(db, deadline, "PRAGMA journal_mode=WAL")
|
|
298
|
+
execute_setup(db, deadline, "PRAGMA synchronous=NORMAL")
|
|
299
|
+
execute_setup(db, deadline, "PRAGMA mmap_size=134217728") # 128MB
|
|
300
|
+
execute_setup(db, deadline, "PRAGMA journal_size_limit=67108864") # 64MB
|
|
301
|
+
execute_setup(db, deadline, "PRAGMA cache_size=2000")
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def execute_setup(db, deadline, statement)
|
|
305
|
+
raise ExportDeadlineExceeded if deadline.expired?
|
|
306
|
+
|
|
307
|
+
apply_busy_timeout(deadline)
|
|
308
|
+
db.execute(statement)
|
|
309
|
+
raise ExportDeadlineExceeded if deadline.expired?
|
|
255
310
|
end
|
|
256
311
|
|
|
257
312
|
def connection_key
|
data/lib/flare/trace_exporter.rb
CHANGED
|
@@ -8,9 +8,11 @@ require "uri"
|
|
|
8
8
|
require "concurrent/atomic/atomic_fixnum"
|
|
9
9
|
require "opentelemetry/sdk"
|
|
10
10
|
|
|
11
|
+
require_relative "client_headers"
|
|
11
12
|
require_relative "sampler"
|
|
12
13
|
require_relative "trace_blob"
|
|
13
14
|
require_relative "http_transport"
|
|
15
|
+
require_relative "deadline"
|
|
14
16
|
|
|
15
17
|
module Flare
|
|
16
18
|
# Custom OTel exporter. For each batch FilteringSpanProcessor hands over:
|
|
@@ -32,6 +34,7 @@ module Flare
|
|
|
32
34
|
class TraceExporter
|
|
33
35
|
SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS
|
|
34
36
|
FAILURE = OpenTelemetry::SDK::Trace::Export::FAILURE
|
|
37
|
+
TIMEOUT = OpenTelemetry::SDK::Trace::Export::TIMEOUT
|
|
35
38
|
|
|
36
39
|
PUT_HEADERS = {
|
|
37
40
|
"Content-Type" => "application/json",
|
|
@@ -57,16 +60,22 @@ module Flare
|
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
def export(spans, timeout: nil)
|
|
63
|
+
deadline = Deadline.new(timeout)
|
|
60
64
|
grouped = spans.group_by(&:trace_id)
|
|
61
65
|
return SUCCESS if grouped.empty?
|
|
62
66
|
|
|
63
67
|
overall = SUCCESS
|
|
64
68
|
grouped.each do |trace_id, group|
|
|
65
|
-
|
|
69
|
+
return TIMEOUT if deadline.expired?
|
|
70
|
+
|
|
71
|
+
result = ship(TraceBlob.build(trace_id: trace_id, spans: group), deadline: deadline)
|
|
72
|
+
return TIMEOUT if result == TIMEOUT || deadline.expired?
|
|
66
73
|
overall = FAILURE if result == FAILURE
|
|
67
74
|
end
|
|
68
75
|
overall
|
|
69
76
|
rescue StandardError => e
|
|
77
|
+
return TIMEOUT if deadline&.expired? || e.is_a?(HttpTransport::DeadlineExceeded)
|
|
78
|
+
|
|
70
79
|
@exception_count.increment
|
|
71
80
|
@logger.warn("[Flare::TraceExporter] export raised: #{e.class}: #{e.message}")
|
|
72
81
|
FAILURE
|
|
@@ -82,8 +91,9 @@ module Flare
|
|
|
82
91
|
|
|
83
92
|
private
|
|
84
93
|
|
|
85
|
-
def ship(blob, retried: false)
|
|
94
|
+
def ship(blob, deadline:, retried: false)
|
|
86
95
|
return FAILURE if blob.nil?
|
|
96
|
+
return TIMEOUT if deadline.expired?
|
|
87
97
|
|
|
88
98
|
entry = @pool.checkout
|
|
89
99
|
if entry.nil?
|
|
@@ -92,38 +102,67 @@ module Flare
|
|
|
92
102
|
end
|
|
93
103
|
|
|
94
104
|
body = gzip(JSON.generate(blob.to_h))
|
|
95
|
-
response =
|
|
105
|
+
response = transport_request(:put, entry[:put_url], body, PUT_HEADERS, timeout: deadline.remaining)
|
|
106
|
+
return TIMEOUT if deadline.expired?
|
|
96
107
|
|
|
97
108
|
case response.code
|
|
98
109
|
when "200", "204"
|
|
99
|
-
notify(entry[:key])
|
|
100
|
-
SUCCESS
|
|
110
|
+
notify(entry[:key], deadline: deadline)
|
|
101
111
|
when "403"
|
|
102
112
|
# Presigned URL probably expired; try once more with the next one.
|
|
103
|
-
retried ? record_put_failure(response) : ship(blob, retried: true)
|
|
113
|
+
retried ? record_put_failure(response) : ship(blob, deadline: deadline, retried: true)
|
|
104
114
|
else
|
|
105
115
|
record_put_failure(response)
|
|
106
116
|
end
|
|
107
117
|
end
|
|
108
118
|
|
|
109
|
-
def notify(key)
|
|
110
|
-
|
|
111
|
-
|
|
119
|
+
def notify(key, deadline:)
|
|
120
|
+
return TIMEOUT if deadline.expired?
|
|
121
|
+
|
|
122
|
+
response = transport_request(
|
|
123
|
+
:post,
|
|
124
|
+
@notify_url,
|
|
125
|
+
JSON.generate(key: key),
|
|
126
|
+
notify_headers,
|
|
127
|
+
timeout: deadline.remaining
|
|
128
|
+
)
|
|
129
|
+
return TIMEOUT if deadline.expired?
|
|
130
|
+
return SUCCESS if response.code == "202"
|
|
112
131
|
|
|
113
132
|
@notify_failure_count.increment
|
|
114
133
|
@logger.warn("[Flare::TraceExporter] notify failed: HTTP #{response.code}")
|
|
134
|
+
SUCCESS
|
|
115
135
|
rescue StandardError => e
|
|
136
|
+
return TIMEOUT if deadline.expired? || e.is_a?(HttpTransport::DeadlineExceeded)
|
|
137
|
+
|
|
116
138
|
@notify_failure_count.increment
|
|
117
139
|
@logger.warn("[Flare::TraceExporter] notify exception: #{e.class}: #{e.message}")
|
|
140
|
+
SUCCESS
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def transport_request(method, *args, timeout:)
|
|
144
|
+
parameters = @transport.method(method).parameters
|
|
145
|
+
accepts_timeout = parameters.any? do |type, name|
|
|
146
|
+
type == :keyrest || ([:key, :keyreq].include?(type) && name == :timeout)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
if accepts_timeout
|
|
150
|
+
@transport.public_send(method, *args, timeout: timeout)
|
|
151
|
+
else
|
|
152
|
+
@transport.public_send(method, *args)
|
|
153
|
+
end
|
|
118
154
|
end
|
|
119
155
|
|
|
156
|
+
# Identifies the client on the Flare-API notify POST. The presigned R2
|
|
157
|
+
# PUT in #ship deliberately uses PUT_HEADERS only -- adding these there
|
|
158
|
+
# could invalidate the signed-header set.
|
|
120
159
|
def notify_headers
|
|
121
|
-
|
|
160
|
+
ClientHeaders.to_h.merge(
|
|
122
161
|
"Content-Type" => "application/json",
|
|
123
162
|
"Authorization" => "Bearer #{@api_key}",
|
|
124
163
|
"Flare-Project" => @project,
|
|
125
164
|
"Flare-Environment" => @environment
|
|
126
|
-
|
|
165
|
+
)
|
|
127
166
|
end
|
|
128
167
|
|
|
129
168
|
def record_put_failure(response)
|
data/lib/flare/version.rb
CHANGED
data/lib/flare.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "flare/configuration"
|
|
|
5
5
|
|
|
6
6
|
require "opentelemetry/sdk"
|
|
7
7
|
|
|
8
|
+
require_relative "flare/client_headers"
|
|
8
9
|
require_relative "flare/source_location"
|
|
9
10
|
require_relative "flare/metric_key"
|
|
10
11
|
require_relative "flare/metric_storage"
|
|
@@ -12,6 +13,8 @@ require_relative "flare/metric_span_processor"
|
|
|
12
13
|
require_relative "flare/metric_flusher"
|
|
13
14
|
require_relative "flare/backoff_policy"
|
|
14
15
|
require_relative "flare/metric_submitter"
|
|
16
|
+
require_relative "flare/recording_batch_span_processor"
|
|
17
|
+
require_relative "flare/lifecycle"
|
|
15
18
|
|
|
16
19
|
require_relative "flare/sampler"
|
|
17
20
|
require_relative "flare/marker"
|
|
@@ -87,7 +90,7 @@ module Flare
|
|
|
87
90
|
end
|
|
88
91
|
|
|
89
92
|
def span_processor
|
|
90
|
-
@span_processor ||=
|
|
93
|
+
@span_processor ||= RecordingBatchSpanProcessor.new(
|
|
91
94
|
exporter,
|
|
92
95
|
max_queue_size: 1000,
|
|
93
96
|
max_export_batch_size: 100,
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: flare
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- John Nunemaker
|
|
@@ -196,11 +196,14 @@ files:
|
|
|
196
196
|
- lib/flare/cli/output.rb
|
|
197
197
|
- lib/flare/cli/setup_command.rb
|
|
198
198
|
- lib/flare/cli/status_command.rb
|
|
199
|
+
- lib/flare/client_headers.rb
|
|
199
200
|
- lib/flare/configuration.rb
|
|
201
|
+
- lib/flare/deadline.rb
|
|
200
202
|
- lib/flare/engine.rb
|
|
201
203
|
- lib/flare/filtering_span_processor.rb
|
|
202
204
|
- lib/flare/http_metrics_config.rb
|
|
203
205
|
- lib/flare/http_transport.rb
|
|
206
|
+
- lib/flare/lifecycle.rb
|
|
204
207
|
- lib/flare/marker.rb
|
|
205
208
|
- lib/flare/metric_counter.rb
|
|
206
209
|
- lib/flare/metric_flusher.rb
|
|
@@ -208,6 +211,7 @@ files:
|
|
|
208
211
|
- lib/flare/metric_span_processor.rb
|
|
209
212
|
- lib/flare/metric_storage.rb
|
|
210
213
|
- lib/flare/metric_submitter.rb
|
|
214
|
+
- lib/flare/recording_batch_span_processor.rb
|
|
211
215
|
- lib/flare/rule_manager.rb
|
|
212
216
|
- lib/flare/sampler.rb
|
|
213
217
|
- lib/flare/source_location.rb
|
|
@@ -244,7 +248,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
244
248
|
- !ruby/object:Gem::Version
|
|
245
249
|
version: '0'
|
|
246
250
|
requirements: []
|
|
247
|
-
rubygems_version: 4.0.
|
|
251
|
+
rubygems_version: 4.0.18
|
|
248
252
|
specification_version: 4
|
|
249
253
|
summary: Light up what's slowing you down
|
|
250
254
|
test_files: []
|