apm_bro 0.1.15 → 0.1.17

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26626be56405c7aa4561db094b0e1bf27b1469cef67275786ec1ece6e28947e3
4
- data.tar.gz: c07ec1b39ff873b5cccfa909fb726aa5971bc87b6abdae8a59de4bd4550b898b
3
+ metadata.gz: 2fde98f2cfbdbc149e9a10f3000a56c14fb35238bd09e0dba8b7cb17db7095b2
4
+ data.tar.gz: 7b995dbae659d5e24982c1c2da7b2b50e644ca4b275bbc5fef410118bfc6a385
5
5
  SHA512:
6
- metadata.gz: 8c8dc70fec2841c841e71b945f9e6db3aaf40f35bd7e750f3386130829a0733e44e91c16da05b0eeb5e52175bad335af9990942ed66385ff5926460fc98b64f9
7
- data.tar.gz: 356a4e343d70b3bac7aee65d285fabe765dbb91d0ba74c4a2b594ca6459e13b7cfc68e8a96fbe43c525f5252e4a3f0692028dbcd38c01a0d7ad0d52b6d4cbda5
6
+ metadata.gz: bcb1b5ce3f643b0b7ef971c3a219884892741664beefa28ae6c4111076e9eb393f8b10840ba655835061f9a481f07c35fc234f9e0bd787bcbceb150a08f0ba11
7
+ data.tar.gz: 9f88694746c9ac3ba2dc463481ca26796bd04306817a44b44e7a596ea2c94aad4f7bb4d84c6cffa0470b614ba09b6da47379d8ce0325b3cc9e1f191fe949c830
data/FEATURES.md ADDED
@@ -0,0 +1,338 @@
1
+ # ApmBro Feature List
2
+
3
+ A comprehensive feature list for comparing ApmBro with other APM (Application Performance Monitoring) tools.
4
+
5
+ ## Core Architecture
6
+
7
+ - **Rails Integration**: Automatic subscription to Rails events via ActiveSupport::Notifications
8
+ - **Zero-Configuration Setup**: Works out of the box with minimal configuration
9
+ - **Asynchronous Metrics Posting**: Non-blocking HTTP requests using background threads
10
+ - **Thread-Local Storage**: Per-request metric collection using thread-local variables
11
+ - **Circuit Breaker Pattern**: Built-in circuit breaker to prevent cascading failures when APM endpoint is down
12
+ - **Deploy Tracking**: Automatic deploy ID resolution from multiple sources (Rails settings, ENV vars, Heroku, Git)
13
+
14
+ ## Request Tracking
15
+
16
+ ### Controller Action Monitoring
17
+ - **Automatic Tracking**: Tracks all controller actions automatically
18
+ - **Request Duration**: Measures total request processing time
19
+ - **HTTP Method & Path**: Captures HTTP method and request path
20
+ - **Status Codes**: Tracks HTTP response status codes
21
+ - **View Runtime**: Separate tracking of view rendering time
22
+ - **Database Runtime**: Separate tracking of database query time
23
+ - **Request Parameters**: Captures request parameters (with sensitive data filtering)
24
+ - **User Agent**: Tracks user agent strings
25
+ - **User ID Extraction**: Extracts authenticated user ID (supports Warden)
26
+ - **Environment Context**: Tracks Rails environment (development, staging, production)
27
+
28
+ ### Request Sampling
29
+ - **Configurable Sample Rate**: Percentage-based sampling (1-100%)
30
+ - **Random Sampling**: Each request has random chance of being tracked
31
+ - **Consistent Per-Request**: Sampling decision applies to all metrics for a request
32
+ - **Error Override**: Errors are always tracked regardless of sampling
33
+ - **Cost Optimization**: Reduces data volume and costs for high-traffic applications
34
+
35
+ ### Exclusion Rules
36
+ - **Controller Exclusion**: Exclude entire controllers from tracking
37
+ - **Action Exclusion**: Exclude specific controller#action combinations
38
+ - **Wildcard Support**: Pattern matching with `*` wildcards (e.g., `Admin::*`, `Admin::*#*`)
39
+ - **Job Exclusion**: Exclude specific background jobs from tracking
40
+ - **Flexible Configuration**: Configure via initializer, Rails settings, or environment variables
41
+
42
+ ## SQL Query Tracking
43
+
44
+ ### Query Details
45
+ - **Full SQL Tracking**: Captures all SQL queries executed during requests and jobs
46
+ - **Query Sanitization**: Automatically sanitizes SQL to remove sensitive data
47
+ - **Query Name**: Tracks query names (e.g., "User Load", "User Update")
48
+ - **Duration Measurement**: Precise query execution time in milliseconds
49
+ - **Cache Detection**: Identifies cached queries
50
+ - **Connection ID**: Tracks database connection ID
51
+ - **Call Stack Traces**: Full backtrace showing where queries were executed
52
+ - **Object Allocations**: Optional tracking of object allocations per query
53
+
54
+ ### Query Performance Analysis
55
+ - **Slow Query Detection**: Configurable threshold for identifying slow queries
56
+ - **EXPLAIN ANALYZE**: Automatic execution plan capture for slow queries
57
+ - **Background Execution**: EXPLAIN ANALYZE runs in separate thread (non-blocking)
58
+ - **Multi-Database Support**: Works with PostgreSQL, MySQL, SQLite, and others
59
+ - **Smart Filtering**: Automatically skips transaction queries (BEGIN, COMMIT, ROLLBACK)
60
+ - **Execution Plan Details**:
61
+ - PostgreSQL: Full EXPLAIN ANALYZE with buffer usage statistics
62
+ - MySQL: EXPLAIN ANALYZE with actual execution times
63
+ - SQLite: EXPLAIN QUERY PLAN output
64
+ - **Query Optimization Insights**: Helps identify missing indexes, full table scans, JOIN issues
65
+
66
+ ## View Rendering Tracking
67
+
68
+ ### View Performance
69
+ - **Template Rendering**: Tracks main template rendering
70
+ - **Partial Rendering**: Tracks partial template rendering with cache key information
71
+ - **Collection Rendering**: Tracks collection rendering (partials in loops)
72
+ - **Rendering Duration**: Precise timing for each view component
73
+ - **Virtual Path Tracking**: Tracks view virtual paths
74
+ - **Layout Information**: Captures layout usage
75
+
76
+ ### View Analysis
77
+ - **Slow View Detection**: Identifies the slowest rendering views
78
+ - **Frequency Analysis**: Tracks most frequently rendered views
79
+ - **Cache Hit Rate**: Calculates cache hit rates for partials
80
+ - **Collection Cache Analysis**: Tracks cache hit rates for collection rendering
81
+ - **Performance Metrics**:
82
+ - Total views rendered per request
83
+ - Total view rendering duration
84
+ - Average view rendering duration
85
+ - Breakdown by view type (template, partial, collection)
86
+
87
+ ## Memory Tracking & Leak Detection
88
+
89
+ ### Lightweight Memory Tracking (Default)
90
+ - **Memory Usage Monitoring**: Tracks memory consumption per request using GC stats
91
+ - **Memory Growth Tracking**: Measures memory growth during request processing
92
+ - **GC Statistics**: Tracks garbage collection count and heap pages
93
+ - **Minimal Performance Impact**: ~0.1ms overhead per request
94
+ - **Memory Before/After**: Captures memory state at request start and end
95
+
96
+ ### Detailed Allocation Tracking (Optional)
97
+ - **Object Allocation Tracking**: Detailed tracking of object allocations (disabled by default)
98
+ - **Allocation Sampling**: Configurable sampling rate for allocations
99
+ - **Large Object Detection**: Identifies objects larger than 1MB threshold
100
+ - **Memory Snapshots**: Periodic memory snapshots during request processing
101
+ - **Object Count Tracking**: Tracks object counts before and after requests
102
+ - **Performance Impact**: ~2-5ms overhead per request (only when enabled)
103
+
104
+ ### Memory Leak Detection
105
+ - **Pattern Detection**: Detects growing memory patterns over time
106
+ - **GC Efficiency Analysis**: Monitors garbage collection effectiveness
107
+ - **Heap Page Tracking**: Tracks heap page growth
108
+ - **Request Correlation**: Correlates memory growth with specific controllers/actions
109
+
110
+ ## Background Job Tracking
111
+
112
+ ### Job Execution Monitoring
113
+ - **ActiveJob Integration**: Automatic tracking when ActiveJob is available
114
+ - **Job Class Tracking**: Tracks job class names
115
+ - **Job ID**: Captures unique job identifiers
116
+ - **Queue Name**: Tracks which queue processed the job
117
+ - **Job Arguments**: Captures job arguments (with sensitive data filtering)
118
+ - **Duration Measurement**: Precise job execution time in milliseconds
119
+ - **Status Tracking**: Tracks job status (completed or failed)
120
+
121
+ ### Job Error Tracking
122
+ - **Exception Capture**: Captures exceptions from failed jobs
123
+ - **Exception Class**: Tracks exception class names
124
+ - **Exception Messages**: Captures exception messages (truncated to 1000 chars)
125
+ - **Backtraces**: Full exception backtraces (first 50 lines)
126
+ - **SQL Query Context**: Includes SQL queries executed during failed jobs
127
+ - **Memory Context**: Includes memory usage during job execution
128
+
129
+ ### Job SQL Tracking
130
+ - **SQL Query Tracking**: Tracks all SQL queries executed during job processing
131
+ - **Query Details**: Same detailed SQL tracking as request tracking
132
+ - **Query Context**: Full context of database operations in background jobs
133
+
134
+ ## Cache Tracking
135
+
136
+ ### Cache Operations
137
+ - **Read Operations**: Tracks cache read operations
138
+ - **Write Operations**: Tracks cache write operations
139
+ - **Delete Operations**: Tracks cache delete operations
140
+ - **Existence Checks**: Tracks cache existence checks
141
+ - **Fetch Operations**: Tracks cache fetch with hit/miss detection
142
+ - **Multi-Read Operations**: Tracks cache read_multi operations
143
+ - **Multi-Write Operations**: Tracks cache write_multi operations
144
+ - **Cache Generation**: Tracks cache generation events
145
+
146
+ ### Cache Analysis
147
+ - **Cache Hit Detection**: Identifies cache hits vs misses
148
+ - **Cache Key Tracking**: Tracks cache keys (truncated to 200 chars)
149
+ - **Store Information**: Identifies which cache store was used
150
+ - **Namespace Tracking**: Tracks cache namespaces
151
+ - **Duration Measurement**: Precise timing for each cache operation
152
+ - **Hit Rate Calculation**: Calculates cache hit rates per request
153
+
154
+ ## Redis Tracking
155
+
156
+ ### Redis Command Tracking
157
+ - **Command Monitoring**: Tracks all Redis commands executed
158
+ - **Command Name**: Captures Redis command names (GET, SET, etc.)
159
+ - **Key Tracking**: Tracks Redis keys (truncated to 200 chars)
160
+ - **Argument Count**: Tracks number of arguments per command
161
+ - **Database Selection**: Tracks which Redis database is used
162
+ - **Duration Measurement**: Precise timing for each Redis command
163
+ - **Error Tracking**: Captures Redis command errors
164
+
165
+ ### Advanced Redis Features
166
+ - **Pipeline Support**: Tracks Redis pipeline operations with command counts
167
+ - **Multi/Transaction Support**: Tracks Redis MULTI/EXEC transactions
168
+ - **ActiveSupport Integration**: Subscribes to ActiveSupport::Notifications for Redis events
169
+ - **Client Instrumentation**: Direct instrumentation of Redis::Client for comprehensive coverage
170
+
171
+ ## Error Tracking
172
+
173
+ ### Exception Handling
174
+ - **Automatic Exception Capture**: Captures exceptions from controller actions
175
+ - **Exception Class**: Tracks exception class names
176
+ - **Exception Messages**: Captures exception messages (truncated to 1000 chars)
177
+ - **Full Backtraces**: Captures complete exception backtraces (first 50 lines)
178
+ - **Request Context**: Includes full request context with exceptions
179
+ - **Error Flagging**: Errors are marked and always tracked (even with sampling)
180
+
181
+ ### Error Context
182
+ - **Controller/Action**: Identifies where the error occurred
183
+ - **Request Parameters**: Includes request parameters at time of error
184
+ - **User Information**: Includes user ID if available
185
+ - **SQL Queries**: Includes SQL queries executed before error
186
+ - **Memory State**: Includes memory usage at time of error
187
+ - **Log Messages**: Includes application logs captured during request
188
+
189
+ ## HTTP Instrumentation
190
+
191
+ ### Outgoing HTTP Tracking
192
+ - **HTTP Request Tracking**: Tracks outgoing HTTP requests (via middleware)
193
+ - **Request Context**: Captures HTTP request details
194
+ - **Response Context**: Captures HTTP response details
195
+ - **Duration Measurement**: Tracks HTTP request duration
196
+
197
+ ## Configuration & Flexibility
198
+
199
+ ### Configuration Options
200
+ - **API Key Management**: Multiple sources (config, Rails credentials, ENV)
201
+ - **Endpoint Configuration**: Configurable endpoint URL
202
+ - **Timeout Settings**: Configurable open and read timeouts
203
+ - **Enable/Disable Toggle**: Can be enabled/disabled via configuration
204
+ - **Environment Detection**: Automatic Rails environment detection
205
+
206
+ ### Circuit Breaker Configuration
207
+ - **Failure Threshold**: Configurable failure threshold (default: 3)
208
+ - **Recovery Timeout**: Configurable recovery timeout (default: 60 seconds)
209
+ - **Retry Timeout**: Configurable retry timeout (default: 300 seconds)
210
+ - **Enable/Disable**: Can enable/disable circuit breaker
211
+
212
+ ### Memory Tracking Configuration
213
+ - **Memory Tracking Toggle**: Enable/disable memory tracking
214
+ - **Allocation Tracking Toggle**: Enable/disable detailed allocation tracking
215
+ - **Sampling Configuration**: Configurable request sampling rate
216
+
217
+ ### Query Analysis Configuration
218
+ - **Slow Query Threshold**: Configurable threshold in milliseconds (default: 500ms)
219
+ - **EXPLAIN ANALYZE Toggle**: Enable/disable automatic EXPLAIN ANALYZE
220
+
221
+ ## Data Safety & Privacy
222
+
223
+ ### Data Sanitization
224
+ - **SQL Sanitization**: Automatically sanitizes SQL queries
225
+ - **Parameter Filtering**: Filters sensitive parameters (password, token, secret, key)
226
+ - **Argument Truncation**: Limits and truncates job arguments
227
+ - **Key Truncation**: Truncates cache and Redis keys to 200 characters
228
+ - **Value Truncation**: Recursively truncates nested values to prevent huge payloads
229
+ - **String Limits**: Limits string values (e.g., user agent to 200 chars, messages to 1000 chars)
230
+
231
+ ### Data Limits
232
+ - **Array Limits**: Limits array sizes (e.g., first 10 job arguments, first 5 array elements)
233
+ - **Hash Limits**: Limits hash key counts (e.g., first 20 hash keys, first 30 params)
234
+ - **Backtrace Limits**: Limits backtraces to first 50 lines
235
+ - **Allocation Limits**: Limits allocations tracked per request (max 1000)
236
+
237
+ ## Performance & Reliability
238
+
239
+ ### Performance Optimizations
240
+ - **Asynchronous Posting**: Non-blocking HTTP requests
241
+ - **Lightweight Default Mode**: Minimal overhead in default configuration
242
+ - **Sampling Support**: Reduces data volume for high-traffic applications
243
+ - **Thread-Local Storage**: Efficient per-request data collection
244
+ - **Background EXPLAIN**: EXPLAIN ANALYZE runs in background thread
245
+
246
+ ### Reliability Features
247
+ - **Circuit Breaker**: Prevents cascading failures
248
+ - **Error Handling**: Comprehensive error handling to prevent instrumentation failures
249
+ - **Graceful Degradation**: Continues working even if some features fail
250
+ - **Timeout Protection**: Configurable timeouts prevent hanging requests
251
+
252
+ ## Integration & Compatibility
253
+
254
+ ### Framework Support
255
+ - **Rails Integration**: Full Rails integration via Railtie
256
+ - **ActiveSupport Notifications**: Uses ActiveSupport::Notifications for event subscription
257
+ - **ActiveRecord Integration**: Tracks ActiveRecord SQL queries
258
+ - **ActiveJob Integration**: Tracks ActiveJob background jobs
259
+ - **ActionView Integration**: Tracks ActionView rendering
260
+
261
+ ### Database Support
262
+ - **PostgreSQL**: Full support with EXPLAIN ANALYZE
263
+ - **MySQL**: Full support with EXPLAIN ANALYZE
264
+ - **SQLite**: Full support with EXPLAIN QUERY PLAN
265
+ - **Other Databases**: Basic support with standard EXPLAIN
266
+
267
+ ### Cache Store Support
268
+ - **All Cache Stores**: Works with any Rails cache store
269
+ - **Multi-Store Support**: Tracks cache operations across different stores
270
+
271
+ ### Redis Support
272
+ - **Redis Gem**: Works with redis gem
273
+ - **Client Instrumentation**: Direct instrumentation of Redis::Client
274
+ - **Pipeline Support**: Tracks Redis pipelines
275
+ - **Transaction Support**: Tracks Redis MULTI/EXEC transactions
276
+
277
+ ## Logging & Debugging
278
+
279
+ ### Application Logging
280
+ - **Log Capture**: Captures application logs during request processing
281
+ - **Log Context**: Includes logs in metric payloads
282
+ - **Debug Logging**: Optional debug logging for skipped requests
283
+
284
+ ## Deployment & Environment
285
+
286
+ ### Deploy Tracking
287
+ - **Deploy ID Resolution**: Multiple sources for deploy identification
288
+ - Explicit configuration
289
+ - Rails settings/credentials
290
+ - Environment variables (APM_BRO_DEPLOY_ID, GIT_REV)
291
+ - Heroku (HEROKU_SLUG_COMMIT)
292
+ - Process-stable UUID fallback
293
+ - **Revision Tracking**: Includes deploy/revision ID in all metric payloads
294
+
295
+ ### Environment Support
296
+ - **Rails Environment**: Automatic Rails environment detection
297
+ - **Rack Environment**: Fallback to RACK_ENV or RAILS_ENV
298
+ - **Environment Context**: Includes environment in all metric payloads
299
+
300
+ ## Data Collection & Transmission
301
+
302
+ ### Metric Payload Structure
303
+ - **Structured Data**: Well-structured JSON payloads
304
+ - **Event Names**: Descriptive event names for different metric types
305
+ - **Timestamp Tracking**: ISO8601 timestamps for all metrics
306
+ - **Metadata**: Rich metadata including environment, host, deploy ID
307
+
308
+ ### HTTP Client
309
+ - **HTTPS Support**: Secure HTTPS communication
310
+ - **Bearer Token Auth**: API key authentication via Bearer tokens
311
+ - **JSON Encoding**: JSON-encoded payloads
312
+ - **Custom Headers**: Proper Content-Type and Authorization headers
313
+
314
+ ## Comparison-Ready Features
315
+
316
+ ### Unique Differentiators
317
+ 1. **Automatic EXPLAIN ANALYZE**: Background execution plan capture for slow queries
318
+ 2. **Lightweight Memory Tracking**: Low-overhead memory monitoring by default
319
+ 3. **Comprehensive Cache Tracking**: Detailed cache operation tracking
320
+ 4. **Redis Instrumentation**: Full Redis command tracking including pipelines
321
+ 5. **View Rendering Analysis**: Detailed view performance analysis with cache hit rates
322
+ 6. **Flexible Exclusion Rules**: Wildcard support for controller/job exclusion
323
+ 7. **Request Sampling**: Configurable percentage-based sampling
324
+ 8. **Circuit Breaker**: Built-in resilience for APM endpoint failures
325
+ 9. **Multi-Source Configuration**: Flexible configuration from multiple sources
326
+ 10. **Deploy Tracking**: Automatic deploy ID resolution from multiple sources
327
+
328
+ ### Standard APM Features
329
+ - Request/response tracking
330
+ - SQL query tracking
331
+ - Error tracking
332
+ - Background job tracking
333
+ - Memory tracking
334
+ - Performance metrics
335
+ - Exception handling
336
+ - User context
337
+ - Environment tracking
338
+
data/README.md CHANGED
@@ -99,6 +99,56 @@ Notes:
99
99
  - Wildcards `*` are supported for controller and action (e.g., `Admin::*#*`).
