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.
@@ -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
- setup_database unless @setup
30
+ detect_forking
31
+ deadline = Deadline.new(timeout)
27
32
 
28
33
  retries = 0
29
34
  exported = 0
30
35
 
31
36
  begin
32
- @mutex.synchronize do
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
- sleep 0.1 * retries
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
- @mutex.synchronize do
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.execute(<<~SQL)
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.execute(<<~SQL)
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.execute(<<~SQL)
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.execute(<<~SQL)
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.execute(<<~SQL)
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.execute(<<~SQL)
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.execute(<<~SQL)
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.execute(<<~SQL)
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.execute(<<~SQL)
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.execute(<<~SQL)
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.execute("PRAGMA journal_mode=WAL")
251
- db.execute("PRAGMA synchronous=NORMAL")
252
- db.execute("PRAGMA mmap_size=134217728") # 128MB
253
- db.execute("PRAGMA journal_size_limit=67108864") # 64MB
254
- db.execute("PRAGMA cache_size=2000")
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
@@ -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
- result = ship(TraceBlob.build(trace_id: trace_id, spans: group))
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 = @transport.put(entry[:put_url], body, PUT_HEADERS)
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
- response = @transport.post(@notify_url, JSON.generate(key: key), notify_headers)
111
- return if response.code == "202"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Flare
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
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 ||= OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
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.3.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.7
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: []