Class: Flare::Storage::SQLite

Inherits:
Base
  • Object
show all
Defined in:
lib/flare/storage/sqlite.rb

Constant Summary collapse

MISSING_PARENT_ID =
"0000000000000000"
RAILS_CONTROLLER_PREFIXES =

Rails framework controller prefixes to filter (lowercase/underscored format)

%w[
  active_storage/
  action_mailbox/
  rails/
  flare/
].freeze
SPAN_CATEGORIES =

Span category patterns for filtering

{
  "queries" => ["sql.active_record", "mysql", "postgres", "sqlite"],
  "cache" => ["cache_read.active_support", "cache_write.active_support", "cache_delete.active_support", "cache_exist?.active_support", "cache_fetch_hit.active_support"],
  "views" => ["render_template.action_view", "render_partial.action_view", "render_layout.action_view", "render_collection.action_view"],
  "http" => ["HTTP", "net_http"],
  "mail" => ["deliver.action_mailer", "process.action_mailer"],
  "redis" => ["redis"],
  "exceptions" => [] # Handled specially via events
}.freeze
TRANSACTION_STATEMENTS =

Transaction statements to filter out from queries list

%w[BEGIN COMMIT ROLLBACK].freeze

Instance Method Summary collapse

Methods inherited from Base

#clues_for_case, #count_cases, #count_clues, #find_case, #find_clue, #list_cases, #list_clues, #save_case, #save_clues

Constructor Details

#initialize(database_path) ⇒ SQLite

Returns a new instance of SQLite.



19
20
21
22
23
# File 'lib/flare/storage/sqlite.rb', line 19

def initialize(database_path)
  @database_path = database_path
  @mutex = Mutex.new
  @setup = false
end

Instance Method Details

#clear_allObject



607
608
609
610
611
# File 'lib/flare/storage/sqlite.rb', line 607

def clear_all
  execute("DELETE FROM flare_properties")
  execute("DELETE FROM flare_events")
  execute("DELETE FROM flare_spans")
end

#count_exception_spans(name: nil) ⇒ Object



310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/flare/storage/sqlite.rb', line 310

def count_exception_spans(name: nil)
  conditions = []
  values = []

  if name
    conditions << "e.name LIKE ?"
    values << "%#{name}%"
  end

  where_clause = conditions.any? ? "WHERE #{conditions.join(" AND ")}" : ""

  row = query_one(<<~SQL, values)
    SELECT COUNT(DISTINCT s.id) as count
    FROM flare_events e
    JOIN flare_spans s ON s.id = e.span_id
    #{where_clause}
  SQL

  row ? row["count"] : 0
end

#count_jobs(status: nil, name: nil) ⇒ Object



331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/flare/storage/sqlite.rb', line 331

def count_jobs(status: nil, name: nil)
  conditions = ["s.parent_span_id = ?", "s.kind = ?"]
  values = [MISSING_PARENT_ID, "consumer"]

  if name
    conditions << "s.name LIKE ?"
    values << "%#{name}%"
  end

  where_clause = "WHERE #{conditions.join(" AND ")}"

  row = query_one(<<~SQL, values)
    SELECT COUNT(*) as count
    FROM flare_spans s
    #{where_clause}
  SQL

  row ? row["count"] : 0
end

#count_requests(status: nil, method: nil, name: nil, origin: nil) ⇒ Object



351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'lib/flare/storage/sqlite.rb', line 351

