apm_bro 0.1.9 → 0.1.11
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/CHANGELOG.md +5 -0
- data/README.md +211 -0
- data/lib/apm_bro/cache_subscriber.rb +116 -0
- data/lib/apm_bro/circuit_breaker.rb +125 -0
- data/lib/apm_bro/client.rb +128 -0
- data/lib/apm_bro/configuration.rb +289 -0
- data/lib/apm_bro/error_middleware.rb +125 -0
- data/lib/apm_bro/http_instrumentation.rb +107 -0
- data/lib/apm_bro/job_sql_tracking_middleware.rb +14 -0
- data/lib/apm_bro/job_subscriber.rb +157 -0
- data/lib/apm_bro/lightweight_memory_tracker.rb +63 -0
- data/lib/apm_bro/memory_helpers.rb +87 -0
- data/lib/apm_bro/memory_leak_detector.rb +196 -0
- data/lib/apm_bro/memory_tracking_subscriber.rb +353 -0
- data/lib/apm_bro/railtie.rb +99 -0
- data/lib/apm_bro/redis_subscriber.rb +285 -0
- data/lib/apm_bro/sql_subscriber.rb +161 -0
- data/lib/apm_bro/sql_tracking_middleware.rb +81 -0
- data/lib/apm_bro/subscriber.rb +334 -0
- data/lib/apm_bro/version.rb +5 -0
- data/lib/apm_bro/view_rendering_subscriber.rb +151 -0
- data/lib/apm_bro.rb +48 -0
- metadata +24 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1763fe75b6723866fbf061cdc8a978105e3a8bd1be612c2dc0937865c576a1da
|
|
4
|
+
data.tar.gz: e1bad4ee3231e2a0783994983986e040cec36314912e30f402c044a41f47bdea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 070b54479382d040aebd53e2927d0ebd0aff1a0e0e3a8b4ad4045cc674cec680fd64f2cb279c18412aeff347a34eb098de6bb4fcf227094d2a11e61c12b33b67
|
|
7
|
+
data.tar.gz: 17894ab0bb4446d2fb90848ee0f1996683390710e6f81c198fa357f31822a6577fae98b70749feb46f1e2fbba70e71dd38bbb4886252114164b5d8dcac96a652
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# ApmBro (Beta Version)
|
|
2
|
+
|
|
3
|
+
Minimal APM for Rails apps. Automatically measures each controller action's total time, tracks SQL queries, monitors view rendering performance, tracks memory usage and detects leaks, monitors background jobs, and posts metrics to a remote endpoint with an API key read from your app's settings/credentials/env.
|
|
4
|
+
|
|
5
|
+
To use the gem you need to have a free account with [DeadBro - Rails APM](https://www.deadbro.com)
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add to your Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "apm_bro", git: "https://github.com/rubydevro/apm_bro.git"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
By default, if Rails is present, ApmBro auto-subscribes to `process_action.action_controller` and posts metrics asynchronously.
|
|
18
|
+
|
|
19
|
+
### Configuration settings
|
|
20
|
+
|
|
21
|
+
You can set via an initializer:
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
ApmBro.configure do |cfg|
|
|
26
|
+
cfg.api_key = ENV["APM_BRO_API_KEY"]
|
|
27
|
+
cfg.enabled = true
|
|
28
|
+
end
|
|
29
|
+
```
|
|
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
|
+
## Request Sampling
|
|
67
|
+
|
|
68
|
+
ApmBro supports configurable request sampling to reduce the volume of metrics sent to your APM endpoint, which is useful for high-traffic applications.
|
|
69
|
+
|
|
70
|
+
### Configuration
|
|
71
|
+
|
|
72
|
+
Set the sample rate as a percentage (1-100):
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
# Track 50% of requests
|
|
76
|
+
ApmBro.configure do |config|
|
|
77
|
+
config.sample_rate = 50
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Track 10% of requests (useful for high-traffic apps)
|
|
81
|
+
ApmBro.configure do |config|
|
|
82
|
+
config.sample_rate = 10
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Track all requests (default)
|
|
86
|
+
ApmBro.configure do |config|
|
|
87
|
+
config.sample_rate = 100
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### How It Works
|
|
92
|
+
|
|
93
|
+
- **Random Sampling**: Each request has a random chance of being tracked based on the sample rate
|
|
94
|
+
- **Consistent Per-Request**: The sampling decision is made once per request and applies to all metrics for that request
|
|
95
|
+
- **Debug Logging**: Skipped requests do not count towards the montly limit
|
|
96
|
+
- **Error Tracking**: Errors are still tracked regardless of sampling
|
|
97
|
+
|
|
98
|
+
### Use Cases
|
|
99
|
+
|
|
100
|
+
- **High-Traffic Applications**: Reduce APM data volume and costs
|
|
101
|
+
- **Development/Staging**: Sample fewer requests to reduce noise
|
|
102
|
+
- **Performance Testing**: Track a subset of requests during load testing
|
|
103
|
+
- **Cost Optimization**: Balance monitoring coverage with data costs
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
## Excluding Controllers and Jobs
|
|
107
|
+
|
|
108
|
+
You can exclude specific controllers and jobs from APM tracking.
|
|
109
|
+
|
|
110
|
+
### Configuration
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
ApmBro.configure do |config|
|
|
115
|
+
config.excluded_controllers = [
|
|
116
|
+
"HealthChecksController",
|
|
117
|
+
"Admin::*" # wildcard supported
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
config.excluded_controller_actions = [
|
|
121
|
+
"UsersController#show",
|
|
122
|
+
"Admin::ReportsController#index",
|
|
123
|
+
"Admin::*#*" # wildcard supported for controller and action
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
config.excluded_jobs = [
|
|
127
|
+
"ActiveStorage::AnalyzeJob",
|
|
128
|
+
"Admin::*"
|
|
129
|
+
]
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Notes:
|
|
134
|
+
- Wildcards `*` are supported for controller and action (e.g., `Admin::*#*`).
|
|
135
|
+
- Matching is done against full names like `UsersController`, `Admin::ReportsController#index`, `MyJob`.
|
|
136
|
+
|
|
137
|
+
## SQL Query Tracking
|
|
138
|
+
|
|
139
|
+
ApmBro automatically tracks SQL queries executed during each request and job. Each request will include a `sql_queries` array containing:
|
|
140
|
+
- `sql` - The SQL query (always sanitized)
|
|
141
|
+
- `name` - Query name (e.g., "User Load", "User Update")
|
|
142
|
+
- `duration_ms` - Query execution time in milliseconds
|
|
143
|
+
- `cached` - Whether the query was cached
|
|
144
|
+
- `connection_id` - Database connection ID
|
|
145
|
+
- `trace` - Call stack showing where the query was executed
|
|
146
|
+
|
|
147
|
+
## View Rendering Tracking
|
|
148
|
+
|
|
149
|
+
ApmBro automatically tracks view rendering performance for each request. This includes:
|
|
150
|
+
|
|
151
|
+
- **Individual view events**: Templates, partials, and collections rendered
|
|
152
|
+
- **Performance metrics**: Rendering times for each view component
|
|
153
|
+
- **Cache analysis**: Cache hit rates for partials and collections
|
|
154
|
+
- **Slow view detection**: Identification of the slowest rendering views
|
|
155
|
+
- **Frequency analysis**: Most frequently rendered views
|
|
156
|
+
|
|
157
|
+
## Memory Tracking & Leak Detection
|
|
158
|
+
|
|
159
|
+
ApmBro automatically tracks memory usage and detects memory leaks with minimal performance impact. This includes:
|
|
160
|
+
|
|
161
|
+
### Performance-Optimized Memory Tracking
|
|
162
|
+
|
|
163
|
+
By default, ApmBro uses **lightweight memory tracking** that has minimal performance impact:
|
|
164
|
+
|
|
165
|
+
- **Memory Usage Monitoring**: Track memory consumption per request (using GC stats, not system calls)
|
|
166
|
+
- **Memory Leak Detection**: Detect growing memory patterns over time
|
|
167
|
+
- **GC Efficiency Analysis**: Monitor garbage collection effectiveness
|
|
168
|
+
- **Zero Allocation Tracking**: No object allocation tracking by default (can be enabled)
|
|
169
|
+
|
|
170
|
+
### Configuration Options
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
# In your Rails configuration
|
|
174
|
+
ApmBro.configure do |config|
|
|
175
|
+
config.memory_tracking_enabled = true # Enable lightweight memory tracking (default: true)
|
|
176
|
+
config.allocation_tracking_enabled = false # Enable detailed allocation tracking (default: false)
|
|
177
|
+
|
|
178
|
+
# Sampling configuration
|
|
179
|
+
config.sample_rate = 100 # Percentage of requests to track (1-100, default: 100)
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Performance Impact:**
|
|
184
|
+
- **Lightweight mode**: ~0.1ms overhead per request
|
|
185
|
+
- **Allocation tracking**: ~2-5ms overhead per request (only enable when needed)
|
|
186
|
+
|
|
187
|
+
## Job Tracking
|
|
188
|
+
|
|
189
|
+
ApmBro automatically tracks ActiveJob background jobs when ActiveJob is available. Each job execution is tracked with:
|
|
190
|
+
|
|
191
|
+
- `job_class` - The job class name (e.g., "UserMailer::WelcomeEmail")
|
|
192
|
+
- `job_id` - Unique job identifier
|
|
193
|
+
- `queue_name` - The queue the job was processed from
|
|
194
|
+
- `arguments` - Sanitized job arguments (sensitive data filtered)
|
|
195
|
+
- `duration_ms` - Job execution time in milliseconds
|
|
196
|
+
- `status` - "completed" or "failed"
|
|
197
|
+
- `sql_queries` - Array of SQL queries executed during the job
|
|
198
|
+
- `exception_class` - Exception class name (for failed jobs)
|
|
199
|
+
- `message` - Exception message (for failed jobs)
|
|
200
|
+
- `backtrace` - Exception backtrace (for failed jobs)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
## Development
|
|
204
|
+
|
|
205
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
206
|
+
|
|
207
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`.
|
|
208
|
+
|
|
209
|
+
## Contributing
|
|
210
|
+
|
|
211
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/rubydevro/apm_bro.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/notifications"
|
|
4
|
+
|
|
5
|
+
module ApmBro
|
|
6
|
+
class CacheSubscriber
|
|
7
|
+
THREAD_LOCAL_KEY = :apm_bro_cache_events
|
|
8
|
+
|
|
9
|
+
EVENTS = [
|
|
10
|
+
"cache_read.active_support",
|
|
11
|
+
"cache_write.active_support",
|
|
12
|
+
"cache_delete.active_support",
|
|
13
|
+
"cache_exist?.active_support",
|
|
14
|
+
"cache_fetch_hit.active_support",
|
|
15
|
+
"cache_generate.active_support",
|
|
16
|
+
"cache_read_multi.active_support",
|
|
17
|
+
"cache_write_multi.active_support"
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def self.subscribe!
|
|
21
|
+
EVENTS.each do |event_name|
|
|
22
|
+
begin
|
|
23
|
+
ActiveSupport::Notifications.subscribe(event_name) do |name, started, finished, _unique_id, data|
|
|
24
|
+
next unless Thread.current[THREAD_LOCAL_KEY]
|
|
25
|
+
|
|
26
|
+
duration_ms = ((finished - started) * 1000.0).round(2)
|
|
27
|
+
event = build_event(name, data, duration_ms)
|
|
28
|
+
Thread.current[THREAD_LOCAL_KEY] << event if event
|
|
29
|
+
end
|
|
30
|
+
rescue StandardError
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
rescue StandardError
|
|
34
|
+
# Never raise from instrumentation install
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.start_request_tracking
|
|
38
|
+
Thread.current[THREAD_LOCAL_KEY] = []
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.stop_request_tracking
|
|
42
|
+
events = Thread.current[THREAD_LOCAL_KEY]
|
|
43
|
+
Thread.current[THREAD_LOCAL_KEY] = nil
|
|
44
|
+
events || []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.build_event(name, data, duration_ms)
|
|
48
|
+
return nil unless data.is_a?(Hash)
|
|
49
|
+
|
|
50
|
+
base = {
|
|
51
|
+
event: name,
|
|
52
|
+
duration_ms: duration_ms,
|
|
53
|
+
key: safe_key(data[:key]),
|
|
54
|
+
keys_count: safe_keys_count(data[:keys]),
|
|
55
|
+
hit: infer_hit(name, data),
|
|
56
|
+
store: safe_store_name(data[:store]),
|
|
57
|
+
namespace: safe_namespace(data[:namespace]),
|
|
58
|
+
at: Time.now.utc.to_i
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
base
|
|
62
|
+
rescue StandardError
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.safe_key(key)
|
|
67
|
+
return nil if key.nil?
|
|
68
|
+
s = key.to_s
|
|
69
|
+
s.length > 200 ? s[0, 200] + "…" : s
|
|
70
|
+
rescue StandardError
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.safe_keys_count(keys)
|
|
75
|
+
if keys.respond_to?(:size)
|
|
76
|
+
keys.size
|
|
77
|
+
else
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
rescue StandardError
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.safe_store_name(store)
|
|
85
|
+
return nil unless store
|
|
86
|
+
if store.respond_to?(:name)
|
|
87
|
+
store.name
|
|
88
|
+
else
|
|
89
|
+
store.class.name
|
|
90
|
+
end
|
|
91
|
+
rescue StandardError
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.safe_namespace(ns)
|
|
96
|
+
ns.to_s[0, 100]
|
|
97
|
+
rescue StandardError
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.infer_hit(name, data)
|
|
102
|
+
case name
|
|
103
|
+
when "cache_fetch_hit.active_support"
|
|
104
|
+
true
|
|
105
|
+
when "cache_read.active_support"
|
|
106
|
+
!!data[:hit]
|
|
107
|
+
else
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
rescue StandardError
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ApmBro
|
|
4
|
+
class CircuitBreaker
|
|
5
|
+
# Circuit breaker states
|
|
6
|
+
CLOSED = :closed
|
|
7
|
+
OPEN = :open
|
|
8
|
+
HALF_OPEN = :half_open
|
|
9
|
+
|
|
10
|
+
# Default configuration
|
|
11
|
+
DEFAULT_FAILURE_THRESHOLD = 3
|
|
12
|
+
DEFAULT_RECOVERY_TIMEOUT = 60 # seconds
|
|
13
|
+
DEFAULT_RETRY_TIMEOUT = 300 # seconds for retry attempts
|
|
14
|
+
|
|
15
|
+
def initialize(
|
|
16
|
+
failure_threshold: DEFAULT_FAILURE_THRESHOLD,
|
|
17
|
+
recovery_timeout: DEFAULT_RECOVERY_TIMEOUT,
|
|
18
|
+
retry_timeout: DEFAULT_RETRY_TIMEOUT
|
|
19
|
+
)
|
|
20
|
+
@failure_threshold = failure_threshold
|
|
21
|
+
@recovery_timeout = recovery_timeout
|
|
22
|
+
@retry_timeout = retry_timeout
|
|
23
|
+
|
|
24
|
+
@state = CLOSED
|
|
25
|
+
@failure_count = 0
|
|
26
|
+
@last_failure_time = nil
|
|
27
|
+
@last_success_time = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call(&block)
|
|
31
|
+
case @state
|
|
32
|
+
when CLOSED
|
|
33
|
+
execute_with_monitoring(&block)
|
|
34
|
+
when OPEN
|
|
35
|
+
if should_attempt_reset?
|
|
36
|
+
@state = HALF_OPEN
|
|
37
|
+
execute_with_monitoring(&block)
|
|
38
|
+
else
|
|
39
|
+
:circuit_open
|
|
40
|
+
end
|
|
41
|
+
when HALF_OPEN
|
|
42
|
+
execute_with_monitoring(&block)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def state
|
|
47
|
+
@state
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def failure_count
|
|
51
|
+
@failure_count
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def last_failure_time
|
|
55
|
+
@last_failure_time
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def last_success_time
|
|
59
|
+
@last_success_time
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def reset!
|
|
63
|
+
@state = CLOSED
|
|
64
|
+
@failure_count = 0
|
|
65
|
+
@last_failure_time = nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def open!
|
|
69
|
+
@state = OPEN
|
|
70
|
+
@last_failure_time = Time.now
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def transition_to_half_open!
|
|
74
|
+
@state = HALF_OPEN
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def should_attempt_reset?
|
|
78
|
+
return false unless @last_failure_time
|
|
79
|
+
|
|
80
|
+
# Try to reset after recovery timeout
|
|
81
|
+
elapsed = Time.now - @last_failure_time
|
|
82
|
+
elapsed >= @recovery_timeout
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def execute_with_monitoring(&block)
|
|
88
|
+
result = block.call
|
|
89
|
+
|
|
90
|
+
if success?(result)
|
|
91
|
+
on_success
|
|
92
|
+
result
|
|
93
|
+
else
|
|
94
|
+
on_failure
|
|
95
|
+
result
|
|
96
|
+
end
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
on_failure
|
|
99
|
+
raise e
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def success?(result)
|
|
103
|
+
# Consider 2xx status codes as success
|
|
104
|
+
result.is_a?(Net::HTTPSuccess)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def on_success
|
|
108
|
+
@failure_count = 0
|
|
109
|
+
@last_success_time = Time.now
|
|
110
|
+
@state = CLOSED
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def on_failure
|
|
114
|
+
@failure_count += 1
|
|
115
|
+
@last_failure_time = Time.now
|
|
116
|
+
|
|
117
|
+
# If we're in half-open state and get a failure, go back to open
|
|
118
|
+
if @state == HALF_OPEN
|
|
119
|
+
@state = OPEN
|
|
120
|
+
elsif @failure_count >= @failure_threshold
|
|
121
|
+
@state = OPEN
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require "timeout"
|
|
7
|
+
|
|
8
|
+
module ApmBro
|
|
9
|
+
class Client
|
|
10
|
+
def initialize(configuration = ApmBro.configuration)
|
|
11
|
+
@configuration = configuration
|
|
12
|
+
@circuit_breaker = create_circuit_breaker
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def post_metric(event_name:, payload:)
|
|
16
|
+
unless @configuration.enabled
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Check sampling rate - skip if not selected for sampling
|
|
21
|
+
unless @configuration.should_sample?
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
api_key = @configuration.resolve_api_key
|
|
26
|
+
|
|
27
|
+
if api_key.nil?
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Check circuit breaker before making request
|
|
32
|
+
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
33
|
+
if @circuit_breaker.state == :open
|
|
34
|
+
# Check if we should attempt a reset to half-open state
|
|
35
|
+
if @circuit_breaker.should_attempt_reset?
|
|
36
|
+
@circuit_breaker.transition_to_half_open!
|
|
37
|
+
else
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Make the HTTP request (async)
|
|
44
|
+
make_http_request(event_name, payload, api_key)
|
|
45
|
+
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def create_circuit_breaker
|
|
52
|
+
return nil unless @configuration.circuit_breaker_enabled
|
|
53
|
+
|
|
54
|
+
CircuitBreaker.new(
|
|
55
|
+
failure_threshold: @configuration.circuit_breaker_failure_threshold,
|
|
56
|
+
recovery_timeout: @configuration.circuit_breaker_recovery_timeout,
|
|
57
|
+
retry_timeout: @configuration.circuit_breaker_retry_timeout
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def make_http_request(event_name, payload, api_key)
|
|
62
|
+
endpoint_url = @configuration.respond_to?(:ruby_dev) && @configuration.ruby_dev ?
|
|
63
|
+
'http://localhost:3100/apm/v1/metrics' :
|
|
64
|
+
"https://www.deadbro.com/apm/v1/metrics"
|
|
65
|
+
|
|
66
|
+
uri = URI.parse(endpoint_url)
|
|
67
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
68
|
+
http.use_ssl = (uri.scheme == "https")
|
|
69
|
+
http.open_timeout = @configuration.open_timeout
|
|
70
|
+
http.read_timeout = @configuration.read_timeout
|
|
71
|
+
|
|
72
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
73
|
+
request["Content-Type"] = "application/json"
|
|
74
|
+
request["Authorization"] = "Bearer #{api_key}"
|
|
75
|
+
body = { event: event_name, payload: payload, sent_at: Time.now.utc.iso8601, revision: @configuration.resolve_deploy_id }
|
|
76
|
+
request.body = JSON.dump(body)
|
|
77
|
+
|
|
78
|
+
# Fire-and-forget using a short-lived thread to avoid blocking the request cycle.
|
|
79
|
+
Thread.new do
|
|
80
|
+
begin
|
|
81
|
+
response = http.request(request)
|
|
82
|
+
|
|
83
|
+
if response
|
|
84
|
+
# Update circuit breaker based on response
|
|
85
|
+
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
86
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
87
|
+
@circuit_breaker.send(:on_success)
|
|
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
|
|
95
|
+
@circuit_breaker.send(:on_failure)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
response
|
|
100
|
+
rescue Timeout::Error => e
|
|
101
|
+
|
|
102
|
+
# Update circuit breaker on timeout
|
|
103
|
+
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
104
|
+
@circuit_breaker.send(:on_failure)
|
|
105
|
+
end
|
|
106
|
+
rescue StandardError => e
|
|
107
|
+
|
|
108
|
+
# Update circuit breaker on exception
|
|
109
|
+
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
110
|
+
@circuit_breaker.send(:on_failure)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def log_debug(message)
|
|
119
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
120
|
+
Rails.logger.debug(message)
|
|
121
|
+
else
|
|
122
|
+
$stdout.puts(message)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
|