100
100
  - Matching is done against full names like `UsersController`, `Admin::ReportsController#index`, `MyJob`.
101
101
 
102
+ ## Exclusive Tracking (Whitelist Mode)
103
+
104
+ You can configure ApmBro to **only** track specific controllers, actions, or jobs. This is useful when you want to focus monitoring on a subset of your application.
105
+
106
+ ### Configuration
107
+
108
+ ```ruby
109
+ ApmBro.configure do |config|
110
+ # Only track these specific controller actions
111
+ config.exclusive_controller_actions = [
112
+ "UsersController#show",
113
+ "UsersController#index",
114
+ "Admin::ReportsController#*", # all actions in this controller
115
+ "Api::*#*" # all actions in all Api controllers
116
+ ]
117
+
118
+ # Only track these specific jobs
119
+ config.exclusive_jobs = [
120
+ "PaymentProcessingJob",
121
+ "EmailDeliveryJob",
122
+ "Admin::*" # all jobs in Admin namespace
123
+ ]
124
+ end
125
+ ```
126
+
127
+ ### How It Works
128
+
129
+ - **If `exclusive_controller_actions` or `exclusive_jobs` is empty/not defined**: All controllers/actions/jobs are tracked (default behavior)
130
+ - **If `exclusive_controller_actions` or `exclusive_jobs` is defined with values**: Only matching controllers/actions/jobs are tracked
131
+ - **Exclusion takes precedence**: If something is in both `excluded_*` and `exclusive_*`, it will be excluded (exclusion is checked first)
132
+
133
+ ### Use Cases
134
+
135
+ - **Focus on Critical Paths**: Monitor only your most important endpoints
136
+ - **Cost Optimization**: Track only specific high-value operations
137
+ - **Debugging**: Temporarily focus on specific controllers/jobs during investigation
138
+ - **Compliance**: Track only operations that require monitoring for compliance reasons
139
+
140
+ ### Environment Variables
141
+
142
+ You can also configure exclusive tracking via environment variables:
143
+
144
+ ```bash
145
+ # Comma-separated list of controller#action patterns
146
+ APM_BRO_EXCLUSIVE_CONTROLLER_ACTIONS="UsersController#show,Admin::*#*"
147
+
148
+ # Comma-separated list of job patterns
149
+ APM_BRO_EXCLUSIVE_JOBS="PaymentProcessingJob,EmailDeliveryJob"
150
+ ```
151
+
102
152
  ## SQL Query Tracking