def count_requests(status: nil, method: nil, name: nil, origin: nil)
  conditions = ["s.parent_span_id = ?", "s.kind = ?"]
  values = [MISSING_PARENT_ID, "server"]

  if status
    case status
    when "2xx"
      conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
      values << "2%"
    when "3xx"
      conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
      values << "3%"
    when "4xx"
      conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
      values << "4%"
    when "5xx"
      conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
      values << "5%"
    else
      conditions << "COALESCE(status_prop.value, status_prop_old.value) = ?"
      values << status.to_s
    end
  end

  if method
    conditions << "COALESCE(method_prop.value, method_prop_old.value) = ?"
    values << "\"#{method}\""
  end

  if name
    conditions << "s.name LIKE ?"
    values << "%#{name}%"
  end

  if origin
    if origin == "rails"
      controller_conditions = RAILS_CONTROLLER_PREFIXES.map { "controller_prop.value LIKE ?" }
      conditions << "(#{controller_conditions.join(" OR ")})"
      RAILS_CONTROLLER_PREFIXES.each { |prefix| values << "%#{prefix}%" }
    elsif origin == "app"
      controller_conditions = RAILS_CONTROLLER_PREFIXES.map { "controller_prop.value LIKE ?" }
      conditions << "(controller_prop.value IS NULL OR NOT (#{controller_conditions.join(" OR ")}))"
      RAILS_CONTROLLER_PREFIXES.each { |prefix| values << "%#{prefix}%" }
    end
  end

  where_clause = "WHERE #{conditions.join(" AND ")}"

  row = query_one(<<~SQL, values)
    SELECT COUNT(*) as count
    FROM flare_spans s
    LEFT JOIN flare_properties method_prop ON method_prop.owner_type = 'Flare::Span' AND method_prop.owner_id = s.id AND method_prop.key = 'http.request.method'
    LEFT JOIN flare_properties method_prop_old ON method_prop_old.owner_type = 'Flare::Span' AND method_prop_old.owner_id = s.id AND method_prop_old.key = 'http.method'
    LEFT JOIN flare_properties status_prop ON status_prop.owner_type = 'Flare::Span' AND status_prop.owner_id = s.id AND status_prop.key = 'http.response.status_code'
    LEFT JOIN flare_properties status_prop_old ON status_prop_old.owner_type = 'Flare::Span' AND status_prop_old.owner_id = s.id AND status_prop_old.key = 'http.status_code'
    LEFT JOIN flare_properties controller_prop ON controller_prop.owner_type = 'Flare::Span' AND controller_prop.owner_id = s.id AND controller_prop.key = 'code.namespace'
    #{where_clause}
    AND COALESCE(method_prop.value, method_prop_old.value) IS NOT NULL
  SQL

  row ? row["count"] : 0
end

#count_spans_by_category(category, name: nil) ⇒ Object



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/flare/storage/sqlite.rb', line 205

def count_spans_by_category(category, name: nil)
  patterns = SPAN_CATEGORIES[category] || []
  return 0 if patterns.empty? && category != "exceptions"

  if category == "exceptions"
    return count_exception_spans(name: name)
  end

  conditions = ["s.parent_span_id != ?"]
  values = [MISSING_PARENT_ID]

  pattern_conditions = patterns.map { "s.name LIKE ?" }
  conditions << "(#{pattern_conditions.join(" OR ")})"
  patterns.each { |p| values << "%#{p}%" }

  if name
    conditions << "s.name LIKE ?"
    values << "%#{name}%"
  end

  # For queries, exclude transaction statements (BEGIN, COMMIT, ROLLBACK)
  transaction_join = ""
  if category == "queries"
    transaction_join = "LEFT JOIN flare_properties stmt_prop ON stmt_prop.owner_type = 'Flare::Span' AND stmt_prop.owner_id = s.id AND stmt_prop.key = 'db.statement'"
    exclusions = TRANSACTION_STATEMENTS.map { "?" }.join(", ")
    conditions << "(stmt_prop.value IS NULL OR stmt_prop.value NOT IN (#{exclusions}))"
    TRANSACTION_STATEMENTS.each { |stmt| values << "\"#{stmt}\"" }
  end

  where_clause = "WHERE #{conditions.join(" AND ")}"

  row = query_one(<<~SQL, values)
    SELECT COUNT(*) as count
    FROM flare_spans s
    #{transaction_join}
    #{where_clause}
  SQL

  row ? row["count"] : 0
end

#find_job(trace_id) ⇒ Object

Find a job by trace_id (for the detail view)



415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'lib/flare/storage/sqlite.rb', line 415

def find_job(trace_id)
  row = query_one(<<~SQL, [trace_id, MISSING_PARENT_ID, "consumer"])
    SELECT s.*
    FROM flare_spans s
    WHERE s.trace_id = ? AND s.parent_span_id = ? AND s.kind = ?
  SQL

  return nil unless row

  span = row_to_span(row)
  span[:properties] = load_properties("Flare::Span", span[:id])
  span
end

#find_request(trace_id) ⇒ Object

Find a request by trace_id (for the detail view)



430
431
432
433
434
435
436
437
438
439
440
441
442
# File 'lib/flare/storage/sqlite.rb', line 430

