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