103
153
 
104
154
  ApmBro automatically tracks SQL queries executed during each request and job. Each request will include a `sql_queries` array containing:
@@ -108,6 +158,54 @@ ApmBro automatically tracks SQL queries executed during each request and job. Ea
108
158
  - `cached` - Whether the query was cached
109
159
  - `connection_id` - Database connection ID
110
160
  - `trace` - Call stack showing where the query was executed
161
+ - `explain_plan` - Query execution plan (when EXPLAIN ANALYZE is enabled, see below)
162
+
163
+ ## Automatic EXPLAIN ANALYZE for Slow Queries
164
+
165
+ 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.
166
+
167
+ ### How It Works
168
+
169
+ - **Automatic Detection**: When a query exceeds the configured threshold, ApmBro automatically captures its execution plan
170
+ - **Background Execution**: EXPLAIN ANALYZE runs in a separate thread using a dedicated database connection, so it never blocks your application
171
+ - **Database Support**: Works with PostgreSQL, MySQL, SQLite, and other databases
172
+ - **Smart Filtering**: Automatically skips transaction queries (BEGIN, COMMIT, ROLLBACK) and other queries that don't benefit from EXPLAIN
173
+
174
+ ### Configuration
175
+
176
+ - **`explain_analyze_enabled`** (default: `false`) - Set to `true` to enable automatic EXPLAIN ANALYZE
177
+ - **`slow_query_threshold_ms`** (default: `500`) - Queries taking longer than this threshold will have their execution plan captured
178
+
179
+ ### Example Configuration
180
+
181
+ ```ruby
182
+ ApmBro.configure do |config|
183
+ config.api_key = ENV['APM_BRO_API_KEY']
184
+ config.enabled = true
185
+
186
+ # Enable EXPLAIN ANALYZE for queries slower than 500ms
187
+ config.explain_analyze_enabled = true
188
+ config.slow_query_threshold_ms = 500
189
+
190
+ # Or use a higher threshold for production
191
+ # config.slow_query_threshold_ms = 1000 # Only explain queries > 1 second
192
+ end
193
+ ```
194
+
195
+ ### What You Get
196
+
197
+ When a slow query is detected, the `explain_plan` field in the SQL query data will contain:
198
+ - **PostgreSQL**: Full EXPLAIN ANALYZE output with buffer usage statistics
199
+ - **MySQL**: EXPLAIN ANALYZE output showing actual execution times
200
+ - **SQLite**: EXPLAIN QUERY PLAN output
201
+ - **Other databases**: Standard EXPLAIN output
202
+
203
+ This execution plan helps you:
204
+ - Identify missing indexes
205
+ - Understand query execution order
206
+ - Spot full table scans
207
+ - Optimize JOIN operations
208
+ - Analyze buffer and cache usage (PostgreSQL)
111
209
 