def find_request(trace_id)
  row = query_one(<<~SQL, [trace_id, MISSING_PARENT_ID])
    SELECT s.*
    FROM flare_spans s
    WHERE s.trace_id = ? AND s.parent_span_id = ?
  SQL

  return nil unless row

  span = row_to_span(row)
  span[:properties] = load_properties("Flare::Span", span[:id])
  span
end

#find_span(id) ⇒ Object

Find a single span by its database ID



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/flare/storage/sqlite.rb', line 247

def find_span(id)
  row = query_one(<<~SQL, [id])
    SELECT s.*,
           root.trace_id as root_trace_id,
           root.name as root_name,
           root.kind as root_kind,
           root_controller.value as root_controller,
           root_action.value as root_action
    FROM flare_spans s
    LEFT JOIN flare_spans root ON root.trace_id = s.trace_id AND root.parent_span_id = '#{MISSING_PARENT_ID}'
    LEFT JOIN flare_properties root_controller ON root_controller.owner_type = 'Flare::Span' AND root_controller.owner_id = root.id AND root_controller.key = 'code.namespace'
    LEFT JOIN flare_properties root_action ON root_action.owner_type = 'Flare::Span' AND root_action.owner_id = root.id AND root_action.key = 'code.function'
    WHERE s.id = ?
  SQL

  return nil unless row

  span = row_to_span_with_root(row)
  span[:properties] = load_properties("Flare::Span", span[:id])
  span[:events] = load_events_for_spans([span[:id]])[span[:id]] || []
  span
end

#list_exception_spans(name: nil, limit: 50, offset: 0) ⇒ Object

List spans that have exception events



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/flare/storage/sqlite.rb', line 271

def list_exception_spans(name: nil, limit: 50, offset: 0)
  conditions = []
  values = []

  if name
    conditions << "e.name LIKE ?"
    values << "%#{name}%"
  end

  where_clause = conditions.any? ? "WHERE #{conditions.join(" AND ")}" : ""
  values << limit
  values << offset

  rows = query_all(<<~SQL, values)
    SELECT DISTINCT s.*,
           root.trace_id as root_trace_id,
           root.name as root_name,
           root.kind as root_kind,
           root_controller.value as root_controller,
           root_action.value as root_action,
           exc_type.value as exception_type,
           exc_message.value as exception_message,
           exc_stacktrace.value as exception_stacktrace
    FROM flare_events e
    JOIN flare_spans s ON s.id = e.span_id
    LEFT JOIN flare_spans root ON root.trace_id = s.trace_id AND root.parent_span_id = '#{MISSING_PARENT_ID}'
    LEFT JOIN flare_properties root_controller ON root_controller.owner_type = 'Flare::Span' AND root_controller.owner_id = root.id AND root_controller.key = 'code.namespace'
    LEFT JOIN flare_properties root_action ON root_action.owner_type = 'Flare::Span' AND root_action.owner_id = root.id AND root_action.key = 'code.function'
    LEFT JOIN flare_properties exc_type ON exc_type.owner_type = 'Flare::Event' AND exc_type.owner_id = e.id AND exc_type.key = 'exception.type'
    LEFT JOIN flare_properties exc_message ON exc_message.owner_type = 'Flare::Event' AND exc_message.owner_id = e.id AND exc_message.key = 'exception.message'
    LEFT JOIN flare_properties exc_stacktrace ON exc_stacktrace.owner_type = 'Flare::Event' AND exc_stacktrace.owner_id = e.id AND exc_stacktrace.key = 'exception.stacktrace'
    #{where_clause}
    ORDER BY e.created_at DESC
    LIMIT ? OFFSET ?
  SQL

  rows.map { |row| row_to_span_with_root(row) }
end

#list_jobs(status: nil, name: nil, limit: 50, offset: 0) ⇒ Object

List root spans that are jobs (for the jobs index)



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/flare/storage/sqlite.rb', line 106

