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
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
|
@@ -28,41 +28,6 @@ ApmBro.configure do |cfg|
|
|
|
28
28
|
end
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
## User Email Tracking
|
|
32
|
-
|
|
33
|
-
ApmBro can track the email of the user making requests, which is useful for debugging user-specific issues and understanding user behavior patterns.
|
|
34
|
-
|
|
35
|
-
### Configuration
|
|
36
|
-
|
|
37
|
-
Enable user email tracking in your Rails configuration:
|
|
38
|
-
|
|
39
|
-
```ruby
|
|
40
|
-
# In config/application.rb or environments/*.rb
|
|
41
|
-
ApmBro.configure do |config|
|
|
42
|
-
config.user_email_tracking_enabled = true
|
|
43
|
-
end
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
### Default Email Extraction
|
|
47
|
-
|
|
48
|
-
By default, ApmBro will try to extract user email from these sources (in order of priority):
|
|
49
|
-
|
|
50
|
-
1. **`current_user.email`** - Most common in Rails apps with authentication
|
|
51
|
-
2. **Request parameters** - `user_email` or `email` in params
|
|
52
|
-
3. **HTTP headers** - `X-User-Email` or `HTTP_X_USER_EMAIL`
|
|
53
|
-
4. **Session data** - `user_email` in session
|
|
54
|
-
|
|
55
|
-
### Custom Email Extractor
|
|
56
|
-
|
|
57
|
-
In progress
|
|
58
|
-
|
|
59
|
-
### Security Considerations
|
|
60
|
-
|
|
61
|
-
- User email tracking is **disabled by default** for privacy
|
|
62
|
-
- Only enable when necessary for debugging or analytics
|
|
63
|
-
- Consider your data privacy requirements and regulations
|
|
64
|
-
- The email is included in all request payloads sent to our APM endpoint
|
|
65
|
-
|
|
66
31
|
## Request Sampling
|
|
67
32
|
|
|
68
33
|
ApmBro supports configurable request sampling to reduce the volume of metrics sent to your APM endpoint, which is useful for high-traffic applications.
|
|
@@ -143,6 +108,54 @@ ApmBro automatically tracks SQL queries executed during each request and job. Ea
|
|
|
143
108
|
- `cached` - Whether the query was cached
|
|
144
109
|
- `connection_id` - Database connection ID
|
|
145
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)
|
|
146
159
|
|
|
147
160
|
## View Rendering Tracking
|
|
148
161
|
|
|
@@ -19,18 +19,16 @@ module ApmBro
|
|
|
19
19
|
|
|
20
20
|
def self.subscribe!
|
|
21
21
|
EVENTS.each do |event_name|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
next unless Thread.current[THREAD_LOCAL_KEY]
|
|
22
|
+
ActiveSupport::Notifications.subscribe(event_name) do |name, started, finished, _unique_id, data|
|
|
23
|
+
next unless Thread.current[THREAD_LOCAL_KEY]
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
end
|
|
30
|
-
rescue StandardError
|
|
25
|
+
duration_ms = ((finished - started) * 1000.0).round(2)
|
|
26
|
+
event = build_event(name, data, duration_ms)
|
|
27
|
+
Thread.current[THREAD_LOCAL_KEY] << event if event
|
|
31
28
|
end
|
|
29
|
+
rescue
|
|
32
30
|
end
|
|
33
|
-
rescue
|
|
31
|
+
rescue
|
|
34
32
|
# Never raise from instrumentation install
|
|
35
33
|
end
|
|
36
34
|
|
|
@@ -47,7 +45,7 @@ module ApmBro
|
|
|
47
45
|
def self.build_event(name, data, duration_ms)
|
|
48
46
|
return nil unless data.is_a?(Hash)
|
|
49
47
|
|
|
50
|
-
|
|
48
|
+
{
|
|
51
49
|
event: name,
|
|
52
50
|
duration_ms: duration_ms,
|
|
53
51
|
key: safe_key(data[:key]),
|
|
@@ -57,27 +55,23 @@ module ApmBro
|
|
|
57
55
|
namespace: safe_namespace(data[:namespace]),
|
|
58
56
|
at: Time.now.utc.to_i
|
|
59
57
|
}
|
|
60
|
-
|
|
61
|
-
base
|
|
62
|
-
rescue StandardError
|
|
58
|
+
rescue
|
|
63
59
|
nil
|
|
64
60
|
end
|
|
65
61
|
|
|
66
62
|
def self.safe_key(key)
|
|
67
63
|
return nil if key.nil?
|
|
68
64
|
s = key.to_s
|
|
69
|
-
s.length > 200 ? s[0, 200] + "…" : s
|
|
70
|
-
rescue
|
|
65
|
+
(s.length > 200) ? s[0, 200] + "…" : s
|
|
66
|
+
rescue
|
|
71
67
|
nil
|
|
72
68
|
end
|
|
73
69
|
|
|
74
70
|
def self.safe_keys_count(keys)
|
|
75
71
|
if keys.respond_to?(:size)
|
|
76
72
|
keys.size
|
|
77
|
-
else
|
|
78
|
-
nil
|
|
79
73
|
end
|
|
80
|
-
rescue
|
|
74
|
+
rescue
|
|
81
75
|
nil
|
|
82
76
|
end
|
|
83
77
|
|
|
@@ -88,13 +82,13 @@ module ApmBro
|
|
|
88
82
|
else
|
|
89
83
|
store.class.name
|
|
90
84
|
end
|
|
91
|
-
rescue
|
|
85
|
+
rescue
|
|
92
86
|
nil
|
|
93
87
|
end
|
|
94
88
|
|
|
95
89
|
def self.safe_namespace(ns)
|
|
96
90
|
ns.to_s[0, 100]
|
|
97
|
-
rescue
|
|
91
|
+
rescue
|
|
98
92
|
nil
|
|
99
93
|
end
|
|
100
94
|
|
|
@@ -104,13 +98,9 @@ module ApmBro
|
|
|
104
98
|
true
|
|
105
99
|
when "cache_read.active_support"
|
|
106
100
|
!!data[:hit]
|
|
107
|
-
else
|
|
108
|
-
nil
|
|
109
101
|
end
|
|
110
|
-
rescue
|
|
102
|
+
rescue
|
|
111
103
|
nil
|
|
112
104
|
end
|
|
113
105
|
end
|
|
114
106
|
end
|
|
115
|
-
|
|
116
|
-
|
|
@@ -20,7 +20,7 @@ module ApmBro
|
|
|
20
20
|
@failure_threshold = failure_threshold
|
|
21
21
|
@recovery_timeout = recovery_timeout
|
|
22
22
|
@retry_timeout = retry_timeout
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
@state = CLOSED
|
|
25
25
|
@failure_count = 0
|
|
26
26
|
@last_failure_time = nil
|
|
@@ -43,21 +43,13 @@ module ApmBro
|
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
@state
|
|
48
|
-
end
|
|
46
|
+
attr_reader :state
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
@failure_count
|
|
52
|
-
end
|
|
48
|
+
attr_reader :failure_count
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
@last_failure_time
|
|
56
|
-
end
|
|
50
|
+
attr_reader :last_failure_time
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
@last_success_time
|
|
60
|
-
end
|
|
52
|
+
attr_reader :last_success_time
|
|
61
53
|
|
|
62
54
|
def reset!
|
|
63
55
|
@state = CLOSED
|
|
@@ -76,7 +68,7 @@ module ApmBro
|
|
|
76
68
|
|
|
77
69
|
def should_attempt_reset?
|
|
78
70
|
return false unless @last_failure_time
|
|
79
|
-
|
|
71
|
+
|
|
80
72
|
# Try to reset after recovery timeout
|
|
81
73
|
elapsed = Time.now - @last_failure_time
|
|
82
74
|
elapsed >= @recovery_timeout
|
|
@@ -86,7 +78,7 @@ module ApmBro
|
|
|
86
78
|
|
|
87
79
|
def execute_with_monitoring(&block)
|
|
88
80
|
result = block.call
|
|
89
|
-
|
|
81
|
+
|
|
90
82
|
if success?(result)
|
|
91
83
|
on_success
|
|
92
84
|
result
|
|
@@ -94,7 +86,7 @@ module ApmBro
|
|
|
94
86
|
on_failure
|
|
95
87
|
result
|
|
96
88
|
end
|
|
97
|
-
rescue
|
|
89
|
+
rescue => e
|
|
98
90
|
on_failure
|
|
99
91
|
raise e
|
|
100
92
|
end
|
|
@@ -113,7 +105,7 @@ module ApmBro
|
|
|
113
105
|
def on_failure
|
|
114
106
|
@failure_count += 1
|
|
115
107
|
@last_failure_time = Time.now
|
|
116
|
-
|
|
108
|
+
|
|
117
109
|
# If we're in half-open state and get a failure, go back to open
|
|
118
110
|
if @state == HALF_OPEN
|
|
119
111
|
@state = OPEN
|
data/lib/apm_bro/client.rb
CHANGED
|
@@ -23,7 +23,7 @@ module ApmBro
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
api_key = @configuration.resolve_api_key
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
if api_key.nil?
|
|
28
28
|
return
|
|
29
29
|
end
|
|
@@ -50,7 +50,7 @@ module ApmBro
|
|
|
50
50
|
|
|
51
51
|
def create_circuit_breaker
|
|
52
52
|
return nil unless @configuration.circuit_breaker_enabled
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
CircuitBreaker.new(
|
|
55
55
|
failure_threshold: @configuration.circuit_breaker_failure_threshold,
|
|
56
56
|
recovery_timeout: @configuration.circuit_breaker_recovery_timeout,
|
|
@@ -59,8 +59,8 @@ module ApmBro
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def make_http_request(event_name, payload, api_key)
|
|
62
|
-
endpoint_url = @configuration.respond_to?(:ruby_dev) && @configuration.ruby_dev ?
|
|
63
|
-
|
|
62
|
+
endpoint_url = (@configuration.respond_to?(:ruby_dev) && @configuration.ruby_dev) ?
|
|
63
|
+
"http://localhost:3100/apm/v1/metrics" :
|
|
64
64
|
"https://www.deadbro.com/apm/v1/metrics"
|
|
65
65
|
|
|
66
66
|
uri = URI.parse(endpoint_url)
|
|
@@ -72,43 +72,37 @@ module ApmBro
|
|
|
72
72
|
request = Net::HTTP::Post.new(uri.request_uri)
|
|
73
73
|
request["Content-Type"] = "application/json"
|
|
74
74
|
request["Authorization"] = "Bearer #{api_key}"
|
|
75
|
-
body = {
|
|
75
|
+
body = {event: event_name, payload: payload, sent_at: Time.now.utc.iso8601, revision: @configuration.resolve_deploy_id}
|
|
76
76
|
request.body = JSON.dump(body)
|
|
77
77
|
|
|
78
78
|
# Fire-and-forget using a short-lived thread to avoid blocking the request cycle.
|
|
79
79
|
Thread.new do
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
else
|
|
89
|
-
@circuit_breaker.send(:on_failure)
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
else
|
|
93
|
-
# Treat nil response as failure for circuit breaker
|
|
94
|
-
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
80
|
+
response = http.request(request)
|
|
81
|
+
|
|
82
|
+
if response
|
|
83
|
+
# Update circuit breaker based on response
|
|
84
|
+
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
85
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
86
|
+
@circuit_breaker.send(:on_success)
|
|
87
|
+
else
|
|
95
88
|
@circuit_breaker.send(:on_failure)
|
|
96
89
|
end
|
|
97
90
|
end
|
|
98
|
-
|
|
99
|
-
response
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
91
|
+
elsif @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
92
|
+
# Treat nil response as failure for circuit breaker
|
|
93
|
+
@circuit_breaker.send(:on_failure)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
response
|
|
97
|
+
rescue Timeout::Error
|
|
98
|
+
# Update circuit breaker on timeout
|
|
99
|
+
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
100
|
+
@circuit_breaker.send(:on_failure)
|
|
101
|
+
end
|
|
102
|
+
rescue
|
|
103
|
+
# Update circuit breaker on exception
|
|
104
|
+
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
105
|
+
@circuit_breaker.send(:on_failure)
|
|
112
106
|
end
|
|
113
107
|
end
|
|
114
108
|
|
|
@@ -124,5 +118,3 @@ module ApmBro
|
|
|
124
118
|
end
|
|
125
119
|
end
|
|
126
120
|
end
|
|
127
|
-
|
|
128
|
-
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module ApmBro
|
|
4
4
|
class Configuration
|
|
5
|
-
DEFAULT_ENDPOINT_PATH = "/v1/metrics"
|
|
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, :
|
|
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
|
|
@@ -19,13 +19,13 @@ module ApmBro
|
|
|
19
19
|
@circuit_breaker_failure_threshold = 3
|
|
20
20
|
@circuit_breaker_recovery_timeout = 60 # seconds
|
|
21
21
|
@circuit_breaker_retry_timeout = 300 # seconds
|
|
22
|
-
@user_email_tracking_enabled = false
|
|
23
|
-
@user_email_extractor = nil
|
|
24
22
|
@sample_rate = 100 # 100% sampling by default
|
|
25
23
|
@excluded_controllers = []
|
|
26
24
|
@excluded_jobs = []
|
|
27
25
|
@excluded_controller_actions = []
|
|
28
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
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def resolve_api_key
|
|
@@ -81,6 +81,9 @@ module ApmBro
|
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
# Prefer explicit env var, then common platform-specific var
|
|
84
|
+
apm_bro_deploy_id = ENV["APM_BRO_DEPLOY_ID"]
|
|
85
|
+
return apm_bro_deploy_id if present?(apm_bro_deploy_id)
|
|
86
|
+
|
|
84
87
|
env_val = ENV["GIT_REV"]
|
|
85
88
|
return env_val if present?(env_val)
|
|
86
89
|
|
|
@@ -91,6 +94,12 @@ module ApmBro
|
|
|
91
94
|
ApmBro.process_deploy_id
|
|
92
95
|
end
|
|
93
96
|
|
|
97
|
+
def excluded_controller?(controller_name)
|
|
98
|
+
list = resolve_excluded_controllers
|
|
99
|
+
return false if list.nil? || list.empty?
|
|
100
|
+
list.any? { |pat| match_name_or_pattern?(controller_name, pat) }
|
|
101
|
+
end
|
|
102
|
+
|
|
94
103
|
def excluded_job?(job_class_name)
|
|
95
104
|
list = resolve_excluded_jobs
|
|
96
105
|
return false if list.nil? || list.empty?
|
|
@@ -105,14 +114,58 @@ module ApmBro
|
|
|
105
114
|
end
|
|
106
115
|
|
|
107
116
|
def resolve_excluded_controller_actions
|
|
108
|
-
|
|
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?
|
|
109
130
|
|
|
110
131
|
if defined?(Rails)
|
|
111
132
|
list = fetch_from_rails_settings(%w[apm_bro excluded_controller_actions])
|
|
112
|
-
|
|
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
|
|
113
143
|
end
|
|
114
144
|
|
|
115
145
|
env = ENV["APM_BRO_EXCLUDED_CONTROLLER_ACTIONS"]
|
|
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
|
|
156
|
+
|
|
157
|
+
[]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def resolve_excluded_controllers
|
|
161
|
+
return @excluded_controllers if @excluded_controllers && !@excluded_controllers.empty?
|
|
162
|
+
|
|
163
|
+
if defined?(Rails)
|
|
164
|
+
list = fetch_from_rails_settings(%w[apm_bro excluded_controllers])
|
|
165
|
+
return Array(list) if list
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
env = ENV["APM_BRO_EXCLUDED_CONTROLLERS"]
|
|
116
169
|
return env.split(",").map(&:strip) if env && !env.strip.empty?
|
|
117
170
|
|
|
118
171
|
[]
|
|
@@ -136,29 +189,23 @@ module ApmBro
|
|
|
136
189
|
sample_rate = resolve_sample_rate
|
|
137
190
|
return true if sample_rate >= 100
|
|
138
191
|
return false if sample_rate <= 0
|
|
139
|
-
|
|
192
|
+
|
|
140
193
|
# Generate random number 1-100 and check if it's within sample rate
|
|
141
194
|
rand(1..100) <= sample_rate
|
|
142
195
|
end
|
|
143
196
|
|
|
144
197
|
def sample_rate=(value)
|
|
145
|
-
|
|
146
|
-
|
|
198
|
+
# Allow nil to use default/resolved value
|
|
199
|
+
if value.nil?
|
|
200
|
+
@sample_rate = nil
|
|
201
|
+
return
|
|
147
202
|
end
|
|
148
|
-
@sample_rate = value
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def extract_user_email(request_data)
|
|
152
|
-
ap request_data[:headers].class
|
|
153
|
-
return nil unless @user_email_tracking_enabled
|
|
154
203
|
|
|
155
|
-
#
|
|
156
|
-
|
|
157
|
-
|
|
204
|
+
# Allow 0 to disable sampling, or 1-100 for percentage
|
|
205
|
+
unless value.is_a?(Integer) && value >= 0 && value <= 100
|
|
206
|
+
raise ArgumentError, "Sample rate must be an integer between 0 and 100, got: #{value.inspect}"
|
|
158
207
|
end
|
|
159
|
-
|
|
160
|
-
# Default extraction logic
|
|
161
|
-
extract_user_email_from_request(request_data)
|
|
208
|
+
@sample_rate = value
|
|
162
209
|
end
|
|
163
210
|
|
|
164
211
|
private
|
|
@@ -171,10 +218,18 @@ module ApmBro
|
|
|
171
218
|
return false if name.nil? || pattern.nil?
|
|
172
219
|
pat = pattern.to_s
|
|
173
220
|
return !!(name.to_s == pat) unless pat.include?("*")
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
176
231
|
!!(name.to_s =~ regex)
|
|
177
|
-
rescue
|
|
232
|
+
rescue
|
|
178
233
|
false
|
|
179
234
|
end
|
|
180
235
|
|
|
@@ -182,12 +237,16 @@ module ApmBro
|
|
|
182
237
|
# Try Rails.application.config_for(:apm_bro)
|
|
183
238
|
begin
|
|
184
239
|
if Rails.respond_to?(:application) && Rails.application.respond_to?(:config_for)
|
|
185
|
-
config =
|
|
240
|
+
config = begin
|
|
241
|
+
Rails.application.config_for(:apm_bro)
|
|
242
|
+
rescue
|
|
243
|
+
nil
|
|
244
|
+
end
|
|
186
245
|
if config && config.is_a?(Hash)
|
|
187
246
|
return dig_hash(config, *Array(path_keys))
|
|
188
247
|
end
|
|
189
248
|
end
|
|
190
|
-
rescue
|
|
249
|
+
rescue
|
|
191
250
|
end
|
|
192
251
|
|
|
193
252
|
# Try Rails.application.credentials
|
|
@@ -198,7 +257,7 @@ module ApmBro
|
|
|
198
257
|
value = dig_credentials(creds, *Array(path_keys))
|
|
199
258
|
return value if present?(value)
|
|
200
259
|
end
|
|
201
|
-
rescue
|
|
260
|
+
rescue
|
|
202
261
|
end
|
|
203
262
|
|
|
204
263
|
# Try Rails.application.config.x.apm_bro.api_key
|
|
@@ -208,7 +267,7 @@ module ApmBro
|
|
|
208
267
|
config_x = x.apm_bro
|
|
209
268
|
return config_x.public_send(Array(path_keys).last) if config_x.respond_to?(Array(path_keys).last)
|
|
210
269
|
end
|
|
211
|
-
rescue
|
|
270
|
+
rescue
|
|
212
271
|
end
|
|
213
272
|
|
|
214
273
|
nil
|
|
@@ -248,42 +307,5 @@ module ApmBro
|
|
|
248
307
|
path = "/#{path}" unless path.start_with?("/")
|
|
249
308
|
base + path
|
|
250
309
|
end
|
|
251
|
-
|
|
252
|
-
def extract_user_email_from_request(request_data)
|
|
253
|
-
# Try to get user email from various common sources
|
|
254
|
-
return nil unless request_data.is_a?(Hash)
|
|
255
|
-
|
|
256
|
-
# Check for current_user.email (common in Rails apps)
|
|
257
|
-
if request_data[:current_user]&.respond_to?(:email)
|
|
258
|
-
return request_data[:current_user].email
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
# Check for user.email in params
|
|
262
|
-
if request_data[:params]&.is_a?(Hash)
|
|
263
|
-
user_email = request_data[:params]["user_email"] ||
|
|
264
|
-
request_data[:params][:user_email] ||
|
|
265
|
-
request_data[:params]["email"] ||
|
|
266
|
-
request_data[:params][:email]
|
|
267
|
-
return user_email if user_email.present?
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
# Check for user email in headers
|
|
271
|
-
if request_data[:request]&.respond_to?(:headers)
|
|
272
|
-
headers = request_data[:request].headers
|
|
273
|
-
return headers["X-User-Email"] if headers["X-User-Email"].present?
|
|
274
|
-
return headers["HTTP_X_USER_EMAIL"] if headers["HTTP_X_USER_EMAIL"].present?
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
# Check for user email in session
|
|
278
|
-
if request_data[:session]&.is_a?(Hash)
|
|
279
|
-
return request_data[:session]["user_email"] || request_data[:session][:user_email]
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
nil
|
|
283
|
-
rescue StandardError
|
|
284
|
-
nil
|
|
285
|
-
end
|
|
286
310
|
end
|
|
287
311
|
end
|
|
288
|
-
|
|
289
|
-
|