112
210
  ## View Rendering Tracking
113
211
 
@@ -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, :exclusive_controller_actions, :exclusive_jobs, :deploy_id, :slow_query_threshold_ms, :explain_analyze_enabled
8
8
 
9
9
  def initialize
10
10
  @api_key = nil
@@ -23,7 +23,11 @@ module ApmBro
23
23
  @excluded_controllers = []
24
24
  @excluded_jobs = []
25
25
  @excluded_controller_actions = []
26
+ @exclusive_controller_actions = []
27
+ @exclusive_jobs = []
26
28
  @deploy_id = resolve_deploy_id
29
+ @slow_query_threshold_ms = 500 # Default: 500ms
30
+ @explain_analyze_enabled = false # Enable EXPLAIN ANALYZE for slow queries by default
27
31
  end
28
32
 
29
33
  def resolve_api_key
@@ -104,6 +108,12 @@ module ApmBro
104
108
  list.any? { |pat| match_name_or_pattern?(job_class_name, pat) }
105
109
  end
106
110
 
111
+ def exclusive_job?(job_class_name)
112
+ list = resolve_exclusive_jobs
113
+ return true if list.nil? || list.empty? # If not defined, allow all (default behavior)
114
+ list.any? { |pat| match_name_or_pattern?(job_class_name, pat) }
115
+ end
116
+
107
117
  def excluded_controller_action?(controller_name, action_name)