def list_jobs(status: nil, name: nil, limit: 50, offset: 0)
  # Find root spans with kind=consumer (ActiveJob processing)
  conditions = ["s.parent_span_id = ?", "s.kind = ?"]
  values = [MISSING_PARENT_ID, "consumer"]

  if name
    conditions << "s.name LIKE ?"
    values << "%#{name}%"
  end

  where_clause = "WHERE #{conditions.join(" AND ")}"
  values << limit
  values << offset

  rows = query_all(<<~SQL, values)
    SELECT s.*,
           job_class_prop.value as job_class,
           queue_prop.value as queue_name
    FROM flare_spans s
    LEFT JOIN flare_properties job_class_prop ON job_class_prop.owner_type = 'Flare::Span' AND job_class_prop.owner_id = s.id AND job_class_prop.key = 'code.namespace'
    LEFT JOIN flare_properties queue_prop ON queue_prop.owner_type = 'Flare::Span' AND queue_prop.owner_id = s.id AND queue_prop.key = 'messaging.destination'
    #{where_clause}
    ORDER BY s.created_at DESC
    LIMIT ? OFFSET ?
  SQL

  rows.map { |row| row_to_job(row) }
end

#list_requests(status: nil, method: nil, name: nil, origin: nil, limit: 50, offset: 0) ⇒ Object

List root spans that are HTTP requests (for the requests index)



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/flare/storage/sqlite.rb', line 26

def list_requests(status: nil, method: nil, name: nil, origin: nil, limit: 50, offset: 0)
  # Find root spans with kind=server that have http.method property
  conditions = ["s.parent_span_id = ?", "s.kind = ?"]
  values = [MISSING_PARENT_ID, "server"]

  # Filter by http.method property existing (makes it an HTTP request)
  # We join with properties to filter

  if status
    case status
    when "2xx"
      conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
      values << "2%"
    when "3xx"
      conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
      values << "3%"
    when "4xx"
      conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
      values << "4%"
    when "5xx"
      conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
      values << "5%"
    else
      conditions << "COALESCE(status_prop.value, status_prop_old.value) = ?"
      values << status.to_s
    end
  end

  if method
    conditions << "COALESCE(method_prop.value, method_prop_old.value) = ?"
    values << "\"#{method}\""
  end

  if name
    conditions << "s.name LIKE ?"
    values << "%#{name}%"
  end

  if origin
    if origin == "rails"
      controller_conditions = RAILS_CONTROLLER_PREFIXES.map { "controller_prop.value LIKE ?" }
      conditions << "(#{controller_conditions.join(" OR ")})"
      RAILS_CONTROLLER_PREFIXES.each { |prefix| values << "%#{prefix}%" }
    elsif origin == "app"
      controller_conditions = RAILS_CONTROLLER_PREFIXES.map { "controller_prop.value LIKE ?" }
      conditions << "(controller_prop.value IS NULL OR NOT (#{controller_conditions.join(" OR ")}))"
      RAILS_CONTROLLER_PREFIXES.each { |prefix| values << "%#{prefix}%" }
    end
  end

  where_clause = "WHERE #{conditions.join(" AND ")}"
  values << limit
  values << offset

  rows = query_all(<<~SQL, values)
    SELECT s.*,
           COALESCE(method_prop.value, method_prop_old.value) as http_method,
           COALESCE(status_prop.value, status_prop_old.value) as http_status,
           COALESCE(target_prop.value, target_prop_old.value) as http_target,
           controller_prop.value as controller,
           action_prop.value as action
    FROM flare_spans s
    LEFT JOIN flare_properties method_prop ON method_prop.owner_type = 'Flare::Span' AND method_prop.owner_id = s.id AND method_prop.key = 'http.request.method'
    LEFT JOIN flare_properties method_prop_old ON method_prop_old.owner_type = 'Flare::Span' AND method_prop_old.owner_id = s.id AND method_prop_old.key = 'http.method'
    LEFT JOIN flare_properties status_prop ON status_prop.owner_type = 'Flare::Span' AND status_prop.owner_id = s.id AND status_prop.key = 'http.response.status_code'
    LEFT JOIN flare_properties status_prop_old ON status_prop_old.owner_type = 'Flare::Span' AND status_prop_old.owner_id = s.id AND status_prop_old.key = 'http.status_code'
    LEFT JOIN flare_properties target_prop ON target_prop.owner_type = 'Flare::Span' AND target_prop.owner_id = s.id AND target_prop.key = 'url.path'
    LEFT JOIN flare_properties target_prop_old ON target_prop_old.owner_type = 'Flare::Span' AND target_prop_old.owner_id = s.id AND target_prop_old.key = 'http.target'
    LEFT JOIN flare_properties controller_prop ON controller_prop.owner_type = 'Flare::Span' AND controller_prop.owner_id = s.id AND controller_prop.key = 'code.namespace'
    LEFT JOIN flare_properties action_prop ON action_prop.owner_type = 'Flare::Span' AND action_prop.owner_id = s.id AND action_prop.key = 'code.function'
    #{where_clause}
    AND COALESCE(method_prop.value, method_prop_old.value) IS NOT NULL
    ORDER BY s.created_at DESC
    LIMIT ? OFFSET ?
  SQL

  rows.map { |row| row_to_request(row) }
