apm_bro 0.1.15 → 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 -0
- data/lib/apm_bro/configuration.rb +46 -6
- data/lib/apm_bro/sql_subscriber.rb +249 -2
- data/lib/apm_bro/version.rb +1 -1
- data/lib/apm_bro.rb +9 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7bf4fc110108b143a2f2e608090ae8c75a5882f8634ef1c01d0ddb949d41e54a
|
|
4
|
+
data.tar.gz: 7c5bd6915f805032ca63b73ab9325ce64b262d8489c2842b45c28a02e84b961d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 601ed7213bb2dc1b551ba8d8e47944492bd67b727b0747aaad9c60b5ce13fb2b728b218a43eb0775e1479f5684ea1721dd0b9310bd25d2ccfcfcfad8cfc9e439
|
|
7
|
+
data.tar.gz: b8a2e25e5c0369d7bd58fe058ef7d524bdc42c7cc22739d30f9ad0956338370f63578aa10688869c817148451b46d7b9e1dab8835c4e7f33531cd9146a04ed88
|
data/README.md
CHANGED
|
@@ -108,6 +108,54 @@ ApmBro automatically tracks SQL queries executed during each request and job. Ea
|
|
|
108
108
|
- `cached` - Whether the query was cached
|
|
109
109
|
- `connection_id` - Database connection ID
|
|
110
110
|
- `trace` - Call stack showing where the query was executed
|
|
111
|
+
- `explain_plan` - Query execution plan (when EXPLAIN ANALYZE is enabled, see below)
|
|
112
|
+
|
|
113
|
+
## Automatic EXPLAIN ANALYZE for Slow Queries
|
|
114
|
+
|
|
115
|
+
ApmBro can automatically run `EXPLAIN ANALYZE` on slow SQL queries to help you understand query performance and identify optimization opportunities. This feature runs in the background and doesn't block your application requests.
|
|
116
|
+
|
|
117
|
+
### How It Works
|
|
118
|
+
|
|
119
|
+
- **Automatic Detection**: When a query exceeds the configured threshold, ApmBro automatically captures its execution plan
|
|
120
|
+
- **Background Execution**: EXPLAIN ANALYZE runs in a separate thread using a dedicated database connection, so it never blocks your application
|
|
121
|
+
- **Database Support**: Works with PostgreSQL, MySQL, SQLite, and other databases
|
|
122
|
+
- **Smart Filtering**: Automatically skips transaction queries (BEGIN, COMMIT, ROLLBACK) and other queries that don't benefit from EXPLAIN
|
|
123
|
+
|
|
124
|
+
### Configuration
|
|
125
|
+
|
|
126
|
+
- **`explain_analyze_enabled`** (default: `false`) - Set to `true` to enable automatic EXPLAIN ANALYZE
|
|
127
|
+
- **`slow_query_threshold_ms`** (default: `500`) - Queries taking longer than this threshold will have their execution plan captured
|
|
128
|
+
|
|
129
|
+
### Example Configuration
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
ApmBro.configure do |config|
|
|
133
|
+
config.api_key = ENV['APM_BRO_API_KEY']
|
|
134
|
+
config.enabled = true
|
|
135
|
+
|
|
136
|
+
# Enable EXPLAIN ANALYZE for queries slower than 500ms
|
|
137
|
+
config.explain_analyze_enabled = true
|
|
138
|
+
config.slow_query_threshold_ms = 500
|
|
139
|
+
|
|
140
|
+
# Or use a higher threshold for production
|
|
141
|
+
# config.slow_query_threshold_ms = 1000 # Only explain queries > 1 second
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### What You Get
|
|
146
|
+
|
|
147
|
+
When a slow query is detected, the `explain_plan` field in the SQL query data will contain:
|
|
148
|
+
- **PostgreSQL**: Full EXPLAIN ANALYZE output with buffer usage statistics
|
|
149
|
+
- **MySQL**: EXPLAIN ANALYZE output showing actual execution times
|
|
150
|
+
- **SQLite**: EXPLAIN QUERY PLAN output
|
|
151
|
+
- **Other databases**: Standard EXPLAIN output
|
|
152
|
+
|
|
153
|
+
This execution plan helps you:
|
|
154
|
+
- Identify missing indexes
|
|
155
|
+
- Understand query execution order
|
|
156
|
+
- Spot full table scans
|
|
157
|
+
- Optimize JOIN operations
|
|
158
|
+
- Analyze buffer and cache usage (PostgreSQL)
|
|
111
159
|
|
|
112
160
|
## View Rendering Tracking
|
|
113
161
|
|
|
@@ -4,7 +4,7 @@ module ApmBro
|
|
|
4
4
|
class Configuration
|
|
5
5
|
DEFAULT_ENDPOINT_PATH = "/v1/metrics"
|
|
6
6
|
|
|
7
|
-
attr_accessor :api_key, :endpoint_url, :open_timeout, :read_timeout, :enabled, :ruby_dev, :memory_tracking_enabled, :allocation_tracking_enabled, :circuit_breaker_enabled, :circuit_breaker_failure_threshold, :circuit_breaker_recovery_timeout, :circuit_breaker_retry_timeout, :sample_rate, :excluded_controllers, :excluded_jobs, :excluded_controller_actions, :deploy_id
|
|
7
|
+
attr_accessor :api_key, :endpoint_url, :open_timeout, :read_timeout, :enabled, :ruby_dev, :memory_tracking_enabled, :allocation_tracking_enabled, :circuit_breaker_enabled, :circuit_breaker_failure_threshold, :circuit_breaker_recovery_timeout, :circuit_breaker_retry_timeout, :sample_rate, :excluded_controllers, :excluded_jobs, :excluded_controller_actions, :deploy_id, :slow_query_threshold_ms, :explain_analyze_enabled
|
|
8
8
|
|
|
9
9
|
def initialize
|
|
10
10
|
@api_key = nil
|
|
@@ -24,6 +24,8 @@ module ApmBro
|
|
|
24
24
|
@excluded_jobs = []
|
|
25
25
|
@excluded_controller_actions = []
|
|
26
26
|
@deploy_id = resolve_deploy_id
|
|
27
|
+
@slow_query_threshold_ms = 500 # Default: 500ms
|
|
28
|
+
@explain_analyze_enabled = false # Enable EXPLAIN ANALYZE for slow queries by default
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
def resolve_api_key
|
|
@@ -112,15 +114,45 @@ module ApmBro
|
|
|
112
114
|
end
|
|
113
115
|
|
|
114
116
|
def resolve_excluded_controller_actions
|
|
115
|
-
|
|
117
|
+
# Collect patterns from @excluded_controller_actions
|
|
118
|
+
patterns = []
|
|
119
|
+
if @excluded_controller_actions && !@excluded_controller_actions.empty?
|
|
120
|
+
patterns.concat(Array(@excluded_controller_actions))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Also check @excluded_controllers for patterns containing '#' (controller action patterns)
|
|
124
|
+
if @excluded_controllers && !@excluded_controllers.empty?
|
|
125
|
+
action_patterns = Array(@excluded_controllers).select { |pat| pat.to_s.include?("#") }
|
|
126
|
+
patterns.concat(action_patterns)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
return patterns if !patterns.empty?
|
|
116
130
|
|
|
117
131
|
if defined?(Rails)
|
|
118
132
|
list = fetch_from_rails_settings(%w[apm_bro excluded_controller_actions])
|
|
119
|
-
|
|
133
|
+
if list
|
|
134
|
+
rails_patterns = Array(list)
|
|
135
|
+
# Also check excluded_controllers from Rails settings for action patterns
|
|
136
|
+
controllers_list = fetch_from_rails_settings(%w[apm_bro excluded_controllers])
|
|
137
|
+
if controllers_list
|
|
138
|
+
action_patterns = Array(controllers_list).select { |pat| pat.to_s.include?("#") }
|
|
139
|
+
rails_patterns.concat(action_patterns)
|
|
140
|
+
end
|
|
141
|
+
return rails_patterns if !rails_patterns.empty?
|
|
142
|
+
end
|
|
120
143
|
end
|
|
121
144
|
|
|
122
145
|
env = ENV["APM_BRO_EXCLUDED_CONTROLLER_ACTIONS"]
|
|
123
|
-
|
|
146
|
+
if env && !env.strip.empty?
|
|
147
|
+
env_patterns = env.split(",").map(&:strip)
|
|
148
|
+
# Also check excluded_controllers env var for action patterns
|
|
149
|
+
controllers_env = ENV["APM_BRO_EXCLUDED_CONTROLLERS"]
|
|
150
|
+
if controllers_env && !controllers_env.strip.empty?
|
|
151
|
+
action_patterns = controllers_env.split(",").map(&:strip).select { |pat| pat.include?("#") }
|
|
152
|
+
env_patterns.concat(action_patterns)
|
|
153
|
+
end
|
|
154
|
+
return env_patterns if !env_patterns.empty?
|
|
155
|
+
end
|
|
124
156
|
|
|
125
157
|
[]
|
|
126
158
|
end
|
|
@@ -186,8 +218,16 @@ module ApmBro
|
|
|
186
218
|
return false if name.nil? || pattern.nil?
|
|
187
219
|
pat = pattern.to_s
|
|
188
220
|
return !!(name.to_s == pat) unless pat.include?("*")
|
|
189
|
-
|
|
190
|
-
|
|
221
|
+
|
|
222
|
+
# For controller action patterns (containing '#'), use .* to match any characters including colons
|
|
223
|
+
# For controller-only patterns, use [^:]* to match namespace segments
|
|
224
|
+
if pat.include?("#")
|
|
225
|
+
# Controller action pattern: allow * to match any characters including colons
|
|
226
|
+
regex = Regexp.new("^" + Regexp.escape(pat).gsub("\\*", ".*") + "$")
|
|
227
|
+
else
|
|
228
|
+
# Controller-only pattern: use [^:]* to match namespace segments
|
|
229
|
+
regex = Regexp.new("^" + Regexp.escape(pat).gsub("\\*", "[^:]*") + "$")
|
|
230
|
+
end
|
|
191
231
|
!!(name.to_s =~ regex)
|
|
192
232
|
rescue
|
|
193
233
|
false
|
|
@@ -13,6 +13,7 @@ module ApmBro
|
|
|
13
13
|
THREAD_LOCAL_ALLOC_START_KEY = :apm_bro_sql_alloc_start
|
|
14
14
|
THREAD_LOCAL_ALLOC_RESULTS_KEY = :apm_bro_sql_alloc_results
|
|
15
15
|
THREAD_LOCAL_BACKTRACE_KEY = :apm_bro_sql_backtraces
|
|
16
|
+
THREAD_LOCAL_EXPLAIN_PENDING_KEY = :apm_bro_explain_pending
|
|
16
17
|
|
|
17
18
|
def self.subscribe!
|
|
18
19
|
# Subscribe with a start/finish listener to measure allocations per query
|
|
@@ -40,15 +41,26 @@ module ApmBro
|
|
|
40
41
|
rescue
|
|
41
42
|
end
|
|
42
43
|
|
|
44
|
+
duration_ms = ((finished - started) * 1000.0).round(2)
|
|
45
|
+
original_sql = data[:sql]
|
|
46
|
+
|
|
43
47
|
query_info = {
|
|
44
|
-
sql: sanitize_sql(
|
|
48
|
+
sql: sanitize_sql(original_sql),
|
|
45
49
|
name: data[:name],
|
|
46
|
-
duration_ms:
|
|
50
|
+
duration_ms: duration_ms,
|
|
47
51
|
cached: data[:cached] || false,
|
|
48
52
|
connection_id: data[:connection_id],
|
|
49
53
|
trace: safe_query_trace(data, captured_backtrace),
|
|
50
54
|
allocations: allocations
|
|
51
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
|
+
|
|
52
64
|
# Add to thread-local storage
|
|
53
65
|
Thread.current[THREAD_LOCAL_KEY] << query_info
|
|
54
66
|
end
|
|
@@ -59,17 +71,43 @@ module ApmBro
|
|
|
59
71
|
Thread.current[THREAD_LOCAL_ALLOC_START_KEY] = {}
|
|
60
72
|
Thread.current[THREAD_LOCAL_ALLOC_RESULTS_KEY] = {}
|
|
61
73
|
Thread.current[THREAD_LOCAL_BACKTRACE_KEY] = {}
|
|
74
|
+
Thread.current[THREAD_LOCAL_EXPLAIN_PENDING_KEY] = []
|
|
62
75
|
end
|
|
63
76
|
|
|
64
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
|
|
65
84
|
queries = Thread.current[THREAD_LOCAL_KEY]
|
|
85
|
+
|
|
66
86
|
Thread.current[THREAD_LOCAL_KEY] = nil
|
|
67
87
|
Thread.current[THREAD_LOCAL_ALLOC_START_KEY] = nil
|
|
68
88
|
Thread.current[THREAD_LOCAL_ALLOC_RESULTS_KEY] = nil
|
|
69
89
|
Thread.current[THREAD_LOCAL_BACKTRACE_KEY] = nil
|
|
90
|
+
Thread.current[THREAD_LOCAL_EXPLAIN_PENDING_KEY] = nil
|
|
70
91
|
queries || []
|
|
71
92
|
end
|
|
72
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
|
+
|
|
73
111
|
def self.sanitize_sql(sql)
|
|
74
112
|
return sql unless sql.is_a?(String)
|
|
75
113
|
|
|
@@ -86,6 +124,215 @@ module ApmBro
|
|
|
86
124
|
(sql.length > 1000) ? sql[0..1000] + "..." : sql
|
|
87
125
|
end
|
|
88
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
|
|
334
|
+
end
|
|
335
|
+
|
|
89
336
|
def self.safe_query_trace(data, captured_backtrace = nil)
|
|
90
337
|
return [] unless data.is_a?(Hash)
|
|
91
338
|
|
data/lib/apm_bro/version.rb
CHANGED
data/lib/apm_bro.rb
CHANGED
|
@@ -51,4 +51,13 @@ module ApmBro
|
|
|
51
51
|
def self.logger
|
|
52
52
|
@logger ||= Logger.new
|
|
53
53
|
end
|
|
54
|
+
|
|
55
|
+
# Returns the current environment (Rails.env or ENV fallback)
|
|
56
|
+
def self.env
|
|
57
|
+
if defined?(Rails) && Rails.respond_to?(:env)
|
|
58
|
+
Rails.env
|
|
59
|
+
else
|
|
60
|
+
ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
54
63
|
end
|