108
118
  list = resolve_excluded_controller_actions
109
119
  return false if list.nil? || list.empty?
@@ -111,16 +121,53 @@ module ApmBro
111
121
  list.any? { |pat| match_name_or_pattern?(target, pat) }
112
122
  end
113
123
 
124
+ def exclusive_controller_action?(controller_name, action_name)
125
+ list = resolve_exclusive_controller_actions
126
+ return true if list.nil? || list.empty? # If not defined, allow all (default behavior)
127
+ target = "#{controller_name}##{action_name}"
128
+ list.any? { |pat| match_name_or_pattern?(target, pat) }
129
+ end
130
+
114
131
  def resolve_excluded_controller_actions
115
- return @excluded_controller_actions if @excluded_controller_actions && !@excluded_controller_actions.empty?
132
+ # Collect patterns from @excluded_controller_actions
133
+ patterns = []
134
+ if @excluded_controller_actions && !@excluded_controller_actions.empty?
135
+ patterns.concat(Array(@excluded_controller_actions))
136
+ end
137
+
138
+ # Also check @excluded_controllers for patterns containing '#' (controller action patterns)
139
+ if @excluded_controllers && !@excluded_controllers.empty?
140
+ action_patterns = Array(@excluded_controllers).select { |pat| pat.to_s.include?("#") }
141
+ patterns.concat(action_patterns)
142
+ end
143
+
144
+ return patterns if !patterns.empty?
116
145
 
