apm_bro 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +48 -35
- data/lib/apm_bro/cache_subscriber.rb +15 -25
- data/lib/apm_bro/circuit_breaker.rb +9 -17
- data/lib/apm_bro/client.rb +28 -36
- data/lib/apm_bro/configuration.rb +87 -65
- data/lib/apm_bro/error_middleware.rb +16 -30
- data/lib/apm_bro/http_instrumentation.rb +22 -16
- data/lib/apm_bro/job_sql_tracking_middleware.rb +1 -1
- data/lib/apm_bro/job_subscriber.rb +41 -25
- data/lib/apm_bro/lightweight_memory_tracker.rb +8 -8
- data/lib/apm_bro/logger.rb +8 -9
- data/lib/apm_bro/memory_helpers.rb +13 -13
- data/lib/apm_bro/memory_leak_detector.rb +37 -37
- data/lib/apm_bro/memory_tracking_subscriber.rb +62 -54
- data/lib/apm_bro/railtie.rb +28 -30
- data/lib/apm_bro/redis_subscriber.rb +34 -37
- data/lib/apm_bro/sql_subscriber.rb +308 -60
- data/lib/apm_bro/sql_tracking_middleware.rb +11 -11
- data/lib/apm_bro/subscriber.rb +53 -39
- data/lib/apm_bro/version.rb +1 -1
- data/lib/apm_bro/view_rendering_subscriber.rb +23 -23
- data/lib/apm_bro.rb +9 -0
- metadata +4 -4
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require "
|
|
3
|
+
begin
|
|
4
|
+
require "active_support/notifications"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# ActiveSupport not available
|
|
7
|
+
end
|
|
5
8
|
|
|
6
9
|
module ApmBro
|
|
7
10
|
class SqlSubscriber
|
|
8
|
-
SQL_EVENT_NAME = "sql.active_record"
|
|
11
|
+
SQL_EVENT_NAME = "sql.active_record"
|
|
9
12
|
THREAD_LOCAL_KEY = :apm_bro_sql_queries
|
|
10
13
|
THREAD_LOCAL_ALLOC_START_KEY = :apm_bro_sql_alloc_start
|
|
11
14
|
THREAD_LOCAL_ALLOC_RESULTS_KEY = :apm_bro_sql_alloc_results
|
|
12
15
|
THREAD_LOCAL_BACKTRACE_KEY = :apm_bro_sql_backtraces
|
|
16
|
+
THREAD_LOCAL_EXPLAIN_PENDING_KEY = :apm_bro_explain_pending
|
|
13
17
|
|
|
14
18
|
def self.subscribe!
|
|
15
19
|
# Subscribe with a start/finish listener to measure allocations per query
|
|
16
20
|
if ActiveSupport::Notifications.notifier.respond_to?(:subscribe)
|
|
17
21
|
begin
|
|
18
22
|
ActiveSupport::Notifications.notifier.subscribe(SQL_EVENT_NAME, SqlAllocListener.new)
|
|
19
|
-
rescue
|
|
23
|
+
rescue
|
|
20
24
|
end
|
|
21
25
|
end
|
|
22
26
|
|
|
@@ -30,25 +34,35 @@ module ApmBro
|
|
|
30
34
|
begin
|
|
31
35
|
alloc_results = Thread.current[THREAD_LOCAL_ALLOC_RESULTS_KEY]
|
|
32
36
|
allocations = alloc_results && alloc_results.delete(unique_id)
|
|
33
|
-
|
|
37
|
+
|
|
34
38
|
# Get the captured backtrace from when the query started
|
|
35
39
|
backtrace_map = Thread.current[THREAD_LOCAL_BACKTRACE_KEY]
|
|
36
40
|
captured_backtrace = backtrace_map && backtrace_map.delete(unique_id)
|
|
37
|
-
rescue
|
|
41
|
+
rescue
|
|
38
42
|
end
|
|
39
43
|
|
|
44
|
+
duration_ms = ((finished - started) * 1000.0).round(2)
|
|
45
|
+
original_sql = data[:sql]
|
|
46
|
+
|
|
40
47
|
query_info = {
|
|
41
|
-
sql: sanitize_sql(
|
|
48
|
+
sql: sanitize_sql(original_sql),
|
|
42
49
|
name: data[:name],
|
|
43
|
-
duration_ms:
|
|
50
|
+
duration_ms: duration_ms,
|
|
44
51
|
cached: data[:cached] || false,
|
|
45
52
|
connection_id: data[:connection_id],
|
|
46
53
|
trace: safe_query_trace(data, captured_backtrace),
|
|
47
54
|
allocations: allocations
|
|
48
55
|
}
|
|
56
|
+
|
|
57
|
+
# Run EXPLAIN ANALYZE for slow queries in the background
|
|
58
|
+
if should_explain_query?(duration_ms, original_sql)
|
|
59
|
+
# Store reference to query_info so we can update it when EXPLAIN completes
|
|
60
|
+
query_info[:explain_plan] = nil # Placeholder
|
|
61
|
+
start_explain_analyze_background(original_sql, data[:connection_id], query_info)
|
|
62
|
+
end
|
|
63
|
+
|
|
49
64
|
# Add to thread-local storage
|
|
50
65
|
Thread.current[THREAD_LOCAL_KEY] << query_info
|
|
51
|
-
|
|
52
66
|
end
|
|
53
67
|
end
|
|
54
68
|
|
|
@@ -57,31 +71,266 @@ module ApmBro
|
|
|
57
71
|
Thread.current[THREAD_LOCAL_ALLOC_START_KEY] = {}
|
|
58
72
|
Thread.current[THREAD_LOCAL_ALLOC_RESULTS_KEY] = {}
|
|
59
73
|
Thread.current[THREAD_LOCAL_BACKTRACE_KEY] = {}
|
|
74
|
+
Thread.current[THREAD_LOCAL_EXPLAIN_PENDING_KEY] = []
|
|
60
75
|
end
|
|
61
76
|
|
|
62
77
|
def self.stop_request_tracking
|
|
78
|
+
# Wait for any pending EXPLAIN ANALYZE queries to complete (with timeout)
|
|
79
|
+
# This must happen BEFORE we get the queries array reference to ensure
|
|
80
|
+
# all explain_plan fields are populated
|
|
81
|
+
wait_for_pending_explains(5.0) # 5 second timeout
|
|
82
|
+
|
|
83
|
+
# Get queries after waiting for EXPLAIN to complete
|
|
63
84
|
queries = Thread.current[THREAD_LOCAL_KEY]
|
|
85
|
+
|
|
64
86
|
Thread.current[THREAD_LOCAL_KEY] = nil
|
|
65
87
|
Thread.current[THREAD_LOCAL_ALLOC_START_KEY] = nil
|
|
66
88
|
Thread.current[THREAD_LOCAL_ALLOC_RESULTS_KEY] = nil
|
|
67
89
|
Thread.current[THREAD_LOCAL_BACKTRACE_KEY] = nil
|
|
90
|
+
Thread.current[THREAD_LOCAL_EXPLAIN_PENDING_KEY] = nil
|
|
68
91
|
queries || []
|
|
69
92
|
end
|
|
70
93
|
|
|
94
|
+
def self.wait_for_pending_explains(timeout_seconds)
|
|
95
|
+
pending = Thread.current[THREAD_LOCAL_EXPLAIN_PENDING_KEY]
|
|
96
|
+
return unless pending && !pending.empty?
|
|
97
|
+
|
|
98
|
+
start_time = Time.now
|
|
99
|
+
pending.each do |thread|
|
|
100
|
+
remaining_time = timeout_seconds - (Time.now - start_time)
|
|
101
|
+
break if remaining_time <= 0
|
|
102
|
+
|
|
103
|
+
begin
|
|
104
|
+
thread.join(remaining_time)
|
|
105
|
+
rescue => e
|
|
106
|
+
ApmBro.logger.debug("Error waiting for EXPLAIN ANALYZE: #{e.message}")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
71
111
|
def self.sanitize_sql(sql)
|
|
72
112
|
return sql unless sql.is_a?(String)
|
|
73
113
|
|
|
74
114
|
# Remove sensitive data patterns
|
|
75
115
|
sql = sql.gsub(/\b(password|token|secret|key|ssn|credit_card)\s*=\s*['"][^'"]*['"]/i, '\1 = ?')
|
|
76
116
|
sql = sql.gsub(/\b(password|token|secret|key|ssn|credit_card)\s*=\s*[^'",\s)]+/i, '\1 = ?')
|
|
77
|
-
|
|
117
|
+
|
|
78
118
|
# Remove specific values in WHERE clauses that might be sensitive
|
|
79
119
|
sql = sql.gsub(/WHERE\s+[^=]+=\s*['"][^'"]*['"]/i) do |match|
|
|
80
|
-
match.gsub(/=\s*['"][^'"]*['"]/,
|
|
120
|
+
match.gsub(/=\s*['"][^'"]*['"]/, "= ?")
|
|
81
121
|
end
|
|
82
122
|
|
|
83
123
|
# Limit query length to prevent huge payloads
|
|
84
|
-
sql.length > 1000 ? sql[0..1000] + "..." : sql
|
|
124
|
+
(sql.length > 1000) ? sql[0..1000] + "..." : sql
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def self.should_explain_query?(duration_ms, sql)
|
|
128
|
+
return false unless ApmBro.configuration.explain_analyze_enabled
|
|
129
|
+
return false if duration_ms < ApmBro.configuration.slow_query_threshold_ms
|
|
130
|
+
return false unless sql.is_a?(String)
|
|
131
|
+
return false if sql.strip.empty?
|
|
132
|
+
|
|
133
|
+
# Skip EXPLAIN for certain query types that don't benefit from it
|
|
134
|
+
sql_upper = sql.upcase.strip
|
|
135
|
+
return false if sql_upper.start_with?("EXPLAIN")
|
|
136
|
+
return false if sql_upper.start_with?("BEGIN")
|
|
137
|
+
return false if sql_upper.start_with?("COMMIT")
|
|
138
|
+
return false if sql_upper.start_with?("ROLLBACK")
|
|
139
|
+
return false if sql_upper.start_with?("SAVEPOINT")
|
|
140
|
+
return false if sql_upper.start_with?("RELEASE")
|
|
141
|
+
|
|
142
|
+
true
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def self.start_explain_analyze_background(sql, connection_id, query_info)
|
|
146
|
+
return unless defined?(ActiveRecord)
|
|
147
|
+
return unless ActiveRecord::Base.respond_to?(:connection)
|
|
148
|
+
|
|
149
|
+
# Capture the main thread reference to append logs to the correct thread
|
|
150
|
+
main_thread = Thread.current
|
|
151
|
+
|
|
152
|
+
# Run EXPLAIN in a background thread to avoid blocking the main request
|
|
153
|
+
explain_thread = Thread.new do
|
|
154
|
+
connection = nil
|
|
155
|
+
begin
|
|
156
|
+
# Use a separate connection to avoid interfering with the main query
|
|
157
|
+
if ActiveRecord::Base.connection_pool.respond_to?(:checkout)
|
|
158
|
+
connection = ActiveRecord::Base.connection_pool.checkout
|
|
159
|
+
else
|
|
160
|
+
connection = ActiveRecord::Base.connection
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Build EXPLAIN query based on database adapter
|
|
164
|
+
explain_sql = build_explain_query(sql, connection)
|
|
165
|
+
|
|
166
|
+
# Execute the EXPLAIN query
|
|
167
|
+
# For PostgreSQL, use select_all which returns ActiveRecord::Result
|
|
168
|
+
# For other databases, use execute
|
|
169
|
+
adapter_name = connection.adapter_name.downcase
|
|
170
|
+
if adapter_name == "postgresql" || adapter_name == "postgis"
|
|
171
|
+
# PostgreSQL: select_all returns ActiveRecord::Result with rows
|
|
172
|
+
result = connection.select_all(explain_sql)
|
|
173
|
+
else
|
|
174
|
+
# Other databases: use execute
|
|
175
|
+
result = connection.execute(explain_sql)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Format the result based on database adapter
|
|
179
|
+
explain_plan = format_explain_result(result, connection)
|
|
180
|
+
|
|
181
|
+
# Update the query_info with the explain plan
|
|
182
|
+
# This updates the hash that's already in the queries array
|
|
183
|
+
if explain_plan && !explain_plan.to_s.strip.empty?
|
|
184
|
+
query_info[:explain_plan] = explain_plan
|
|
185
|
+
append_log_to_thread(main_thread, :debug, "Captured EXPLAIN ANALYZE for slow query (#{query_info[:duration_ms]}ms): #{explain_plan[0..1000]}...")
|
|
186
|
+
else
|
|
187
|
+
query_info[:explain_plan] = nil
|
|
188
|
+
append_log_to_thread(main_thread, :debug, "EXPLAIN ANALYZE returned empty result. Result type: #{result.class}, Result: #{result.inspect[0..200]}")
|
|
189
|
+
end
|
|
190
|
+
rescue => e
|
|
191
|
+
# Silently fail - don't let EXPLAIN break the application
|
|
192
|
+
append_log_to_thread(main_thread, :debug, "Failed to capture EXPLAIN ANALYZE: #{e.message}")
|
|
193
|
+
query_info[:explain_plan] = nil
|
|
194
|
+
ensure
|
|
195
|
+
# Return connection to pool if we checked it out
|
|
196
|
+
if connection && ActiveRecord::Base.connection_pool.respond_to?(:checkin)
|
|
197
|
+
ActiveRecord::Base.connection_pool.checkin(connection) rescue nil
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Track the thread so we can wait for it when stopping request tracking
|
|
203
|
+
pending = Thread.current[THREAD_LOCAL_EXPLAIN_PENDING_KEY] ||= []
|
|
204
|
+
pending << explain_thread
|
|
205
|
+
rescue => e
|
|
206
|
+
# Use ApmBro.logger here since we're still in the main thread
|
|
207
|
+
ApmBro.logger.debug("Failed to start EXPLAIN ANALYZE thread: #{e.message}")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Append a log entry directly to a specific thread's log storage
|
|
211
|
+
# This is used when logging from background threads to ensure logs
|
|
212
|
+
# are collected with the main request thread's logs
|
|
213
|
+
def self.append_log_to_thread(thread, severity, message)
|
|
214
|
+
timestamp = Time.now.utc
|
|
215
|
+
log_entry = {
|
|
216
|
+
sev: severity.to_s,
|
|
217
|
+
msg: message.to_s,
|
|
218
|
+
time: timestamp.iso8601(3)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# Append to the specified thread's log storage
|
|
222
|
+
thread[:apm_bro_logs] ||= []
|
|
223
|
+
thread[:apm_bro_logs] << log_entry
|
|
224
|
+
|
|
225
|
+
# Also print the message immediately (using current thread's logger)
|
|
226
|
+
begin
|
|
227
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
228
|
+
formatted_message = "[ApmBro] #{timestamp.iso8601(3)} #{severity.to_s.upcase}: #{message}"
|
|
229
|
+
case severity
|
|
230
|
+
when :debug
|
|
231
|
+
Rails.logger.debug(formatted_message)
|
|
232
|
+
when :info
|
|
233
|
+
Rails.logger.info(formatted_message)
|
|
234
|
+
when :warn
|
|
235
|
+
Rails.logger.warn(formatted_message)
|
|
236
|
+
when :error
|
|
237
|
+
Rails.logger.error(formatted_message)
|
|
238
|
+
when :fatal
|
|
239
|
+
Rails.logger.fatal(formatted_message)
|
|
240
|
+
end
|
|
241
|
+
else
|
|
242
|
+
# Fallback to stdout
|
|
243
|
+
$stdout.puts("[ApmBro] #{timestamp.iso8601(3)} #{severity.to_s.upcase}: #{message}")
|
|
244
|
+
end
|
|
245
|
+
rescue
|
|
246
|
+
# Never let logging break the application
|
|
247
|
+
$stdout.puts("[ApmBro] #{severity.to_s.upcase}: #{message}")
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def self.build_explain_query(sql, connection)
|
|
252
|
+
adapter_name = connection.adapter_name.downcase
|
|
253
|
+
|
|
254
|
+
case adapter_name
|
|
255
|
+
when "postgresql", "postgis"
|
|
256
|
+
# PostgreSQL supports ANALYZE and BUFFERS
|
|
257
|
+
"EXPLAIN (ANALYZE, BUFFERS) #{sql}"
|
|
258
|
+
when "mysql", "mysql2", "trilogy"
|
|
259
|
+
# MySQL uses different syntax - ANALYZE is a separate keyword
|
|
260
|
+
"EXPLAIN ANALYZE #{sql}"
|
|
261
|
+
when "sqlite3"
|
|
262
|
+
# SQLite supports EXPLAIN QUERY PLAN
|
|
263
|
+
"EXPLAIN QUERY PLAN #{sql}"
|
|
264
|
+
else
|
|
265
|
+
# Generic fallback - just EXPLAIN
|
|
266
|
+
"EXPLAIN #{sql}"
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def self.format_explain_result(result, connection)
|
|
271
|
+
adapter_name = connection.adapter_name.downcase
|
|
272
|
+
|
|
273
|
+
case adapter_name
|
|
274
|
+
when "postgresql", "postgis"
|
|
275
|
+
# PostgreSQL returns ActiveRecord::Result from select_all
|
|
276
|
+
if result.respond_to?(:rows)
|
|
277
|
+
# ActiveRecord::Result object - rows is an array of arrays
|
|
278
|
+
# Each row is [query_plan_string]
|
|
279
|
+
plan_text = result.rows.map { |row| row.is_a?(Array) ? row.first.to_s : row.to_s }.join("\n")
|
|
280
|
+
return plan_text unless plan_text.strip.empty?
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Try alternative methods to extract the plan
|
|
284
|
+
if result.respond_to?(:each) && result.respond_to?(:columns)
|
|
285
|
+
# ActiveRecord::Result with columns
|
|
286
|
+
plan_column = result.columns.find { |col| col.downcase.include?("plan") || col.downcase.include?("query") } || result.columns.first
|
|
287
|
+
plan_text = result.map { |row|
|
|
288
|
+
if row.is_a?(Hash)
|
|
289
|
+
row[plan_column] || row[plan_column.to_sym] || row.values.first
|
|
290
|
+
else
|
|
291
|
+
row
|
|
292
|
+
end
|
|
293
|
+
}.join("\n")
|
|
294
|
+
return plan_text unless plan_text.strip.empty?
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
if result.is_a?(Array)
|
|
298
|
+
# Array of hashes or arrays
|
|
299
|
+
plan_text = result.map do |row|
|
|
300
|
+
if row.is_a?(Hash)
|
|
301
|
+
row["QUERY PLAN"] || row["query plan"] || row[:query_plan] || row.values.first.to_s
|
|
302
|
+
elsif row.is_a?(Array)
|
|
303
|
+
row.first.to_s
|
|
304
|
+
else
|
|
305
|
+
row.to_s
|
|
306
|
+
end
|
|
307
|
+
end.join("\n")
|
|
308
|
+
return plan_text unless plan_text.strip.empty?
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Fallback to string representation
|
|
312
|
+
result.to_s
|
|
313
|
+
when "mysql", "mysql2", "trilogy"
|
|
314
|
+
# MySQL returns rows
|
|
315
|
+
if result.is_a?(Array)
|
|
316
|
+
result.map { |row| row.is_a?(Hash) ? row.values.join(" | ") : row.to_s }.join("\n")
|
|
317
|
+
else
|
|
318
|
+
result.to_s
|
|
319
|
+
end
|
|
320
|
+
when "sqlite3"
|
|
321
|
+
# SQLite returns rows
|
|
322
|
+
if result.is_a?(Array)
|
|
323
|
+
result.map { |row| row.is_a?(Hash) ? row.values.join(" | ") : row.to_s }.join("\n")
|
|
324
|
+
else
|
|
325
|
+
result.to_s
|
|
326
|
+
end
|
|
327
|
+
else
|
|
328
|
+
# Generic fallback
|
|
329
|
+
result.to_s
|
|
330
|
+
end
|
|
331
|
+
rescue => e
|
|
332
|
+
# Fallback to string representation
|
|
333
|
+
result.to_s
|
|
85
334
|
end
|
|
86
335
|
|
|
87
336
|
def self.safe_query_trace(data, captured_backtrace = nil)
|
|
@@ -89,94 +338,93 @@ module ApmBro
|
|
|
89
338
|
|
|
90
339
|
# Build trace from available data fields
|
|
91
340
|
trace = []
|
|
92
|
-
|
|
341
|
+
|
|
93
342
|
# Use filename, line, and method if available
|
|
94
343
|
if data[:filename] && data[:line] && data[:method]
|
|
95
344
|
trace << "#{data[:filename]}:#{data[:line]}:in `#{data[:method]}'"
|
|
96
345
|
end
|
|
97
|
-
|
|
346
|
+
|
|
98
347
|
# Use the captured backtrace from when the query started (most accurate)
|
|
99
348
|
if captured_backtrace && captured_backtrace.is_a?(Array) && !captured_backtrace.empty?
|
|
100
349
|
# Filter to only include frames that contain "app/" (application code)
|
|
101
350
|
app_frames = captured_backtrace.select do |frame|
|
|
102
|
-
frame.include?(
|
|
351
|
+
frame.include?("app/") && !frame.include?("/vendor/")
|
|
103
352
|
end
|
|
104
|
-
|
|
353
|
+
|
|
105
354
|
caller_trace = app_frames.map do |line|
|
|
106
355
|
# Remove any potential sensitive information from file paths
|
|
107
|
-
line.gsub(/\/[^\/]*(password|secret|key|token)[^\/]*\//i,
|
|
356
|
+
line.gsub(/\/[^\/]*(password|secret|key|token)[^\/]*\//i, "/[FILTERED]/")
|
|
108
357
|
end
|
|
109
|
-
|
|
358
|
+
|
|
110
359
|
trace.concat(caller_trace)
|
|
111
360
|
else
|
|
112
361
|
# Fallback: try to get backtrace from current context
|
|
113
362
|
begin
|
|
114
363
|
# Get all available frames - we'll filter to find application code
|
|
115
364
|
all_frames = Thread.current.backtrace || []
|
|
116
|
-
|
|
365
|
+
|
|
117
366
|
if all_frames.empty?
|
|
118
367
|
# Fallback to caller_locations if backtrace is empty
|
|
119
368
|
locations = caller_locations(1, 50)
|
|
120
369
|
all_frames = locations.map { |loc| "#{loc.path}:#{loc.lineno}:in `#{loc.label}'" } if locations
|
|
121
370
|
end
|
|
122
|
-
|
|
371
|
+
|
|
123
372
|
# Filter to only include frames that contain "app/" (application code)
|
|
124
373
|
app_frames = all_frames.select do |frame|
|
|
125
|
-
frame.include?(
|
|
374
|
+
frame.include?("app/") && !frame.include?("/vendor/")
|
|
126
375
|
end
|
|
127
|
-
|
|
376
|
+
|
|
128
377
|
caller_trace = app_frames.map do |line|
|
|
129
|
-
line.gsub(/\/[^\/]*(password|secret|key|token)[^\/]*\//i,
|
|
378
|
+
line.gsub(/\/[^\/]*(password|secret|key|token)[^\/]*\//i, "/[FILTERED]/")
|
|
130
379
|
end
|
|
131
|
-
|
|
380
|
+
|
|
132
381
|
trace.concat(caller_trace)
|
|
133
|
-
rescue
|
|
382
|
+
rescue
|
|
134
383
|
# If backtrace fails, try caller as fallback
|
|
135
384
|
begin
|
|
136
385
|
caller_stack = caller(20, 50) # Get more frames to find app/ frames
|
|
137
|
-
app_frames = caller_stack.select { |frame| frame.include?(
|
|
386
|
+
app_frames = caller_stack.select { |frame| frame.include?("app/") && !frame.include?("/vendor/") }
|
|
138
387
|
caller_trace = app_frames.map do |line|
|
|
139
|
-
line.gsub(/\/[^\/]*(password|secret|key|token)[^\/]*\//i,
|
|
388
|
+
line.gsub(/\/[^\/]*(password|secret|key|token)[^\/]*\//i, "/[FILTERED]/")
|
|
140
389
|
end
|
|
141
390
|
trace.concat(caller_trace)
|
|
142
|
-
rescue
|
|
391
|
+
rescue
|
|
143
392
|
# If caller also fails, we still have the immediate location
|
|
144
393
|
end
|
|
145
394
|
end
|
|
146
395
|
end
|
|
147
|
-
|
|
396
|
+
|
|
148
397
|
# If we have a backtrace in the data, use it (but it's usually nil for SQL events)
|
|
149
398
|
if data[:backtrace] && data[:backtrace].is_a?(Array)
|
|
150
399
|
# Filter to only include frames that contain "app/"
|
|
151
400
|
app_backtrace = data[:backtrace].select do |line|
|
|
152
|
-
line.is_a?(String) && line.include?(
|
|
401
|
+
line.is_a?(String) && line.include?("app/") && !line.include?("/vendor/")
|
|
153
402
|
end
|
|
154
|
-
|
|
403
|
+
|
|
155
404
|
backtrace_trace = app_backtrace.map do |line|
|
|
156
405
|
case line
|
|
157
406
|
when String
|
|
158
|
-
line.gsub(/\/[^\/]*(password|secret|key|token)[^\/]*\//i,
|
|
407
|
+
line.gsub(/\/[^\/]*(password|secret|key|token)[^\/]*\//i, "/[FILTERED]/")
|
|
159
408
|
else
|
|
160
409
|
line.to_s
|
|
161
410
|
end
|
|
162
411
|
end
|
|
163
412
|
trace.concat(backtrace_trace)
|
|
164
413
|
end
|
|
165
|
-
|
|
414
|
+
|
|
166
415
|
# Remove duplicates and return all app/ frames (no limit)
|
|
167
416
|
trace.uniq.map do |line|
|
|
168
417
|
case line
|
|
169
418
|
when String
|
|
170
419
|
# Remove any potential sensitive information from file paths
|
|
171
|
-
line.gsub(/\/[^\/]*(password|secret|key|token)[^\/]*\//i,
|
|
420
|
+
line.gsub(/\/[^\/]*(password|secret|key|token)[^\/]*\//i, "/[FILTERED]/")
|
|
172
421
|
else
|
|
173
422
|
line.to_s
|
|
174
423
|
end
|
|
175
424
|
end
|
|
176
|
-
rescue
|
|
425
|
+
rescue
|
|
177
426
|
[]
|
|
178
427
|
end
|
|
179
|
-
|
|
180
428
|
end
|
|
181
429
|
end
|
|
182
430
|
|
|
@@ -184,36 +432,36 @@ module ApmBro
|
|
|
184
432
|
# Listener that records GC allocation deltas per SQL event id
|
|
185
433
|
class SqlAllocListener
|
|
186
434
|
def start(name, id, payload)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
backtrace_map[id] = captured_backtrace[5..-1] || captured_backtrace
|
|
198
|
-
end
|
|
199
|
-
rescue StandardError
|
|
435
|
+
map = (Thread.current[ApmBro::SqlSubscriber::THREAD_LOCAL_ALLOC_START_KEY] ||= {})
|
|
436
|
+
map[id] = GC.stat[:total_allocated_objects] if defined?(GC) && GC.respond_to?(:stat)
|
|
437
|
+
|
|
438
|
+
# Capture the backtrace at query start time (before notification system processes it)
|
|
439
|
+
# This gives us the actual call stack where the SQL was executed
|
|
440
|
+
backtrace_map = (Thread.current[ApmBro::SqlSubscriber::THREAD_LOCAL_BACKTRACE_KEY] ||= {})
|
|
441
|
+
captured_backtrace = Thread.current.backtrace
|
|
442
|
+
if captured_backtrace && captured_backtrace.is_a?(Array)
|
|
443
|
+
# Skip the first few frames (our listener code) to get to the actual query execution
|
|
444
|
+
backtrace_map[id] = captured_backtrace[5..-1] || captured_backtrace
|
|
200
445
|
end
|
|
446
|
+
rescue
|
|
201
447
|
end
|
|
202
448
|
|
|
203
449
|
def finish(name, id, payload)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return unless start_map && start_map.key?(id)
|
|
207
|
-
|
|
208
|
-
start_count = start_map.delete(id)
|
|
209
|
-
end_count = (GC.stat[:total_allocated_objects] rescue nil)
|
|
210
|
-
return unless start_count && end_count
|
|
450
|
+
start_map = Thread.current[ApmBro::SqlSubscriber::THREAD_LOCAL_ALLOC_START_KEY]
|
|
451
|
+
return unless start_map && start_map.key?(id)
|
|
211
452
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
rescue
|
|
453
|
+
start_count = start_map.delete(id)
|
|
454
|
+
end_count = begin
|
|
455
|
+
GC.stat[:total_allocated_objects]
|
|
456
|
+
rescue
|
|
457
|
+
nil
|
|
216
458
|
end
|
|
459
|
+
return unless start_count && end_count
|
|
460
|
+
|
|
461
|
+
delta = end_count - start_count
|
|
462
|
+
results = (Thread.current[ApmBro::SqlSubscriber::THREAD_LOCAL_ALLOC_RESULTS_KEY] ||= {})
|
|
463
|
+
results[id] = delta
|
|
464
|
+
rescue
|
|
217
465
|
end
|
|
218
466
|
end
|
|
219
467
|
end
|
|
@@ -12,37 +12,37 @@ module ApmBro
|
|
|
12
12
|
|
|
13
13
|
# Start SQL tracking for this request
|
|
14
14
|
if defined?(ApmBro::SqlSubscriber)
|
|
15
|
-
puts "Starting SQL tracking for request: #{env[
|
|
15
|
+
puts "Starting SQL tracking for request: #{env["REQUEST_METHOD"]} #{env["PATH_INFO"]}"
|
|
16
16
|
ApmBro::SqlSubscriber.start_request_tracking
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
# Start cache tracking for this request
|
|
20
20
|
if defined?(ApmBro::CacheSubscriber)
|
|
21
|
-
puts "Starting cache tracking for request: #{env[
|
|
21
|
+
puts "Starting cache tracking for request: #{env["REQUEST_METHOD"]} #{env["PATH_INFO"]}"
|
|
22
22
|
ApmBro::CacheSubscriber.start_request_tracking
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
# Start Redis tracking for this request
|
|
26
26
|
if defined?(ApmBro::RedisSubscriber)
|
|
27
|
-
puts "Starting redis tracking for request: #{env[
|
|
27
|
+
puts "Starting redis tracking for request: #{env["REQUEST_METHOD"]} #{env["PATH_INFO"]}"
|
|
28
28
|
ApmBro::RedisSubscriber.start_request_tracking
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
# Start view rendering tracking for this request
|
|
32
32
|
if defined?(ApmBro::ViewRenderingSubscriber)
|
|
33
|
-
puts "Starting view rendering tracking for request: #{env[
|
|
33
|
+
puts "Starting view rendering tracking for request: #{env["REQUEST_METHOD"]} #{env["PATH_INFO"]}"
|
|
34
34
|
ApmBro::ViewRenderingSubscriber.start_request_tracking
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# Start lightweight memory tracking for this request
|
|
38
38
|
if defined?(ApmBro::LightweightMemoryTracker)
|
|
39
|
-
puts "Starting lightweight memory tracking for request: #{env[
|
|
39
|
+
puts "Starting lightweight memory tracking for request: #{env["REQUEST_METHOD"]} #{env["PATH_INFO"]}"
|
|
40
40
|
ApmBro::LightweightMemoryTracker.start_request_tracking
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# Start detailed memory tracking when allocation tracking is enabled
|
|
44
44
|
if ApmBro.configuration.allocation_tracking_enabled && defined?(ApmBro::MemoryTrackingSubscriber)
|
|
45
|
-
puts "Starting detailed memory tracking for request: #{env[
|
|
45
|
+
puts "Starting detailed memory tracking for request: #{env["REQUEST_METHOD"]} #{env["PATH_INFO"]}"
|
|
46
46
|
ApmBro::MemoryTrackingSubscriber.start_request_tracking
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -53,27 +53,27 @@ module ApmBro
|
|
|
53
53
|
ensure
|
|
54
54
|
# Clean up thread-local storage
|
|
55
55
|
if defined?(ApmBro::SqlSubscriber)
|
|
56
|
-
|
|
56
|
+
Thread.current[:apm_bro_sql_queries]
|
|
57
57
|
Thread.current[:apm_bro_sql_queries] = nil
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
if defined?(ApmBro::CacheSubscriber)
|
|
61
|
-
|
|
61
|
+
Thread.current[:apm_bro_cache_events]
|
|
62
62
|
Thread.current[:apm_bro_cache_events] = nil
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
if defined?(ApmBro::RedisSubscriber)
|
|
66
|
-
|
|
66
|
+
Thread.current[:apm_bro_redis_events]
|
|
67
67
|
Thread.current[:apm_bro_redis_events] = nil
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
if defined?(ApmBro::ViewRenderingSubscriber)
|
|
71
|
-
|
|
71
|
+
Thread.current[:apm_bro_view_events]
|
|
72
72
|
Thread.current[:apm_bro_view_events] = nil
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
if defined?(ApmBro::LightweightMemoryTracker)
|
|
76
|
-
|
|
76
|
+
Thread.current[:apm_bro_lightweight_memory]
|
|
77
77
|
Thread.current[:apm_bro_lightweight_memory] = nil
|
|
78
78
|
end
|
|
79
79
|
|