flare 0.4.0 → 1.0.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.

Potentially problematic release.


This version of flare might be problematic. Click here for more details.

Files changed (70) hide show
  1. data/.document +5 -0
  2. data/.gitignore +22 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +18 -0
  5. data/Rakefile +55 -0
  6. data/VERSION +1 -0
  7. data/flare.gemspec +66 -0
  8. data/lib/flare/active_record.rb +102 -0
  9. data/lib/flare/collection.rb +47 -0
  10. data/lib/flare/configuration.rb +64 -123
  11. data/lib/flare/index_builder.rb +24 -0
  12. data/lib/flare/session.rb +142 -0
  13. data/lib/flare/tasks.rb +18 -0
  14. data/lib/flare.rb +19 -537
  15. data/test/helper.rb +10 -0
  16. data/test/test_flare.rb +7 -0
  17. metadata +89 -242
  18. checksums.yaml +0 -7
  19. data/CHANGELOG.md +0 -3
  20. data/LICENSE.txt +0 -21
  21. data/README.md +0 -148
  22. data/app/controllers/flare/application_controller.rb +0 -30
  23. data/app/controllers/flare/jobs_controller.rb +0 -55
  24. data/app/controllers/flare/requests_controller.rb +0 -73
  25. data/app/controllers/flare/spans_controller.rb +0 -101
  26. data/app/helpers/flare/application_helper.rb +0 -168
  27. data/app/views/flare/jobs/index.html.erb +0 -69
  28. data/app/views/flare/jobs/show.html.erb +0 -323
  29. data/app/views/flare/requests/index.html.erb +0 -120
  30. data/app/views/flare/requests/show.html.erb +0 -498
  31. data/app/views/flare/spans/index.html.erb +0 -112
  32. data/app/views/flare/spans/show.html.erb +0 -184
  33. data/app/views/layouts/flare/application.html.erb +0 -126
  34. data/config/routes.rb +0 -20
  35. data/exe/flare +0 -9
  36. data/lib/flare/backoff_policy.rb +0 -73
  37. data/lib/flare/cli/doctor_command.rb +0 -129
  38. data/lib/flare/cli/output.rb +0 -45
  39. data/lib/flare/cli/setup_command.rb +0 -404
  40. data/lib/flare/cli/status_command.rb +0 -47
  41. data/lib/flare/cli.rb +0 -50
  42. data/lib/flare/client_headers.rb +0 -39
  43. data/lib/flare/deadline.rb +0 -27
  44. data/lib/flare/engine.rb +0 -45
  45. data/lib/flare/filtering_span_processor.rb +0 -429
  46. data/lib/flare/http_metrics_config.rb +0 -101
  47. data/lib/flare/http_transport.rb +0 -76
  48. data/lib/flare/lifecycle.rb +0 -39
  49. data/lib/flare/marker.rb +0 -106
  50. data/lib/flare/metric_counter.rb +0 -51
  51. data/lib/flare/metric_flusher.rb +0 -279
  52. data/lib/flare/metric_key.rb +0 -42
  53. data/lib/flare/metric_span_processor.rb +0 -470
  54. data/lib/flare/metric_storage.rb +0 -67
  55. data/lib/flare/metric_submitter.rb +0 -239
  56. data/lib/flare/recording_batch_span_processor.rb +0 -238
  57. data/lib/flare/rule_manager.rb +0 -141
  58. data/lib/flare/sampler.rb +0 -130
  59. data/lib/flare/source_location.rb +0 -113
  60. data/lib/flare/sqlite_exporter.rb +0 -334
  61. data/lib/flare/storage/sqlite.rb +0 -794
  62. data/lib/flare/storage.rb +0 -54
  63. data/lib/flare/trace_blob.rb +0 -116
  64. data/lib/flare/trace_exporter.rb +0 -182
  65. data/lib/flare/trace_health_reporter.rb +0 -74
  66. data/lib/flare/upload_url_pool.rb +0 -108
  67. data/lib/flare/version.rb +0 -5
  68. data/lib/flare/web_marker_subscriber.rb +0 -76
  69. data/public/flare-assets/flare.css +0 -1245
  70. data/public/flare-assets/images/flipper.png +0 -0