117
146
  if defined?(Rails)
118
147
  list = fetch_from_rails_settings(%w[apm_bro excluded_controller_actions])
119
- return Array(list) if list
148
+ if list
149
+ rails_patterns = Array(list)
150
+ # Also check excluded_controllers from Rails settings for action patterns
151
+ controllers_list = fetch_from_rails_settings(%w[apm_bro excluded_controllers])
152
+ if controllers_list
153
+ action_patterns = Array(controllers_list).select { |pat| pat.to_s.include?("#") }
154
+ rails_patterns.concat(action_patterns)
155
+ end
156
+ return rails_patterns if !rails_patterns.empty?
157
+ end
120
158
  end
121
159
 
122
160
  env = ENV["APM_BRO_EXCLUDED_CONTROLLER_ACTIONS"]
123
- return env.split(",").map(&:strip) if env && !env.strip.empty?
161
+ if env && !env.strip.empty?
162
+ env_patterns = env.split(",").map(&:strip)
163
+ # Also check excluded_controllers env var for action patterns
164
+ controllers_env = ENV["APM_BRO_EXCLUDED_CONTROLLERS"]
165
+ if controllers_env && !controllers_env.strip.empty?
166
+ action_patterns = controllers_env.split(",").map(&:strip).select { |pat| pat.include?("#") }
167
+ env_patterns.concat(action_patterns)
168
+ end
169
+ return env_patterns if !env_patterns.empty?
170
+ end
124
171
 