end

#list_spans_by_category(category, name: nil, limit: 50, offset: 0) ⇒ Object

List spans by category (for the spans listing pages)



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/flare/storage/sqlite.rb', line 150

def list_spans_by_category(category, name: nil, limit: 50, offset: 0)
  patterns = SPAN_CATEGORIES[category] || []
  return [] if patterns.empty? && category != "exceptions"

  if category == "exceptions"
    # Exceptions are stored as events on spans, not as spans themselves
    return list_exception_spans(name: name, limit: limit, offset: offset)
  end

  conditions = ["s.parent_span_id != ?"]
  values = [MISSING_PARENT_ID]

  # Build OR conditions for matching span names
  pattern_conditions = patterns.map { "s.name LIKE ?" }
  conditions << "(#{pattern_conditions.join(" OR ")})"
  patterns.each { |p| values << "%#{p}%" }

  if name
    conditions << "s.name LIKE ?"
    values << "%#{name}%"
  end

  # For queries, exclude transaction statements (BEGIN, COMMIT, ROLLBACK)
  transaction_join = ""
  if category == "queries"
    transaction_join = "LEFT JOIN flare_properties stmt_prop ON stmt_prop.owner_type = 'Flare::Span' AND stmt_prop.owner_id = s.id AND stmt_prop.key = 'db.statement'"
    exclusions = TRANSACTION_STATEMENTS.map { "?" }.join(", ")
    conditions << "(stmt_prop.value IS NULL OR stmt_prop.value NOT IN (#{exclusions}))"
    TRANSACTION_STATEMENTS.each { |stmt| values << "\"#{stmt}\"" }
  end

  where_clause = "WHERE #{conditions.join(" AND ")}"
  values << limit
  values << offset

  rows = query_all(<<~SQL, values)
    SELECT s.*,
           root.trace_id as root_trace_id,
           root.name as root_name,
           root.kind as root_kind,
           root_controller.value as root_controller,
           root_action.value as root_action
    FROM flare_spans s
    LEFT JOIN flare_spans root ON root.trace_id = s.trace_id AND root.parent_span_id = '#{MISSING_PARENT_ID}'
    LEFT JOIN flare_properties root_controller ON root_controller.owner_type = 'Flare::Span' AND root_controller.owner_id = root.id AND root_controller.key = 'code.namespace'
    LEFT JOIN flare_properties root_action ON root_action.owner_type = 'Flare::Span' AND root_action.owner_id = root.id AND root_action.key = 'code.function'
    #{transaction_join}
    #{where_clause}
    ORDER BY s.created_at DESC
    LIMIT ? OFFSET ?
  SQL

  rows.map { |row| row_to_span_with_root(row) }
end

#load_events_for_spans(span_ids) ⇒ Object

Load events for multiple spans at once



504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'lib/flare/storage/sqlite.rb', line 504