@@ -1,794 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "sqlite3"
4
- require "json"
5
-
6
- module Flare
7
- module Storage
8
- class SQLite < Base
9
- MISSING_PARENT_ID = "0000000000000000"
10
-
11
- # Rails framework controller prefixes to filter (lowercase/underscored format)
12
- RAILS_CONTROLLER_PREFIXES = %w[
13
- active_storage/
14
- action_mailbox/
15
- rails/
16
- flare/
17
- ].freeze
18
-
19
- def initialize(database_path)
20
- @database_path = database_path
21
- @mutex = Mutex.new
22
- @setup = false
23
- end
24
-
25
- # List root spans that are HTTP requests (for the requests index)
26
- def list_requests(status: nil, method: nil, name: nil, origin: nil, limit: 50, offset: 0)
27
- # Find root spans with kind=server that have http.method property
28
- conditions = ["s.parent_span_id = ?", "s.kind = ?"]
29
- values = [MISSING_PARENT_ID, "server"]
30
-
31
- # Filter by http.method property existing (makes it an HTTP request)
32
- # We join with properties to filter
33
-
34
- if status
35
- case status
36
- when "2xx"
37
- conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
38
- values << "2%"
39
- when "3xx"
40
- conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
41
- values << "3%"
42
- when "4xx"
43
- conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
44
- values << "4%"
45
- when "5xx"
46
- conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
47
- values << "5%"
48
- else
49
- conditions << "COALESCE(status_prop.value, status_prop_old.value) = ?"
50
- values << status.to_s
51
- end
52
- end
53
-
54
- if method
55
- conditions << "COALESCE(method_prop.value, method_prop_old.value) = ?"
56
- values << "\"#{method}\""
57
- end
58
-
59
- if name
60
- conditions << "s.name LIKE ?"
61
- values << "%#{name}%"
62
- end
63
-
64
- if origin
65
- if origin == "rails"
66
- controller_conditions = RAILS_CONTROLLER_PREFIXES.map { "controller_prop.value LIKE ?" }
67
- conditions << "(#{controller_conditions.join(" OR ")})"
68
- RAILS_CONTROLLER_PREFIXES.each { |prefix| values << "%#{prefix}%" }
69
- elsif origin == "app"
70
- controller_conditions = RAILS_CONTROLLER_PREFIXES.map { "controller_prop.value LIKE ?" }
71
- conditions << "(controller_prop.value IS NULL OR NOT (#{controller_conditions.join(" OR ")}))"
72
- RAILS_CONTROLLER_PREFIXES.each { |prefix| values << "%#{prefix}%" }
73
- end
74
- end
75
-
76
- where_clause = "WHERE #{conditions.join(" AND ")}"
77
- values << limit
78
- values << offset
79
-
80
- rows = query_all(<<~SQL, values)
81
- SELECT s.*,
82
- COALESCE(method_prop.value, method_prop_old.value) as http_method,
83
- COALESCE(status_prop.value, status_prop_old.value) as http_status,
84
- COALESCE(target_prop.value, target_prop_old.value) as http_target,
85
- controller_prop.value as controller,
86
- action_prop.value as action
87
- FROM flare_spans s
88
- 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'
89
- 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'
90
- 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'
91
- 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'
92
- 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'
93
- 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'
94
- 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'
95
- 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'
96
- #{where_clause}
97
- AND COALESCE(method_prop.value, method_prop_old.value) IS NOT NULL
98
- ORDER BY s.created_at DESC
99
- LIMIT ? OFFSET ?
100
- SQL
101
-
102
- rows.map { |row| row_to_request(row) }
103
- end
104
-
105
- # List root spans that are jobs (for the jobs index)
106
- def list_jobs(status: nil, name: nil, limit: 50, offset: 0)
107
- # Find root spans with kind=consumer (ActiveJob processing)
108
- conditions = ["s.parent_span_id = ?", "s.kind = ?"]
109
- values = [MISSING_PARENT_ID, "consumer"]
110
-
111
- if name
112
- conditions << "s.name LIKE ?"
113
- values << "%#{name}%"
114
- end
115
-
116
- where_clause = "WHERE #{conditions.join(" AND ")}"
117
- values << limit
118
- values << offset
119
-
120
- rows = query_all(<<~SQL, values)
121
- SELECT s.*,
122
- job_class_prop.value as job_class,
123
- queue_prop.value as queue_name
124
- FROM flare_spans s
125
- 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'
126
- 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'
127
- #{where_clause}
128
- ORDER BY s.created_at DESC
129
- LIMIT ? OFFSET ?
130
- SQL
131
-
132
- rows.map { |row| row_to_job(row) }
133
- end
134
-
135
- # Span category patterns for filtering
136
- SPAN_CATEGORIES = {
137
- "queries" => ["sql.active_record", "mysql", "postgres", "sqlite"],
138
- "cache" => ["cache_read.active_support", "cache_write.active_support", "cache_delete.active_support", "cache_exist?.active_support", "cache_fetch_hit.active_support"],
139
- "views" => ["render_template.action_view", "render_partial.action_view", "render_layout.action_view", "render_collection.action_view"],
140
- "http" => ["HTTP", "net_http"],
141
- "mail" => ["deliver.action_mailer", "process.action_mailer"],
142
- "redis" => ["redis"],
143
- "exceptions" => [] # Handled specially via events
144
- }.freeze
145
-
146
- # Transaction statements to filter out from queries list
147
- TRANSACTION_STATEMENTS = %w[BEGIN COMMIT ROLLBACK].freeze
148
-
149
- # List spans by category (for the spans listing pages)
150
- def list_spans_by_category(category, name: nil, limit: 50, offset: 0)
151
- patterns = SPAN_CATEGORIES[category] || []
152
- return [] if patterns.empty? && category != "exceptions"
153
-
154
- if category == "exceptions"
155
- # Exceptions are stored as events on spans, not as spans themselves
156
- return list_exception_spans(name: name, limit: limit, offset: offset)
157
- end
158
-
159
- conditions = ["s.parent_span_id != ?"]
160
- values = [MISSING_PARENT_ID]
161
-
162
- # Build OR conditions for matching span names
163
- pattern_conditions = patterns.map { "s.name LIKE ?" }
164
- conditions << "(#{pattern_conditions.join(" OR ")})"
165
- patterns.each { |p| values << "%#{p}%" }
166
-
167
- if name
168
- conditions << "s.name LIKE ?"
169
- values << "%#{name}%"
170
- end
171
-
172
- # For queries, exclude transaction statements (BEGIN, COMMIT, ROLLBACK)
173
- transaction_join = ""
174
- if category == "queries"
175
- 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'"
176
- exclusions = TRANSACTION_STATEMENTS.map { "?" }.join(", ")
177
- conditions << "(stmt_prop.value IS NULL OR stmt_prop.value NOT IN (#{exclusions}))"
178
- TRANSACTION_STATEMENTS.each { |stmt| values << "\"#{stmt}\"" }
179
- end
180
-
181
- where_clause = "WHERE #{conditions.join(" AND ")}"
182
- values << limit
183
- values << offset
184
-
185
- rows = query_all(<<~SQL, values)
186
- SELECT s.*,
187
- root.trace_id as root_trace_id,
188
- root.name as root_name,
189
- root.kind as root_kind,
190
- root_controller.value as root_controller,
191
- root_action.value as root_action
192
- FROM flare_spans s
193
- LEFT JOIN flare_spans root ON root.trace_id = s.trace_id AND root.parent_span_id = '#{MISSING_PARENT_ID}'
194
- 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'
195
- 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'
196
- #{transaction_join}
197
- #{where_clause}
198
- ORDER BY s.created_at DESC
199
- LIMIT ? OFFSET ?
200
- SQL
201
-
202
- rows.map { |row| row_to_span_with_root(row) }
203
- end
204
-
205
- def count_spans_by_category(category, name: nil)
206
- patterns = SPAN_CATEGORIES[category] || []
207
- return 0 if patterns.empty? && category != "exceptions"
208
-
209
- if category == "exceptions"
210
- return count_exception_spans(name: name)
211
- end
212
-
213
- conditions = ["s.parent_span_id != ?"]
214
- values = [MISSING_PARENT_ID]
215
-
216
- pattern_conditions = patterns.map { "s.name LIKE ?" }
217
- conditions << "(#{pattern_conditions.join(" OR ")})"
218
- patterns.each { |p| values << "%#{p}%" }
219
-
220
- if name
221
- conditions << "s.name LIKE ?"
222
- values << "%#{name}%"
223
- end
224
-
225
- # For queries, exclude transaction statements (BEGIN, COMMIT, ROLLBACK)
226
- transaction_join = ""
227
- if category == "queries"
228
- 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'"
229
- exclusions = TRANSACTION_STATEMENTS.map { "?" }.join(", ")
230
- conditions << "(stmt_prop.value IS NULL OR stmt_prop.value NOT IN (#{exclusions}))"
231
- TRANSACTION_STATEMENTS.each { |stmt| values << "\"#{stmt}\"" }
232
- end
233
-
234
- where_clause = "WHERE #{conditions.join(" AND ")}"
235
-
236
- row = query_one(<<~SQL, values)
237
- SELECT COUNT(*) as count
238
- FROM flare_spans s
239
- #{transaction_join}
240
- #{where_clause}
241
- SQL
242
-
243
- row ? row["count"] : 0
244
- end
245
-
246
- # Find a single span by its database ID
247
- def find_span(id)
248
- row = query_one(<<~SQL, [id])
249
- SELECT s.*,
250
- root.trace_id as root_trace_id,
251
- root.name as root_name,
252
- root.kind as root_kind,
253
- root_controller.value as root_controller,
254
- root_action.value as root_action
255
- FROM flare_spans s
256
- LEFT JOIN flare_spans root ON root.trace_id = s.trace_id AND root.parent_span_id = '#{MISSING_PARENT_ID}'
257
- 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'
258
- 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'
259
- WHERE s.id = ?
260
- SQL
261
-
262
- return nil unless row
263
-
264
- span = row_to_span_with_root(row)
265
- span[:properties] = load_properties("Flare::Span", span[:id])
266
- span[:events] = load_events_for_spans([span[:id]])[span[:id]] || []
267
- span
268
- end
269
-
270
- # List spans that have exception events
271
- def list_exception_spans(name: nil, limit: 50, offset: 0)
272
- conditions = []
273
- values = []
274
-
275
- if name
276
- conditions << "e.name LIKE ?"
277
- values << "%#{name}%"
278
- end
279
-
280
- where_clause = conditions.any? ? "WHERE #{conditions.join(" AND ")}" : ""
281
- values << limit
282
- values << offset
283
-
284
- rows = query_all(<<~SQL, values)
285
- SELECT DISTINCT s.*,
286
- root.trace_id as root_trace_id,
287
- root.name as root_name,
288
- root.kind as root_kind,
289
- root_controller.value as root_controller,
290
- root_action.value as root_action,
291
- exc_type.value as exception_type,
292
- exc_message.value as exception_message,
293
- exc_stacktrace.value as exception_stacktrace
294
- FROM flare_events e
295
- JOIN flare_spans s ON s.id = e.span_id
296
- LEFT JOIN flare_spans root ON root.trace_id = s.trace_id AND root.parent_span_id = '#{MISSING_PARENT_ID}'
297
- 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'
298
- 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'
299
- 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'
300
- 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'
301
- 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'
302
- #{where_clause}
303
- ORDER BY e.created_at DESC
304
- LIMIT ? OFFSET ?
305
- SQL
306
-
307
- rows.map { |row| row_to_span_with_root(row) }
308
- end
309
-
310
- def count_exception_spans(name: nil)
311
- conditions = []
312
- values = []
313
-
314
- if name
315
- conditions << "e.name LIKE ?"
316
- values << "%#{name}%"
317
- end
318
-
319
- where_clause = conditions.any? ? "WHERE #{conditions.join(" AND ")}" : ""
320
-
321
- row = query_one(<<~SQL, values)
322
- SELECT COUNT(DISTINCT s.id) as count
323
- FROM flare_events e
324
- JOIN flare_spans s ON s.id = e.span_id
325
- #{where_clause}
326
- SQL
327
-
328
- row ? row["count"] : 0
329
- end
330
-
331
- def count_jobs(status: nil, name: nil)
332
- conditions = ["s.parent_span_id = ?", "s.kind = ?"]
333
- values = [MISSING_PARENT_ID, "consumer"]
334
-
335
- if name
336
- conditions << "s.name LIKE ?"
337
- values << "%#{name}%"
338
- end
339
-
340
- where_clause = "WHERE #{conditions.join(" AND ")}"
341
-
342
- row = query_one(<<~SQL, values)
343
- SELECT COUNT(*) as count
344
- FROM flare_spans s
345
- #{where_clause}
346
- SQL
347
-
348
- row ? row["count"] : 0
349
- end
350
-
351
- def count_requests(status: nil, method: nil, name: nil, origin: nil)
352
- conditions = ["s.parent_span_id = ?", "s.kind = ?"]
353
- values = [MISSING_PARENT_ID, "server"]
354
-
355
- if status
356
- case status
357
- when "2xx"
358
- conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
359
- values << "2%"
360
- when "3xx"
361
- conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
362
- values << "3%"
363
- when "4xx"
364
- conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
365
- values << "4%"
366
- when "5xx"
367
- conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
368
- values << "5%"
369
- else
370
- conditions << "COALESCE(status_prop.value, status_prop_old.value) = ?"
371
- values << status.to_s
372
- end
373
- end
374
-
375
- if method
376
- conditions << "COALESCE(method_prop.value, method_prop_old.value) = ?"
377
- values << "\"#{method}\""
378
- end
379
-
380
- if name
381
- conditions << "s.name LIKE ?"
382
- values << "%#{name}%"
383
- end
384
-
385
- if origin
386
- if origin == "rails"
387
- controller_conditions = RAILS_CONTROLLER_PREFIXES.map { "controller_prop.value LIKE ?" }
388
- conditions << "(#{controller_conditions.join(" OR ")})"
389
- RAILS_CONTROLLER_PREFIXES.each { |prefix| values << "%#{prefix}%" }
390
- elsif origin == "app"
391
- controller_conditions = RAILS_CONTROLLER_PREFIXES.map { "controller_prop.value LIKE ?" }
392
- conditions << "(controller_prop.value IS NULL OR NOT (#{controller_conditions.join(" OR ")}))"
393
- RAILS_CONTROLLER_PREFIXES.each { |prefix| values << "%#{prefix}%" }
394
- end
395
- end
396
-
397
- where_clause = "WHERE #{conditions.join(" AND ")}"
398
-
399
- row = query_one(<<~SQL, values)
400
- SELECT COUNT(*) as count
401
- FROM flare_spans s
402
- 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'
403
- 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'
404
- 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'
405
- 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'
406
- 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'
407
- #{where_clause}
408
- AND COALESCE(method_prop.value, method_prop_old.value) IS NOT NULL
409
- SQL
410
-
411
- row ? row["count"] : 0
412
- end
413
-
414
- # Find a job by trace_id (for the detail view)
415
- def find_job(trace_id)
416
- row = query_one(<<~SQL, [trace_id, MISSING_PARENT_ID, "consumer"])
417
- SELECT s.*
418
- FROM flare_spans s
419
- WHERE s.trace_id = ? AND s.parent_span_id = ? AND s.kind = ?
420
- SQL
421
-
422
- return nil unless row
423
-
424
- span = row_to_span(row)
425
- span[:properties] = load_properties("Flare::Span", span[:id])
426
- span
427
- end
428
-
429
- # Find a request by trace_id (for the detail view)
430
- def find_request(trace_id)
431
- row = query_one(<<~SQL, [trace_id, MISSING_PARENT_ID])
432
- SELECT s.*
433
- FROM flare_spans s
434
- WHERE s.trace_id = ? AND s.parent_span_id = ?
435
- SQL
436
-
437
- return nil unless row
438
-
439
- span = row_to_span(row)
440
- span[:properties] = load_properties("Flare::Span", span[:id])
441
- span
442
- end
443
-
444
- # Get all spans for a trace (for the waterfall view)
445
- def spans_for_trace(trace_id)
446
- rows = query_all(<<~SQL, [trace_id])
447
- SELECT * FROM flare_spans
448
- WHERE trace_id = ?
449
- ORDER BY start_timestamp ASC
450
- SQL
451
-
452
- spans = rows.map { |row| row_to_span(row) }
453
-
454
- # Load properties for all spans
455
- span_ids = spans.map { |s| s[:id] }
456
- if span_ids.any?
457
- all_properties = load_properties_for_ids("Flare::Span", span_ids)
458
- spans.each do |span|
459
- span[:properties] = all_properties[span[:id]] || {}
460
- end
461
- end
462
-
463
- # Load events for all spans
464
- if span_ids.any?
465
- all_events = load_events_for_spans(span_ids)
466
- spans.each do |span|
467
- span[:events] = all_events[span[:id]] || []
468
- end
469
- end
470
-
471
- spans
472
- end
473
-
474
- # Load properties for a specific owner
475
- def load_properties(owner_type, owner_id)
476
- rows = query_all(<<~SQL, [owner_type, owner_id])
477
- SELECT key, value, value_type FROM flare_properties
478
- WHERE owner_type = ? AND owner_id = ?
479
- SQL
480
-
481
- rows.each_with_object({}) do |row, hash|
482
- hash[row["key"]] = parse_property_value(row["value"], row["value_type"])
483
- end
484
- end
485
-
486
- # Load properties for multiple owners at once
487
- def load_properties_for_ids(owner_type, owner_ids)
488
- return {} if owner_ids.empty?
489
-
490
- placeholders = owner_ids.map { "?" }.join(", ")
491
- rows = query_all(<<~SQL, [owner_type] + owner_ids)
492
- SELECT owner_id, key, value, value_type FROM flare_properties
493
- WHERE owner_type = ? AND owner_id IN (#{placeholders})
494
- SQL
495
-
496
- result = Hash.new { |h, k| h[k] = {} }
497
- rows.each do |row|
498
- result[row["owner_id"]][row["key"]] = parse_property_value(row["value"], row["value_type"])
499
- end
500
- result
501
- end
502
-
503
- # Load events for multiple spans at once
504
- def load_events_for_spans(span_ids)
505
- return {} if span_ids.empty?
506
-
507
- placeholders = span_ids.map { "?" }.join(", ")
508
- event_rows = query_all(<<~SQL, span_ids)
509
- SELECT * FROM flare_events
510
- WHERE span_id IN (#{placeholders})
511
- SQL
512
-
513
- # Group events by span_id
514
- events_by_span = Hash.new { |h, k| h[k] = [] }
515
- event_ids = []
516
-
517
- event_rows.each do |row|
518
- event = {
519
- id: row["id"],
520
- span_id: row["span_id"],
521
- name: row["name"],
522
- created_at: row["created_at"]
523
- }
524
- events_by_span[row["span_id"]] << event
525
- event_ids << row["id"]
526
- end
527
-
528
- # Load properties for all events
529
- if event_ids.any?
530
- event_properties = load_properties_for_ids("Flare::Event", event_ids)
531
- events_by_span.each do |_, events|
532
- events.each do |event|
533
- event[:properties] = event_properties[event[:id]] || {}
534
- end
535
- end
536
- end
537
-
538
- events_by_span
539
- end
540
-
541
- def prune(retention_hours:, max_spans:)
542
- cutoff = (Time.now - (retention_hours * 3600)).iso8601(6)
543
-
544
- # Delete old properties first (for old spans and events)
545
- execute(<<~SQL, [cutoff])
546
- DELETE FROM flare_properties WHERE owner_type = 'Flare::Span' AND owner_id IN (
547
- SELECT id FROM flare_spans WHERE created_at < ?
548
- )
549
- SQL
550
-
551
- execute(<<~SQL, [cutoff])
552
- DELETE FROM flare_properties WHERE owner_type = 'Flare::Event' AND owner_id IN (
553
- SELECT id FROM flare_events WHERE span_id IN (
554
- SELECT id FROM flare_spans WHERE created_at < ?
555
- )
556
- )
557
- SQL
558
-
559
- # Delete old events
560
- execute(<<~SQL, [cutoff])
561
- DELETE FROM flare_events WHERE span_id IN (
562
- SELECT id FROM flare_spans WHERE created_at < ?
563
- )
564
- SQL
565
-
566
- # Delete old spans
567
- execute(<<~SQL, [cutoff])
568
- DELETE FROM flare_spans WHERE created_at < ?
569
- SQL
570
-
571
- # Also prune if over max_spans (keep newest)
572
- execute(<<~SQL, [max_spans])
573
- DELETE FROM flare_properties WHERE owner_type = 'Flare::Span' AND owner_id IN (
574
- SELECT id FROM flare_spans
575
- ORDER BY created_at DESC
576
- LIMIT -1 OFFSET ?
577
- )
578
- SQL
579
-
580
- execute(<<~SQL, [max_spans])
581
- DELETE FROM flare_properties WHERE owner_type = 'Flare::Event' AND owner_id IN (
582
- SELECT id FROM flare_events WHERE span_id IN (
583
- SELECT id FROM flare_spans
584
- ORDER BY created_at DESC
585
- LIMIT -1 OFFSET ?
586
- )
587
- )
588
- SQL
589
-
590
- execute(<<~SQL, [max_spans])
591
- DELETE FROM flare_events WHERE span_id IN (
592
- SELECT id FROM flare_spans
593
- ORDER BY created_at DESC
594
- LIMIT -1 OFFSET ?
595
- )
596
- SQL
597
-
598
- execute(<<~SQL, [max_spans])
599
- DELETE FROM flare_spans WHERE id IN (
600
- SELECT id FROM flare_spans
601
- ORDER BY created_at DESC
602
- LIMIT -1 OFFSET ?
603
- )
604
- SQL
605
- end
606
-
607
- def clear_all
608
- execute("DELETE FROM flare_properties")
609
- execute("DELETE FROM flare_events")
610
- execute("DELETE FROM flare_spans")
611
- end
612
-
613
- private
614
-
615
- def setup_database
616
- # The SQLiteExporter creates the tables, but we ensure they exist here too
617
- @mutex.synchronize do
618
- return if @setup
619
-
620
- db = connection
621
- configure_pragmas(db)
622
-
623
- db.execute(<<~SQL)
624
- CREATE TABLE IF NOT EXISTS flare_spans (
625
- id INTEGER PRIMARY KEY AUTOINCREMENT,
626
- name TEXT NOT NULL,
627
- kind TEXT NOT NULL,
628
- span_id TEXT NOT NULL,
629
- trace_id TEXT NOT NULL,
630
- parent_span_id TEXT,
631
- start_timestamp INTEGER NOT NULL,
632
- end_timestamp INTEGER NOT NULL,
633
- total_recorded_properties INTEGER NOT NULL DEFAULT 0,
634
- total_recorded_events INTEGER NOT NULL DEFAULT 0,
635
- total_recorded_links INTEGER NOT NULL DEFAULT 0,
636
- created_at TEXT NOT NULL,
637
- updated_at TEXT NOT NULL
638
- )
639
- SQL
640
-
641
- db.execute("CREATE INDEX IF NOT EXISTS idx_spans_span_id ON flare_spans(span_id)")
642
- db.execute("CREATE INDEX IF NOT EXISTS idx_spans_trace_id ON flare_spans(trace_id)")
643
- db.execute("CREATE INDEX IF NOT EXISTS idx_spans_parent_span_id ON flare_spans(parent_span_id)")
644
- db.execute("CREATE INDEX IF NOT EXISTS idx_spans_created_at ON flare_spans(created_at)")
645
-
646
- db.execute(<<~SQL)
647
- CREATE TABLE IF NOT EXISTS flare_events (
648
- id INTEGER PRIMARY KEY AUTOINCREMENT,
649
- span_id INTEGER NOT NULL,
650
- name TEXT NOT NULL,
651
- created_at TEXT NOT NULL,
652
- updated_at TEXT NOT NULL,
653
- FOREIGN KEY (span_id) REFERENCES flare_spans(id)
654
- )
655
- SQL
656
-
657
- db.execute("CREATE INDEX IF NOT EXISTS idx_events_span_id ON flare_events(span_id)")
658
-
659
- db.execute(<<~SQL)
660
- CREATE TABLE IF NOT EXISTS flare_properties (
661
- id INTEGER PRIMARY KEY AUTOINCREMENT,
662
- key TEXT NOT NULL,
663
- value TEXT,
664
- value_type INTEGER NOT NULL DEFAULT 0,
665
- owner_type TEXT NOT NULL,
666
- owner_id INTEGER NOT NULL,
667
- created_at TEXT NOT NULL,
668
- updated_at TEXT NOT NULL
669
- )
670
- SQL
671
-
672
- db.execute("CREATE INDEX IF NOT EXISTS idx_properties_owner ON flare_properties(owner_type, owner_id)")
673
- db.execute("CREATE INDEX IF NOT EXISTS idx_properties_key ON flare_properties(key)")
674
-
675
- close_connection # avoid inheriting connection across fork
676
- @setup = true
677
- end
678
- end
679
-
680
- # Applies the same SQLite pragmas that ActiveRecord uses for good
681
- # concurrency and performance with threaded/multi-process access.
682
- def configure_pragmas(db)
683
- db.execute("PRAGMA journal_mode=WAL")
684
- db.execute("PRAGMA synchronous=NORMAL")
685
- db.execute("PRAGMA mmap_size=134217728") # 128MB
686
- db.execute("PRAGMA journal_size_limit=67108864") # 64MB
687
- db.execute("PRAGMA cache_size=2000")
688
- end
689
-
690
- def connection
691
- Thread.current[:flare_storage_db] ||= begin
692
- dir = File.dirname(@database_path)
693
- FileUtils.mkdir_p(dir) unless File.directory?(dir)
694
- db = ::SQLite3::Database.new(@database_path, results_as_hash: true)
695
- db.busy_timeout = 5000
696
- db
697
- end
698
- end
699
-
700
- def close_connection
701
- if db = Thread.current[:flare_storage_db]
702
- db.close rescue nil
703
- Thread.current[:flare_storage_db] = nil
704
- end
705
- end
706
-
707
- def execute(sql, values = [])
708
- setup_database unless @setup
709
- @mutex.synchronize do
710
- connection.execute(sql, values)
711
- end
712
- end
713
-
714
- def query_one(sql, values = [])
715
- setup_database unless @setup
716
- @mutex.synchronize do
717
- connection.execute(sql, values).first
718
- end
719
- end
720
-
721
- def query_all(sql, values = [])
722
- setup_database unless @setup
723
- @mutex.synchronize do
724
- connection.execute(sql, values)
725
- end
726
- end
727
-
728
- def row_to_span(row)
729
- {
730
- id: row["id"],
731
- name: row["name"],
732
- kind: row["kind"],
733
- span_id: row["span_id"],
734
- trace_id: row["trace_id"],
735
- parent_span_id: row["parent_span_id"],
736
- start_timestamp: row["start_timestamp"],
737
- end_timestamp: row["end_timestamp"],
738
- duration_ms: (row["end_timestamp"] - row["start_timestamp"]) / 1_000_000.0,
739
- created_at: row["created_at"],
740
- properties: {},
741
- events: []
742
- }
743
- end
744
-
745
- def row_to_request(row)
746
- span = row_to_span(row)
747
-
748
- # Add convenience accessors from the joined properties
749
- span[:http_method] = parse_property_value(row["http_method"], 0)
750
- span[:http_status] = parse_property_value(row["http_status"], 1)
751
- span[:http_target] = parse_property_value(row["http_target"], 0)
752
- span[:controller] = parse_property_value(row["controller"], 0)
753
- span[:action] = parse_property_value(row["action"], 0)
754
-
755
- span
756
- end
757
-
758
- def row_to_job(row)
759
- span = row_to_span(row)
760
-
761
- # Add convenience accessors from the joined properties
762
- span[:job_class] = parse_property_value(row["job_class"], 0)
763
- span[:queue_name] = parse_property_value(row["queue_name"], 0)
764
-
765
- span
766
- end
767
-
768
- def row_to_span_with_root(row)
769
- span = row_to_span(row)
770
-
771
- # Add root span info for linking back to request/job
772
- span[:root_trace_id] = row["root_trace_id"]
773
- span[:root_name] = row["root_name"]
774
- span[:root_kind] = row["root_kind"]
775
- # For requests: controller#action, for jobs: code.namespace is the job class
776
- span[:root_controller] = parse_property_value(row["root_controller"], 0)
777
- span[:root_action] = parse_property_value(row["root_action"], 0)
778
- span[:exception_type] = parse_property_value(row["exception_type"], 0) if row["exception_type"]
779
- span[:exception_message] = parse_property_value(row["exception_message"], 0) if row["exception_message"]
780
- span[:exception_stacktrace] = parse_property_value(row["exception_stacktrace"], 0) if row["exception_stacktrace"]
781
-
782
- span
783
- end
784
-
785
- def parse_property_value(value, value_type)
786
- return nil if value.nil?
787
-
788
- JSON.parse(value)
789
- rescue JSON::ParserError
790
- value
791
- end
792
- end
793
- end
794
- end