125
172
  []
126
173
  end
@@ -153,6 +200,58 @@ module ApmBro
153
200
  []
154
201
  end
155
202
 
203
+ def resolve_exclusive_controller_actions
204
+ # Collect patterns from @exclusive_controller_actions
205
+ patterns = []
206
+ if @exclusive_controller_actions && !@exclusive_controller_actions.empty?
207
+ patterns.concat(Array(@exclusive_controller_actions))
208
+ end
209
+
210
+ return patterns if !patterns.empty?
211
+
212
+ if defined?(Rails)
213
+ list = fetch_from_rails_settings(%w[apm_bro exclusive_controller_actions])
214
+ if list
215
+ rails_patterns = Array(list)
216
+ return rails_patterns if !rails_patterns.empty?
217
+ end
218
+ end
219
+
220
+ env = ENV["APM_BRO_EXCLUSIVE_CONTROLLER_ACTIONS"]
221
+ if env && !env.strip.empty?
222
+ env_patterns = env.split(",").map(&:strip)
223
+ return env_patterns if !env_patterns.empty?
224
+ end
225
+
226
+ []
227
+ end
228
+
229
+ def resolve_exclusive_jobs
230
+ # Collect patterns from @exclusive_jobs
231
+ patterns = []
232
+ if @exclusive_jobs && !@exclusive_jobs.empty?
233
+ patterns.concat(Array(@exclusive_jobs))
234
+ end
235
+
236
+ return patterns if !patterns.empty?
237
+
238
+ if defined?(Rails)
239
+ list = fetch_from_rails_settings(%w[apm_bro exclusive_jobs])
240
+ if list
241
+ rails_patterns = Array(list)
242
+ return rails_patterns if !rails_patterns.empty?
243
+ end
244
+ end
245
+
246
+ env = ENV["APM_BRO_EXCLUSIVE_JOBS"]
247
+ if env && !env.strip.empty?
248
+ env_patterns = env.split(",").map(&:strip)
249
+ return env_patterns if !env_patterns.empty?
250
+ end
251
+
252
+ []
253
+ end
254
+
156
255
  def should_sample?