def load_events_for_spans(span_ids)
  return {} if span_ids.empty?

  placeholders = span_ids.map { "?" }.join(", ")
  event_rows = query_all(<<~SQL, span_ids)
    SELECT * FROM flare_events
    WHERE span_id IN (#{placeholders})
  SQL

  # Group events by span_id
  events_by_span = Hash.new { |h, k| h[k] = [] }
  event_ids = []

  event_rows.each do |row|
    event = {
      id: row["id"],
      span_id: row["span_id"],
      name: row["name"],
      created_at: row["created_at"]
    }
    events_by_span[row["span_id"]] << event
    event_ids << row["id"]
  end

  # Load properties for all events
  if event_ids.any?
    event_properties = load_properties_for_ids("Flare::Event", event_ids)
    events_by_span.each do |_, events|
      events.each do |event|
        event[:properties] = event_properties[event[:id]] || {}
      end
    end
  end

  events_by_span
end

#load_properties(owner_type, owner_id) ⇒ Object

Load properties for a specific owner



475
476
477
478
479
480
481
482
483
484
# File 'lib/flare/storage/sqlite.rb', line 475

def load_properties(owner_type, owner_id)
  rows = query_all(<<~SQL, [owner_type, owner_id])
    SELECT key, value, value_type FROM flare_properties
    WHERE owner_type = ? AND owner_id = ?
  SQL

  rows.each_with_object({}) do |row, hash|
    hash[row["key"]] = parse_property_value(row["value"], row["value_type"])
  end
end

#load_properties_for_ids(owner_type, owner_ids) ⇒ Object

Load properties for multiple owners at once



487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
# File 'lib/flare/storage/sqlite.rb', line 487

def load_properties_for_ids(owner_type, owner_ids)
  return {} if owner_ids.empty?

  placeholders = owner_ids.map { "?" }.join(", ")
  rows = query_all(<<~SQL, [owner_type] + owner_ids)
    SELECT owner_id, key, value, value_type FROM flare_properties
    WHERE owner_type = ? AND owner_id IN (#{placeholders})
  SQL

  result = Hash.new { |h, k| h[k] = {} }
  rows.each do |row|
    result[row["owner_id"]][row["key"]] = parse_property_value(row["value"], row["value_type"])
  end
  result
end

#prune(retention_hours:, max_spans:) ⇒ Object



541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
# File 'lib/flare/storage/sqlite.rb', line 541

def prune(retention_hours:, max_spans:)
  cutoff = (Time.now - (retention_hours * 3600)).iso8601(6)

  # Delete old properties first (for old spans and events)
  execute(<<~SQL, [cutoff])
    DELETE FROM flare_properties WHERE owner_type = 'Flare::Span' AND owner_id IN (
      SELECT id FROM flare_spans WHERE created_at < ?
    )
  SQL

  execute(<<~SQL, [cutoff])
    DELETE FROM flare_properties WHERE owner_type = 'Flare::Event' AND owner_id IN (
      SELECT id FROM flare_events WHERE span_id IN (
        SELECT id FROM flare_spans WHERE created_at < ?
      )
    )
  SQL

  # Delete old events
  execute(<<~SQL, [cutoff])
    DELETE FROM flare_events WHERE span_id IN (
      SELECT id FROM flare_spans WHERE created_at < ?
    )
  SQL

  # Delete old spans
  execute(<<~SQL, [cutoff])
    DELETE FROM flare_spans WHERE created_at < ?
  SQL

  # Also prune if over max_spans (keep newest)
  execute(<<~SQL, [max_spans])
    DELETE FROM flare_properties WHERE owner_type = 'Flare::Span' AND owner_id IN (
      SELECT id FROM flare_spans
      ORDER BY created_at DESC
      LIMIT -1 OFFSET ?
    )
  SQL

  execute(<<~SQL, [max_spans])
    DELETE FROM flare_properties WHERE owner_type = 'Flare::Event' AND owner_id IN (
      SELECT id FROM flare_events WHERE span_id IN (
        SELECT id FROM flare_spans
        ORDER BY created_at DESC
        LIMIT -1 OFFSET ?
      )
    )
  SQL

  execute(<<~SQL, [max_spans])
    DELETE FROM flare_events WHERE span_id IN (
      SELECT id FROM flare_spans
      ORDER BY created_at DESC
      LIMIT -1 OFFSET ?
    )
  SQL

  execute(<<~SQL, [max_spans])
    DELETE FROM flare_spans WHERE id IN (
      SELECT id FROM flare_spans
      ORDER BY created_at DESC
      LIMIT -1 OFFSET ?
    )
  SQL
end

#spans_for_trace(trace_id) ⇒ Object

Get all spans for a trace (for the waterfall view)



445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# File 'lib/flare/storage/sqlite.rb', line 445

def spans_for_trace(trace_id)
  rows = query_all(<<~SQL, [trace_id])
    SELECT * FROM flare_spans
    WHERE trace_id = ?
    ORDER BY start_timestamp ASC
  SQL

  spans = rows.map { |row| row_to_span(row) }

  # Load properties for all spans
  span_ids = spans.map { |s| s[:id] }
  if span_ids.any?
    all_properties = load_properties_for_ids("Flare::Span", span_ids)
    spans.each do |span|
      span[:properties] = all_properties[span[:id]] || {}
    end
  end

  # Load events for all spans
  if span_ids.any?
    all_events = load_events_for_spans(span_ids)
    spans.each do |span|
      span[:events] = all_events[span[:id]] || []
    end
  end

  spans
end