rails_performance-alerts 0.1.0
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +308 -0
- data/Rakefile +11 -0
- data/lib/rails_performance/alerts/checker.rb +84 -0
- data/lib/rails_performance/alerts/checker_job.rb +11 -0
- data/lib/rails_performance/alerts/email_notifier.rb +36 -0
- data/lib/rails_performance/alerts/mailer.rb +57 -0
- data/lib/rails_performance/alerts/railtie.rb +9 -0
- data/lib/rails_performance/alerts/rule.rb +126 -0
- data/lib/rails_performance/alerts/state.rb +80 -0
- data/lib/rails_performance/alerts/tasks.rake +75 -0
- data/lib/rails_performance/alerts/version.rb +5 -0
- data/lib/rails_performance/alerts/views/rails_performance/alerts/layouts/mailer.html.erb +76 -0
- data/lib/rails_performance/alerts/views/rails_performance/alerts/mailer/resolved.html.erb +30 -0
- data/lib/rails_performance/alerts/views/rails_performance/alerts/mailer/resolved.text.erb +15 -0
- data/lib/rails_performance/alerts/views/rails_performance/alerts/mailer/triggered.html.erb +30 -0
- data/lib/rails_performance/alerts/views/rails_performance/alerts/mailer/triggered.text.erb +15 -0
- data/lib/rails_performance/alerts.rb +52 -0
- data/lib/rails_performance-alerts.rb +1 -0
- metadata +105 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6cc172f0cc721a2a1aa349f9df6ae0ac5d888b85b4075e7d24db94fcd3e27457
|
|
4
|
+
data.tar.gz: b1ae3e85f458a871ff5f73e8a7582309dd85b8647b5c5947bde0c1a4f0bd45f0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7284b5d1210c3dd1545e53418efe55fe1e34ac8f42cdd8e76a932317715513631ccddb53576eb6a135777a38f2e29dbbf3ba9272bb4fc6c70a2e6dcca0219978
|
|
7
|
+
data.tar.gz: 00cbdbc9646511eb19d39c95e6ad6bed55db4fdda018c15c581e3607bac1281c4f8758c80d860ea8d31accfecf5f730cc548edbb5273693ef859b3d3c5e8c59e
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2025
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# RailsPerformance::Alerts
|
|
2
|
+
|
|
3
|
+
Customizable alerts for [rails_performance](https://github.com/igorkasyanchuk/rails_performance) metrics.
|
|
4
|
+
|
|
5
|
+
Monitor your Rails application and receive alerts when metrics exceed configured thresholds.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
Assuming you already have rails_performance set up, follow these steps:
|
|
10
|
+
|
|
11
|
+
1. Add to your Gemfile and run `bundle install`:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem "rails_performance-alerts"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
2. Update `config/initializers/rails_performance.rb`:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
RailsPerformance.setup do |config|
|
|
21
|
+
# ... existing rails_performance config ...
|
|
22
|
+
|
|
23
|
+
# example alerts:
|
|
24
|
+
config.alert_rules = [
|
|
25
|
+
# Request-based alerts
|
|
26
|
+
{
|
|
27
|
+
name: "High Response Time",
|
|
28
|
+
report: "ResponseTimeReport", # RailsPerformance::Reports::ResponseTimeReport
|
|
29
|
+
threshold: ->(value) { value > 500 }, # Alert when > 500ms
|
|
30
|
+
time_window: 5.minutes, # Calculate average from last 5 minutes
|
|
31
|
+
duration: 3.minutes # Alert if breached for 3 consecutive minutes
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "Low Throughput",
|
|
35
|
+
report: "ThroughputReport",
|
|
36
|
+
threshold: ->(value) { value < 10 }, # Alert when < 10 requests/min
|
|
37
|
+
time_window: 3.minutes,
|
|
38
|
+
duration: 2.minutes
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "High Error Count",
|
|
42
|
+
report: "ThroughputReport",
|
|
43
|
+
query: { status: 500 }, # Filter to 500 errors only
|
|
44
|
+
threshold: ->(value) { value > 5 },
|
|
45
|
+
time_window: 5.minutes,
|
|
46
|
+
duration: 1.minute
|
|
47
|
+
},
|
|
48
|
+
# System resource alerts (requires rails_performance's system monitoring to be enabled)
|
|
49
|
+
{
|
|
50
|
+
name: "High CPU Load",
|
|
51
|
+
report: "CPULoad", # RailsPerformance::SystemMonitor::CPULoad
|
|
52
|
+
threshold: ->(value) { value > 80 }, # Alert when CPU > 80%
|
|
53
|
+
time_window: 5.minutes,
|
|
54
|
+
duration: 3.minutes
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "High Memory Usage",
|
|
58
|
+
report: "MemoryUsage",
|
|
59
|
+
threshold: ->(value) { value > 1.gigabyte }, # Alert when > 1GB
|
|
60
|
+
time_window: 5.minutes,
|
|
61
|
+
duration: 2.minutes
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "Disk Space Exhaustion",
|
|
65
|
+
report: "DiskUsage",
|
|
66
|
+
threshold: ->(value) { value < 5.gigabytes }, # Alert when < 5GB free
|
|
67
|
+
time_window: 10.minutes,
|
|
68
|
+
duration: 5.minutes,
|
|
69
|
+
},
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
config.alert_handler = RailsPerformance::Alerts::EmailNotifier.new(
|
|
73
|
+
recipients: ["team@yourapp.com", "ops@yourapp.com"],
|
|
74
|
+
from: "alerts@yourapp.com",
|
|
75
|
+
smtp_settions: { ... } # defaults to Rails ActionMailer config if omitted
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
3. Run the checks periodically using either ActiveJob, cron, or a standalone daemon:
|
|
81
|
+
|
|
82
|
+
* ActiveJob: `RailsPerformance::Alerts::CheckerJob`
|
|
83
|
+
* Rake task for cron: `rails_performance:alerts:check`
|
|
84
|
+
* Rake task for daemon: `rails_performance:alerts:monitor`
|
|
85
|
+
|
|
86
|
+
## How It Works
|
|
87
|
+
|
|
88
|
+
Alerts can be configured for two types of metrics:
|
|
89
|
+
|
|
90
|
+
### Request-Based Reports
|
|
91
|
+
|
|
92
|
+
Any report class from `RailsPerformance::Reports::*`:
|
|
93
|
+
- `ResponseTimeReport` - Average response time in milliseconds
|
|
94
|
+
- `ThroughputReport` - Requests per minute
|
|
95
|
+
- `PercentileReport` - P50, P90, P95, P99 response times
|
|
96
|
+
- Any custom report you create
|
|
97
|
+
|
|
98
|
+
### System Resource Charts
|
|
99
|
+
|
|
100
|
+
Monitor system resources with `RailsPerformance::SystemMonitor::*` classes:
|
|
101
|
+
- `CPULoad` - CPU load average
|
|
102
|
+
- `MemoryUsage` - Application memory usage (in bytes)
|
|
103
|
+
- `DiskUsage` - Available disk space (in bytes)
|
|
104
|
+
- Any custom system monitor you create
|
|
105
|
+
|
|
106
|
+
The `threshold` parameter accepts a lambda that receives the calculated metric value and returns true when the threshold is breached.
|
|
107
|
+
|
|
108
|
+
## Understanding `time_window` vs `duration`
|
|
109
|
+
|
|
110
|
+
- **`time_window`**: Period of historical data to analyze (lookback period)
|
|
111
|
+
- **`duration`**: How long the threshold must be exceeded before alerting
|
|
112
|
+
|
|
113
|
+
Example: `time_window: 5.minutes, duration: 3.minutes` means:
|
|
114
|
+
> "Calculate the average over the last 5 minutes of data. If that average exceeds the threshold for 3 consecutive checks, trigger the alert."
|
|
115
|
+
|
|
116
|
+
This prevents false alarms from brief spikes while still detecting sustained problems.
|
|
117
|
+
|
|
118
|
+
## Running Alert Checks
|
|
119
|
+
|
|
120
|
+
### With ActiveJob (recommended)
|
|
121
|
+
|
|
122
|
+
Schedule with your preferred job scheduler:
|
|
123
|
+
|
|
124
|
+
#### With Solid Queue (config/recurring.yml):
|
|
125
|
+
|
|
126
|
+
```yaml
|
|
127
|
+
rails_performance_alerts:
|
|
128
|
+
class: RailsPerformance::Alerts::CheckerJob
|
|
129
|
+
schedule: every minute
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### With whenever gem (config/schedule.rb):
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
every 1.minute do
|
|
136
|
+
runner "RailsPerformance::Alerts::CheckerJob.perform_later"
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### With cron and rake
|
|
141
|
+
|
|
142
|
+
Add to your crontab:
|
|
143
|
+
```bash
|
|
144
|
+
* * * * * cd /app && bundle exec rake rails_performance:alerts:check
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Run as its own process
|
|
148
|
+
|
|
149
|
+
You can run the alert monitor continuously as a daemon process. It will check alerts every minute.
|
|
150
|
+
```bash
|
|
151
|
+
bundle exec rake rails_performance:alerts:monitor
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Filtering Alerts by Query Parameters
|
|
155
|
+
|
|
156
|
+
You can scope alerts to specific controllers, actions, status codes, or any other attribute:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
{
|
|
160
|
+
name: "Slow API Endpoint",
|
|
161
|
+
report: RailsPerformance::Reports::ResponseTimeReport,
|
|
162
|
+
query: {
|
|
163
|
+
controller: "Api::UsersController",
|
|
164
|
+
action: "index"
|
|
165
|
+
},
|
|
166
|
+
threshold: ->(value) { value > 1000 },
|
|
167
|
+
time_window: 3.minutes,
|
|
168
|
+
duration: 2.minutes
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The `query` parameter is passed directly to the RailsPerformance DataSource, so you can filter by any attribute available in your performance data.
|
|
173
|
+
|
|
174
|
+
## Monitoring Specific Servers
|
|
175
|
+
|
|
176
|
+
For system resource alerts, you can monitor a specific server by providing the `server` parameter:
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
{
|
|
180
|
+
name: "High Memory on Production Web Server",
|
|
181
|
+
report: RailsPerformance::SystemMonitor::MemoryUsage,
|
|
182
|
+
server: "web-server-1///production///web", # Format: hostname///context///role
|
|
183
|
+
threshold: ->(value) { value > 2_000_000_000 },
|
|
184
|
+
time_window: 5.minutes,
|
|
185
|
+
duration: 2.minutes
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
If no `server` is specified, the alert will monitor the first available server in the ResourcesReport.
|
|
190
|
+
|
|
191
|
+
## Alerting on Missing Data
|
|
192
|
+
|
|
193
|
+
By default, alerts will trigger when metric data is missing (e.g., no server reporting resource metrics). This is useful for detecting when servers go offline or stop reporting data.
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
{
|
|
197
|
+
name: "Server Offline",
|
|
198
|
+
report: RailsPerformance::SystemMonitor::CPULoad,
|
|
199
|
+
threshold: ->(value) { value > 80 },
|
|
200
|
+
time_window: 5.minutes,
|
|
201
|
+
duration: 2.minutes,
|
|
202
|
+
alert_on_missing_data: true # Default: true
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
If you want to ignore missing data and only alert on threshold breaches:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
{
|
|
210
|
+
name: "High CPU (ignore missing data)",
|
|
211
|
+
report: RailsPerformance::SystemMonitor::CPULoad,
|
|
212
|
+
threshold: ->(value) { value > 80 },
|
|
213
|
+
time_window: 5.minutes,
|
|
214
|
+
duration: 2.minutes,
|
|
215
|
+
alert_on_missing_data: false # Don't alert when data is missing
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Note**: For request-based reports, missing data typically means no requests were recorded, which returns a value of `0.0` rather than `nil`.
|
|
220
|
+
|
|
221
|
+
## Custom Alert Handlers
|
|
222
|
+
|
|
223
|
+
While the built-in `EmailNotifier` (shown in the configuration example above) handles most use cases, you can create custom alert handlers for other notification systems.
|
|
224
|
+
|
|
225
|
+
### Post to Slack
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
config.alert_handler = proc do |alert|
|
|
229
|
+
slack = Slack::Notifier.new(ENV['SLACK_WEBHOOK_URL'])
|
|
230
|
+
|
|
231
|
+
case alert[:action]
|
|
232
|
+
when :triggered
|
|
233
|
+
slack.ping("🚨 *Alert Triggered*: #{alert[:name]}\n" \
|
|
234
|
+
"Current value: #{alert[:current_value]}")
|
|
235
|
+
when :resolved
|
|
236
|
+
slack.ping("✅ *Alert Resolved*: #{alert[:name]}")
|
|
237
|
+
when :error
|
|
238
|
+
slack.ping("⚠️ *Alert System Error*: #{alert[:error].message}")
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Create PagerDuty Incident
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
config.alert_handler = proc do |alert|
|
|
247
|
+
pagerduty = Pagerduty.build(
|
|
248
|
+
integration_key: ENV['PAGERDUTY_INTEGRATION_KEY']
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
case alert[:action]
|
|
252
|
+
when :triggered
|
|
253
|
+
pagerduty.trigger(
|
|
254
|
+
summary: "#{alert[:name]}: value is #{alert[:current_value]}",
|
|
255
|
+
severity: 'error',
|
|
256
|
+
custom_details: alert
|
|
257
|
+
)
|
|
258
|
+
when :resolved
|
|
259
|
+
pagerduty.resolve
|
|
260
|
+
when :error
|
|
261
|
+
pagerduty.trigger(
|
|
262
|
+
summary: "Alert system error: #{alert[:error].message}",
|
|
263
|
+
severity: 'critical',
|
|
264
|
+
custom_details: alert
|
|
265
|
+
)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Custom Email Mailer
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
config.alert_handler = proc do |alert|
|
|
274
|
+
case alert[:action]
|
|
275
|
+
when :triggered
|
|
276
|
+
MyAlertMailer.alert_triggered(alert).deliver_later
|
|
277
|
+
when :resolved
|
|
278
|
+
MyAlertMailer.alert_resolved(alert).deliver_later
|
|
279
|
+
when :error
|
|
280
|
+
MyAlertMailer.alert_error(alert).deliver_later
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Alert Handler Actions
|
|
286
|
+
|
|
287
|
+
Your alert handler will receive alerts with three possible actions:
|
|
288
|
+
|
|
289
|
+
- **`:triggered`** - An alert threshold has been exceeded
|
|
290
|
+
- Contains: `:name`, `:current_value`, `:time_window`, `:duration`, `:triggered_at`
|
|
291
|
+
- **`:resolved`** - An alert has returned to normal
|
|
292
|
+
- Contains: `:name`, `:current_value`, `:resolved_at`, `:was_triggered_at`
|
|
293
|
+
- **`:error`** - An error occurred while checking alerts
|
|
294
|
+
- Contains: `:error` (exception object), `:rule_config` (may be nil)
|
|
295
|
+
|
|
296
|
+
## Rake Tasks
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
# Check alerts once
|
|
300
|
+
rake rails_performance:alerts:check
|
|
301
|
+
|
|
302
|
+
# Run alert monitor continuously as a daemon
|
|
303
|
+
rake rails_performance:alerts:monitor
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
The gem is available as open source under the terms of the MIT License.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module RailsPerformance
|
|
2
|
+
module Alerts
|
|
3
|
+
class Checker
|
|
4
|
+
def self.check_all
|
|
5
|
+
new.check_all
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def check_all
|
|
9
|
+
RailsPerformance.alert_rules.map do |rule_config|
|
|
10
|
+
begin
|
|
11
|
+
rule = Rule.new(rule_config)
|
|
12
|
+
check_rule(rule)
|
|
13
|
+
rescue => e
|
|
14
|
+
RailsPerformance.log "Error checking alert rule: #{e.message}"
|
|
15
|
+
notify_error e, rule_config
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
end.compact
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def check_rule rule
|
|
22
|
+
current_value, breaching = rule.evaluate
|
|
23
|
+
|
|
24
|
+
# Only skip if value is nil AND not breaching (alert_on_missing_data: false)
|
|
25
|
+
return nil if current_value.nil? && !breaching
|
|
26
|
+
|
|
27
|
+
state = State.new(rule.id)
|
|
28
|
+
|
|
29
|
+
if breaching
|
|
30
|
+
state.record_breach
|
|
31
|
+
duration_met = state.breaching_duration >= rule.duration
|
|
32
|
+
|
|
33
|
+
if duration_met && !state.triggered?
|
|
34
|
+
trigger_alert rule, current_value, state
|
|
35
|
+
return { action: :triggered, rule: rule, value: current_value }
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
if state.triggered?
|
|
39
|
+
resolve_alert rule, current_value, state
|
|
40
|
+
return { action: :resolved, rule: rule, value: current_value }
|
|
41
|
+
end
|
|
42
|
+
state.clear_breaches
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def trigger_alert rule, current_value, state
|
|
51
|
+
state.mark_triggered!
|
|
52
|
+
|
|
53
|
+
RailsPerformance.alert_handler&.call({
|
|
54
|
+
action: :triggered,
|
|
55
|
+
name: rule.name,
|
|
56
|
+
current_value: current_value,
|
|
57
|
+
time_window: rule.time_window,
|
|
58
|
+
duration: rule.duration,
|
|
59
|
+
triggered_at: Time.now
|
|
60
|
+
})
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def resolve_alert rule, current_value, state
|
|
64
|
+
state.mark_resolved!
|
|
65
|
+
|
|
66
|
+
RailsPerformance.alert_handler&.call({
|
|
67
|
+
action: :resolved,
|
|
68
|
+
name: rule.name,
|
|
69
|
+
current_value: current_value,
|
|
70
|
+
resolved_at: Time.now,
|
|
71
|
+
was_triggered_at: state.triggered_at
|
|
72
|
+
})
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def notify_error(exception, rule_config)
|
|
76
|
+
RailsPerformance.alert_handler&.call({
|
|
77
|
+
action: :error,
|
|
78
|
+
error: exception,
|
|
79
|
+
rule_config: rule_config
|
|
80
|
+
})
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require_relative "mailer"
|
|
2
|
+
|
|
3
|
+
module RailsPerformance
|
|
4
|
+
module Alerts
|
|
5
|
+
class EmailNotifier < Data.define(:recipients, :from, :smtp_settings)
|
|
6
|
+
def initialize recipients:, from:, smtp_settings: ::Rails.application.config.action_mailer.smtp_settings
|
|
7
|
+
recipients = Array(recipients)
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call alert
|
|
12
|
+
configure_mailer
|
|
13
|
+
deliver_emails alert
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def deliver_emails alert
|
|
19
|
+
case alert[:action]
|
|
20
|
+
when :triggered
|
|
21
|
+
recipients.each do |recipient|
|
|
22
|
+
Mailer.triggered(alert, to: recipient, from: from).deliver_now
|
|
23
|
+
end
|
|
24
|
+
when :resolved
|
|
25
|
+
recipients.each do |recipient|
|
|
26
|
+
Mailer.resolved(alert, to: recipient, from: from).deliver_now
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def configure_mailer
|
|
32
|
+
Mailer.smtp_settings = smtp_settings
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module RailsPerformance
|
|
2
|
+
module Alerts
|
|
3
|
+
class Mailer < ActionMailer::Base
|
|
4
|
+
prepend_view_path File.expand_path("views", __dir__)
|
|
5
|
+
layout "rails_performance/alerts/layouts/mailer"
|
|
6
|
+
|
|
7
|
+
def triggered alert_data, to:, from:
|
|
8
|
+
@alert = alert_data
|
|
9
|
+
@name = alert_data[:name]
|
|
10
|
+
@current_value = alert_data[:current_value]
|
|
11
|
+
@time_window = format_duration(alert_data[:time_window])
|
|
12
|
+
@duration = format_duration(alert_data[:duration])
|
|
13
|
+
@triggered_at = alert_data[:triggered_at]
|
|
14
|
+
|
|
15
|
+
mail(
|
|
16
|
+
to: to,
|
|
17
|
+
from: from,
|
|
18
|
+
subject: "🚨 Alert Triggered: #{@name}",
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def resolved alert_data, to:, from:
|
|
23
|
+
@alert = alert_data
|
|
24
|
+
@name = alert_data[:name]
|
|
25
|
+
@current_value = alert_data[:current_value]
|
|
26
|
+
@resolved_at = alert_data[:resolved_at]
|
|
27
|
+
@was_triggered_at = alert_data[:was_triggered_at]
|
|
28
|
+
@duration_triggered = calculate_duration(@was_triggered_at, @resolved_at)
|
|
29
|
+
|
|
30
|
+
mail(
|
|
31
|
+
to: to,
|
|
32
|
+
from: from,
|
|
33
|
+
subject: "✅ Alert Resolved: #{@name}",
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def format_duration seconds
|
|
40
|
+
return "N/A" unless seconds
|
|
41
|
+
|
|
42
|
+
if seconds < 60
|
|
43
|
+
"#{seconds.to_i} seconds"
|
|
44
|
+
elsif seconds < 3600
|
|
45
|
+
"#{(seconds / 60).to_i} minutes"
|
|
46
|
+
else
|
|
47
|
+
"#{(seconds / 3600).to_i} hours"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def calculate_duration start_time, end_time
|
|
52
|
+
return "N/A" unless start_time && end_time
|
|
53
|
+
format_duration(end_time - start_time)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
module RailsPerformance
|
|
2
|
+
module Alerts
|
|
3
|
+
class Rule
|
|
4
|
+
attr_reader :name, :time_window, :duration, :report, :query, :threshold, :server, :alert_on_missing_data
|
|
5
|
+
|
|
6
|
+
def initialize(options = {})
|
|
7
|
+
@name = options[:name] || "Unnamed Alert"
|
|
8
|
+
@time_window = options[:time_window] || 5.minutes
|
|
9
|
+
@duration = options[:duration] || 5.minutes
|
|
10
|
+
@report = options[:report]
|
|
11
|
+
@query = options[:query] || {}
|
|
12
|
+
@threshold = options[:threshold]
|
|
13
|
+
@server = options[:server]
|
|
14
|
+
@alert_on_missing_data = options.fetch(:alert_on_missing_data, true)
|
|
15
|
+
|
|
16
|
+
validate!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def id
|
|
20
|
+
@id ||= Digest::MD5.hexdigest("#{name}_#{report_class}_#{query}_#{server}_#{threshold.source_location}")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def evaluate
|
|
24
|
+
data = fetch_data
|
|
25
|
+
|
|
26
|
+
value = calculate_average_value(data)
|
|
27
|
+
breaching = if value.nil?
|
|
28
|
+
@alert_on_missing_data
|
|
29
|
+
else
|
|
30
|
+
@threshold.call(value)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
[value, breaching]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_s
|
|
37
|
+
"#{name} (#{report})"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def report_class
|
|
41
|
+
return @report if @report.is_a?(Class)
|
|
42
|
+
|
|
43
|
+
unless @report.is_a?(String)
|
|
44
|
+
raise ArgumentError, "Invalid report class name: #{@report.inspect}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Try RailsPerformance::Reports namespace first
|
|
48
|
+
if RailsPerformance::Reports.const_defined?(@report, false)
|
|
49
|
+
return RailsPerformance::Reports.const_get(@report, false)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Try RailsPerformance::SystemMonitor namespace
|
|
53
|
+
if RailsPerformance::SystemMonitor.const_defined?(@report, false)
|
|
54
|
+
return RailsPerformance::SystemMonitor.const_get(@report, false)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Try top-level constant
|
|
58
|
+
if Object.const_defined?(@report, false)
|
|
59
|
+
return Object.const_get(@report, false)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
raise ArgumentError, "Class #{@report} not found in RailsPerformance::Reports, RailsPerformance::SystemMonitor, or top level"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def resource_chart?
|
|
68
|
+
report_class.ancestors.include?(RailsPerformance::SystemMonitor::ResourceChart)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def fetch_data
|
|
72
|
+
if resource_chart?
|
|
73
|
+
fetch_resource_chart_data
|
|
74
|
+
else
|
|
75
|
+
fetch_report_data
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def fetch_resource_chart_data
|
|
80
|
+
time_window_days = RailsPerformance::Utils.days(@time_window)
|
|
81
|
+
query_params = @query.merge({on: Date.today})
|
|
82
|
+
db = RailsPerformance::DataSource.new(type: :resources, q: query_params, days: time_window_days).db
|
|
83
|
+
|
|
84
|
+
resources_report = RailsPerformance::Reports::ResourcesReport.new(db)
|
|
85
|
+
|
|
86
|
+
target_server = if @server
|
|
87
|
+
resources_report.servers.find { |s| s.key == @server }
|
|
88
|
+
else
|
|
89
|
+
resources_report.servers.first
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
return nil if target_server.nil?
|
|
93
|
+
|
|
94
|
+
chart = report_class.new(target_server)
|
|
95
|
+
chart.data
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def fetch_report_data
|
|
99
|
+
time_window_days = RailsPerformance::Utils.days(@time_window)
|
|
100
|
+
query_params = @query.merge({on: Date.today})
|
|
101
|
+
db = RailsPerformance::DataSource.new(type: :requests, q: query_params, days: time_window_days).db
|
|
102
|
+
|
|
103
|
+
report = report_class.new(db)
|
|
104
|
+
report.data
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def calculate_average_value(data)
|
|
108
|
+
window_start = (Time.now - @time_window).to_i * 1000
|
|
109
|
+
recent_data = (data || []).select { |timestamp, value| timestamp >= window_start && value }
|
|
110
|
+
values = recent_data.map { |_, value| value }.compact
|
|
111
|
+
|
|
112
|
+
return nil if values.empty?
|
|
113
|
+
|
|
114
|
+
(values.sum.to_f / values.size).round(2)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def validate!
|
|
118
|
+
raise ArgumentError, "name is required" if @name.nil? || @name.empty?
|
|
119
|
+
raise ArgumentError, "report class is required" unless report_class.is_a?(Class)
|
|
120
|
+
raise ArgumentError, "threshold lambda is required" unless @threshold.is_a?(Proc)
|
|
121
|
+
raise ArgumentError, "time_window must be positive" unless @time_window.to_i > 0
|
|
122
|
+
raise ArgumentError, "duration must be positive" unless @duration.to_i > 0
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module RailsPerformance
|
|
2
|
+
module Alerts
|
|
3
|
+
class State
|
|
4
|
+
REDIS_KEY_PREFIX = "rails_performance:alerts:state"
|
|
5
|
+
STATE_TTL = 7.days.to_i
|
|
6
|
+
|
|
7
|
+
attr_reader :rule_id
|
|
8
|
+
|
|
9
|
+
def initialize(rule_id)
|
|
10
|
+
@rule_id = rule_id
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def triggered?
|
|
14
|
+
redis.get(triggered_key) == "true"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def mark_triggered!(timestamp = Time.now)
|
|
18
|
+
redis.setex(triggered_key, STATE_TTL, "true")
|
|
19
|
+
redis.setex(triggered_at_key, STATE_TTL, timestamp.to_i.to_s)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def mark_resolved!(timestamp = Time.now)
|
|
23
|
+
redis.del(triggered_key)
|
|
24
|
+
redis.setex(resolved_at_key, STATE_TTL, timestamp.to_i.to_s)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def triggered_at
|
|
28
|
+
timestamp = redis.get(triggered_at_key)
|
|
29
|
+
timestamp ? Time.at(timestamp.to_i) : nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def resolved_at
|
|
33
|
+
timestamp = redis.get(resolved_at_key)
|
|
34
|
+
timestamp ? Time.at(timestamp.to_i) : nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def first_breach_at
|
|
38
|
+
timestamp = redis.get(first_breach_key)
|
|
39
|
+
timestamp ? Time.at(timestamp.to_i) : nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def record_breach(timestamp = Time.now)
|
|
43
|
+
unless first_breach_at
|
|
44
|
+
redis.setex(first_breach_key, STATE_TTL, timestamp.to_i.to_s)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def clear_breaches
|
|
49
|
+
redis.del(first_breach_key)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def breaching_duration
|
|
53
|
+
return 0 unless first_breach_at
|
|
54
|
+
Time.now - first_breach_at
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def redis
|
|
60
|
+
RailsPerformance.redis
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def triggered_key
|
|
64
|
+
"#{REDIS_KEY_PREFIX}:#{rule_id}:triggered"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def triggered_at_key
|
|
68
|
+
"#{REDIS_KEY_PREFIX}:#{rule_id}:triggered_at"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def resolved_at_key
|
|
72
|
+
"#{REDIS_KEY_PREFIX}:#{rule_id}:resolved_at"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def first_breach_key
|
|
76
|
+
"#{REDIS_KEY_PREFIX}:#{rule_id}:first_breach_at"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
namespace :rails_performance do
|
|
2
|
+
namespace :alerts do
|
|
3
|
+
desc "Check all configured alerts"
|
|
4
|
+
task check: :environment do
|
|
5
|
+
unless RailsPerformance.enabled
|
|
6
|
+
puts "RailsPerformance is disabled. Skipping alert checks."
|
|
7
|
+
next
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
if RailsPerformance.alert_rules.empty?
|
|
11
|
+
puts "No alert rules configured. Skipping alert checks."
|
|
12
|
+
next
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
puts "Checking #{RailsPerformance.alert_rules.size} alert rule(s)..."
|
|
16
|
+
|
|
17
|
+
results = RailsPerformance::Alerts::Checker.check_all
|
|
18
|
+
|
|
19
|
+
if results.empty?
|
|
20
|
+
puts "All metrics are within normal thresholds."
|
|
21
|
+
else
|
|
22
|
+
results.each do |result|
|
|
23
|
+
case result[:action]
|
|
24
|
+
when :triggered
|
|
25
|
+
puts "ALERT TRIGGERED: #{result[:rule].name} - #{result[:rule].metric} is #{result[:value]} (threshold: #{result[:rule].threshold})"
|
|
26
|
+
when :resolved
|
|
27
|
+
puts "ALERT RESOLVED: #{result[:rule].name} - #{result[:rule].metric} is now #{result[:value]}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
desc "Run alert checker continuously (for background process)"
|
|
34
|
+
task monitor: :environment do
|
|
35
|
+
unless RailsPerformance.enabled
|
|
36
|
+
puts "RailsPerformance is disabled. Exiting."
|
|
37
|
+
exit
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if RailsPerformance.alert_rules.empty?
|
|
41
|
+
puts "No alert rules configured. Exiting."
|
|
42
|
+
exit
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
puts "Starting alert monitor (checking every minute)..."
|
|
46
|
+
puts "Monitoring #{RailsPerformance.alert_rules.size} alert rule(s)"
|
|
47
|
+
puts "Press Ctrl+C to stop"
|
|
48
|
+
|
|
49
|
+
trap("INT") do
|
|
50
|
+
puts "\nStopping alert monitor..."
|
|
51
|
+
exit
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
loop do
|
|
55
|
+
begin
|
|
56
|
+
results = RailsPerformance::Alerts::Checker.check_all
|
|
57
|
+
|
|
58
|
+
results.each do |result|
|
|
59
|
+
case result[:action]
|
|
60
|
+
when :triggered
|
|
61
|
+
puts "[#{Time.now}] ALERT TRIGGERED: #{result[:rule].name}"
|
|
62
|
+
when :resolved
|
|
63
|
+
puts "[#{Time.now}] ALERT RESOLVED: #{result[:rule].name}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
rescue => e
|
|
67
|
+
puts "[#{Time.now}] Error checking alerts: #{e.message}"
|
|
68
|
+
puts e.backtrace.first(5).join("\n") if RailsPerformance.debug
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
sleep 1.minute
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
|
|
5
|
+
<style>
|
|
6
|
+
body {
|
|
7
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
8
|
+
line-height: 1.6;
|
|
9
|
+
color: #333;
|
|
10
|
+
max-width: 600px;
|
|
11
|
+
margin: 0 auto;
|
|
12
|
+
padding: 20px;
|
|
13
|
+
}
|
|
14
|
+
.alert-box {
|
|
15
|
+
border-radius: 8px;
|
|
16
|
+
padding: 20px;
|
|
17
|
+
margin: 20px 0;
|
|
18
|
+
}
|
|
19
|
+
.alert-box.warning {
|
|
20
|
+
background-color: #fff3cd;
|
|
21
|
+
border: 2px solid #ffc107;
|
|
22
|
+
}
|
|
23
|
+
.alert-box.success {
|
|
24
|
+
background-color: #d4edda;
|
|
25
|
+
border: 2px solid #28a745;
|
|
26
|
+
}
|
|
27
|
+
.alert-header {
|
|
28
|
+
font-size: 24px;
|
|
29
|
+
font-weight: bold;
|
|
30
|
+
margin-bottom: 15px;
|
|
31
|
+
}
|
|
32
|
+
.alert-box.warning .alert-header {
|
|
33
|
+
color: #856404;
|
|
34
|
+
}
|
|
35
|
+
.alert-box.success .alert-header {
|
|
36
|
+
color: #155724;
|
|
37
|
+
}
|
|
38
|
+
.alert-details {
|
|
39
|
+
background-color: white;
|
|
40
|
+
border-radius: 4px;
|
|
41
|
+
padding: 15px;
|
|
42
|
+
margin: 15px 0;
|
|
43
|
+
}
|
|
44
|
+
.detail-row {
|
|
45
|
+
display: flex;
|
|
46
|
+
padding: 8px 0;
|
|
47
|
+
border-bottom: 1px solid #f0f0f0;
|
|
48
|
+
}
|
|
49
|
+
.detail-row:last-child {
|
|
50
|
+
border-bottom: none;
|
|
51
|
+
}
|
|
52
|
+
.detail-label {
|
|
53
|
+
font-weight: bold;
|
|
54
|
+
min-width: 150px;
|
|
55
|
+
color: #666;
|
|
56
|
+
}
|
|
57
|
+
.detail-value {
|
|
58
|
+
color: #333;
|
|
59
|
+
}
|
|
60
|
+
.footer {
|
|
61
|
+
margin-top: 30px;
|
|
62
|
+
padding-top: 20px;
|
|
63
|
+
border-top: 1px solid #ddd;
|
|
64
|
+
font-size: 12px;
|
|
65
|
+
color: #999;
|
|
66
|
+
}
|
|
67
|
+
</style>
|
|
68
|
+
</head>
|
|
69
|
+
<body>
|
|
70
|
+
<%= yield %>
|
|
71
|
+
|
|
72
|
+
<div class="footer">
|
|
73
|
+
This is an automated alert from RailsPerformance::Alerts
|
|
74
|
+
</div>
|
|
75
|
+
</body>
|
|
76
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<div class="alert-box success">
|
|
2
|
+
<div class="alert-header">
|
|
3
|
+
✅ Alert Resolved
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<p><strong><%= @name %></strong> <%= @current_value.nil? ? "data is now available." : "has returned to normal levels." %></p>
|
|
7
|
+
|
|
8
|
+
<div class="alert-details">
|
|
9
|
+
<div class="detail-row">
|
|
10
|
+
<div class="detail-label">Current Value:</div>
|
|
11
|
+
<div class="detail-value"><%= @current_value.nil? ? "No data available" : @current_value %></div>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="detail-row">
|
|
14
|
+
<div class="detail-label">Was Triggered At:</div>
|
|
15
|
+
<div class="detail-value"><%= @was_triggered_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></div>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="detail-row">
|
|
18
|
+
<div class="detail-label">Resolved At:</div>
|
|
19
|
+
<div class="detail-value"><%= @resolved_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></div>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="detail-row">
|
|
22
|
+
<div class="detail-label">Duration:</div>
|
|
23
|
+
<div class="detail-value"><%= @duration_triggered %></div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<p style="margin-top: 20px;">
|
|
28
|
+
The alert condition is no longer active.
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
✅ ALERT RESOLVED ✅
|
|
2
|
+
|
|
3
|
+
<%= @name %> has returned to normal levels.
|
|
4
|
+
|
|
5
|
+
Alert Details:
|
|
6
|
+
----------------
|
|
7
|
+
Current Value: <%= @current_value %>
|
|
8
|
+
Was Triggered At: <%= @was_triggered_at.strftime("%Y-%m-%d %H:%M:%S %Z") %>
|
|
9
|
+
Resolved At: <%= @resolved_at.strftime("%Y-%m-%d %H:%M:%S %Z") %>
|
|
10
|
+
Duration: <%= @duration_triggered %>
|
|
11
|
+
|
|
12
|
+
The alert condition is no longer active.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
This is an automated alert from RailsPerformance::Alerts
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<div class="alert-box warning">
|
|
2
|
+
<div class="alert-header">
|
|
3
|
+
🚨 Alert Triggered
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<p><strong><%= @name %></strong> <%= @current_value.nil? ? "has no data available." : "has exceeded its threshold." %></p>
|
|
7
|
+
|
|
8
|
+
<div class="alert-details">
|
|
9
|
+
<div class="detail-row">
|
|
10
|
+
<div class="detail-label">Current Value:</div>
|
|
11
|
+
<div class="detail-value"><%= @current_value.nil? ? "No data available" : @current_value %></div>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="detail-row">
|
|
14
|
+
<div class="detail-label">Time Window:</div>
|
|
15
|
+
<div class="detail-value"><%= @time_window %></div>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="detail-row">
|
|
18
|
+
<div class="detail-label">Duration:</div>
|
|
19
|
+
<div class="detail-value"><%= @duration %></div>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="detail-row">
|
|
22
|
+
<div class="detail-label">Triggered At:</div>
|
|
23
|
+
<div class="detail-value"><%= @triggered_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<p style="margin-top: 20px;">
|
|
28
|
+
Please investigate this issue as soon as possible.
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
🚨 ALERT TRIGGERED 🚨
|
|
2
|
+
|
|
3
|
+
<%= @name %> <%= @current_value.nil? ? "has no data available." : "has exceeded its threshold." %>
|
|
4
|
+
|
|
5
|
+
Alert Details:
|
|
6
|
+
----------------
|
|
7
|
+
Current Value: <%= @current_value.nil? ? "No data available" : @current_value %>
|
|
8
|
+
Time Window: <%= @time_window %>
|
|
9
|
+
Duration: <%= @duration %>
|
|
10
|
+
Triggered At: <%= @triggered_at.strftime("%Y-%m-%d %H:%M:%S %Z") %>
|
|
11
|
+
|
|
12
|
+
Please investigate this issue as soon as possible.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
This is an automated alert from RailsPerformance::Alerts
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require "rails_performance"
|
|
2
|
+
require "active_support/core_ext/integer"
|
|
3
|
+
require_relative "alerts/version"
|
|
4
|
+
require_relative "alerts/rule"
|
|
5
|
+
require_relative "alerts/state"
|
|
6
|
+
require_relative "alerts/checker"
|
|
7
|
+
require_relative "alerts/checker_job" if defined?(ActiveJob)
|
|
8
|
+
require_relative "alerts/mailer" if defined?(ActionMailer)
|
|
9
|
+
require_relative "alerts/email_notifier" if defined?(ActionMailer)
|
|
10
|
+
require_relative "alerts/railtie" if defined?(Rails::Railtie)
|
|
11
|
+
|
|
12
|
+
module RailsPerformance
|
|
13
|
+
module Alerts
|
|
14
|
+
class ConfigurationError < StandardError; end
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def validate_configuration!
|
|
18
|
+
# Check if alert_rules exist without handler
|
|
19
|
+
if RailsPerformance.alert_rules.any? && RailsPerformance.alert_handler.nil?
|
|
20
|
+
raise ConfigurationError, "alert_rules are configured but alert_handler is not set"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Validate handler is callable
|
|
24
|
+
if RailsPerformance.alert_handler && !RailsPerformance.alert_handler.respond_to?(:call)
|
|
25
|
+
raise ConfigurationError, "alert_handler must respond to :call"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Validate each rule can be instantiated
|
|
29
|
+
RailsPerformance.alert_rules.each_with_index do |rule_config, index|
|
|
30
|
+
begin
|
|
31
|
+
Rule.new(rule_config)
|
|
32
|
+
rescue => e
|
|
33
|
+
raise ConfigurationError, "Invalid alert rule at index #{index}: #{e.message}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
mattr_accessor :alert_rules
|
|
41
|
+
@@alert_rules = []
|
|
42
|
+
|
|
43
|
+
mattr_accessor :alert_handler
|
|
44
|
+
@@alert_handler = nil
|
|
45
|
+
|
|
46
|
+
singleton_class.prepend Module.new {
|
|
47
|
+
def setup
|
|
48
|
+
super
|
|
49
|
+
RailsPerformance::Alerts.validate_configuration!
|
|
50
|
+
end
|
|
51
|
+
}
|
|
52
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "rails_performance/alerts"
|
metadata
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rails_performance-alerts
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Micah
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-10-29 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rails_performance
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: minitest
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: redis
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
description: Add user-configurable alerts to rails_performance gem for monitoring
|
|
56
|
+
response time, throughput, error rates, and system resources
|
|
57
|
+
email:
|
|
58
|
+
- micah@example.com
|
|
59
|
+
executables: []
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- MIT-LICENSE
|
|
64
|
+
- README.md
|
|
65
|
+
- Rakefile
|
|
66
|
+
- lib/rails_performance-alerts.rb
|
|
67
|
+
- lib/rails_performance/alerts.rb
|
|
68
|
+
- lib/rails_performance/alerts/checker.rb
|
|
69
|
+
- lib/rails_performance/alerts/checker_job.rb
|
|
70
|
+
- lib/rails_performance/alerts/email_notifier.rb
|
|
71
|
+
- lib/rails_performance/alerts/mailer.rb
|
|
72
|
+
- lib/rails_performance/alerts/railtie.rb
|
|
73
|
+
- lib/rails_performance/alerts/rule.rb
|
|
74
|
+
- lib/rails_performance/alerts/state.rb
|
|
75
|
+
- lib/rails_performance/alerts/tasks.rake
|
|
76
|
+
- lib/rails_performance/alerts/version.rb
|
|
77
|
+
- lib/rails_performance/alerts/views/rails_performance/alerts/layouts/mailer.html.erb
|
|
78
|
+
- lib/rails_performance/alerts/views/rails_performance/alerts/mailer/resolved.html.erb
|
|
79
|
+
- lib/rails_performance/alerts/views/rails_performance/alerts/mailer/resolved.text.erb
|
|
80
|
+
- lib/rails_performance/alerts/views/rails_performance/alerts/mailer/triggered.html.erb
|
|
81
|
+
- lib/rails_performance/alerts/views/rails_performance/alerts/mailer/triggered.text.erb
|
|
82
|
+
homepage: https://github.com/username/rails_performance-alerts
|
|
83
|
+
licenses:
|
|
84
|
+
- MIT
|
|
85
|
+
metadata: {}
|
|
86
|
+
post_install_message:
|
|
87
|
+
rdoc_options: []
|
|
88
|
+
require_paths:
|
|
89
|
+
- lib
|
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - ">="
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '0'
|
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
|
+
requirements:
|
|
97
|
+
- - ">="
|
|
98
|
+
- !ruby/object:Gem::Version
|
|
99
|
+
version: '0'
|
|
100
|
+
requirements: []
|
|
101
|
+
rubygems_version: 3.5.11
|
|
102
|
+
signing_key:
|
|
103
|
+
specification_version: 4
|
|
104
|
+
summary: Customizable alerts for rails_performance metrics
|
|
105
|
+
test_files: []
|