157
256
  sample_rate = resolve_sample_rate
158
257
  return true if sample_rate >= 100
@@ -186,8 +285,16 @@ module ApmBro
186
285
  return false if name.nil? || pattern.nil?
187
286
  pat = pattern.to_s
188
287
  return !!(name.to_s == pat) unless pat.include?("*")
189
- # Convert simple wildcard pattern (e.g., "Admin::*") to regex
190
- regex = Regexp.new("^" + Regexp.escape(pat).gsub("\\*", "[^:]*") + "$")
288
+
289
+ # For controller action patterns (containing '#'), use .* to match any characters including colons
290
+ # For controller-only patterns, use [^:]* to match namespace segments
291
+ if pat.include?("#")
292
+ # Controller action pattern: allow * to match any characters including colons
293
+ regex = Regexp.new("^" + Regexp.escape(pat).gsub("\\*", ".*") + "$")
294
+ else
295
+ # Controller-only pattern: use [^:]* to match namespace segments
296
+ regex = Regexp.new("^" + Regexp.escape(pat).gsub("\\*", "[^:]*") + "$")
297
+ end
191
298
  !!(name.to_s =~ regex)
192
299
  rescue
193
300
  false
@@ -19,6 +19,10 @@ module ApmBro
19
19
  if ApmBro.configuration.excluded_job?(job_class_name)
20
20
  next
21
21
  end
22
+ # If exclusive_jobs is defined and not empty, only track matching jobs
23
+ unless ApmBro.configuration.exclusive_job?(job_class_name)
24
+ next
25
+ end
22
26
  rescue
23
27
  end
24
28
 
@@ -82,6 +86,10 @@ module ApmBro
82
86
  if ApmBro.configuration.excluded_job?(job_class_name)
83
87
  next
84
88
  end
89
+ # If exclusive_jobs is defined and not empty, only track matching jobs
90
+ unless ApmBro.configuration.exclusive_job?(job_class_name)
91
+ next
92
+ end
85
93
  rescue
86
94
  end
87
95
 
@@ -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(data[:sql]),
48
+ sql: sanitize_sql(original_sql),
45
49
  name: data[:name],
46
- duration_ms: ((finished - started) * 1000.0).round(2),
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
 
@@ -9,12 +9,17 @@ module ApmBro
9
9
  def self.subscribe!(client: Client.new)
10
10
  ActiveSupport::Notifications.subscribe(EVENT_NAME) do |name, started, finished, _unique_id, data|
11
11
  # Skip excluded controllers or controller#action pairs
12
+ # Also check exclusive_controller_actions - if defined, only track those
12
13
  begin
13
14
  controller_name = data[:controller].to_s
14
15
  action_name = data[:action].to_s
15
16
  if ApmBro.configuration.excluded_controller_action?(controller_name, action_name)
16
17
  next
17
18
  end
19
+ # If exclusive_controller_actions is defined and not empty, only track matching actions
20
+ unless ApmBro.configuration.exclusive_controller_action?(controller_name, action_name)
21
+ next
22
+ end
18
23
  rescue
19
24
  end
20
25
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ApmBro
4
- VERSION = "0.1.15"
4
+ VERSION = "0.1.17"
5
5
  end
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apm_bro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.15
4
+ version: 0.1.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emanuel Comsa
@@ -18,6 +18,7 @@ extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
20
  - CHANGELOG.md
21
+ - FEATURES.md
21
22
  - README.md
22
23
  - lib/apm_bro.rb
23
24
  - lib/apm_bro/cache_subscriber.rb