Module: ErrorRadar::Notifier
- Defined in:
- lib/error_radar/notifier.rb
Overview
Dispatches alert notifications (Slack, Discord, email, webhooks, custom callbacks) after an ErrorLog is persisted. Called from Tracking.capture and Tracking.notify. Never raises — a broken notification must not affect the host application.
Notification events:
:new_error — first time a fingerprint is seen (no throttle)
:spike — error rate exceeds config.spike_threshold in the window
:recurring — matches :critical or :all rule, throttled to 1/hour
Constant Summary collapse
- THROTTLE_MUTEX =
Mutex.new
- THROTTLE =
key => Time of last notification (in-memory, resets on restart) Keys: fingerprint (for :recurring) or "spike:fingerprint" (for :spike)
{}
- THROTTLE_INTERVAL =
seconds between repeat alerts per fingerprint
3600
Class Method Summary collapse
- .app_name ⇒ Object
-
.determine_event(log, rules) ⇒ Object
── Fire decision ──────────────────────────────────────────────────────.
- .dispatch(log) ⇒ Object
-
.error_url(log) ⇒ Object
── Helpers ───────────────────────────────────────────────────────────.
-
.fire_all(log, event) ⇒ Object
── Channel dispatchers ────────────────────────────────────────────────.
- .safe_call(cb, log) ⇒ Object
- .send_discord(log, event) ⇒ Object
- .send_email(log, event) ⇒ Object
- .send_slack(log, event) ⇒ Object
- .send_webhook(log, url, event) ⇒ Object
- .throttle_ok?(key, interval = THROTTLE_INTERVAL) ⇒ Boolean
Class Method Details
.app_name ⇒ Object
124 125 126 127 |
# File 'lib/error_radar/notifier.rb', line 124 def self.app_name ErrorRadar.config.app_name || (defined?(Rails) && Rails.application ? Rails.application.class.module_parent_name : 'App') end |
.determine_event(log, rules) ⇒ Object
── Fire decision ──────────────────────────────────────────────────────
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/error_radar/notifier.rb', line 42 def self.determine_event(log, rules) # :new_error fires exactly once per fingerprint, no throttle return :new_error if rules.include?(:new_error) && log.new_fingerprint? # :spike — rate-based alert when occurrences in window exceed threshold if rules.include?(:spike) spike_count = SpikeDetector.check(log) if spike_count window_secs = ErrorRadar.config.spike_window_minutes * 60 if throttle_ok?("spike:#{log.fingerprint}", window_secs) log.instance_variable_set(:@spike_data, { count: spike_count, window_minutes: ErrorRadar.config.spike_window_minutes }) return :spike end end end # :critical / :all — throttled recurring alerts matches = rules.include?(:all) || (rules.include?(:critical) && log.severity_critical?) return :recurring if matches && throttle_ok?(log.fingerprint, THROTTLE_INTERVAL) nil end |
.dispatch(log) ⇒ Object
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/error_radar/notifier.rb', line 20 def self.dispatch(log) return unless ErrorRadar.config.enabled rules = Array(ErrorRadar.config.notify_on).map(&:to_sym) return if rules.empty? # Record the hit for in-memory spike tracking before we test the count. if rules.include?(:spike) require 'error_radar/spike_detector' SpikeDetector.record_hit(log.fingerprint) end event = determine_event(log, rules) return unless event fire_all(log, event) rescue StandardError => e ErrorRadar::Tracking.warn_internal("Notifier.dispatch failed: #{e.}") end |
.error_url(log) ⇒ Object
── Helpers ───────────────────────────────────────────────────────────
117 118 119 120 121 122 |
# File 'lib/error_radar/notifier.rb', line 117 def self.error_url(log) host = ErrorRadar.config.app_host.to_s.chomp('/') return nil if host.empty? "#{host}/error_radar/errors/#{log.id}" end |
.fire_all(log, event) ⇒ Object
── Channel dispatchers ────────────────────────────────────────────────
80 81 82 83 84 85 86 87 |
# File 'lib/error_radar/notifier.rb', line 80 def self.fire_all(log, event) cfg = ErrorRadar.config send_slack(log, event) if cfg.slack_webhook_url.to_s.start_with?('http') send_discord(log, event) if cfg.discord_webhook_url.to_s.start_with?('http') send_email(log, event) if cfg.email_recipients.any? cfg.webhook_urls.each { |url| send_webhook(log, url, event) } cfg.error_callbacks.each { |cb| safe_call(cb, log) } end |
.safe_call(cb, log) ⇒ Object
109 110 111 112 113 |
# File 'lib/error_radar/notifier.rb', line 109 def self.safe_call(cb, log) cb.call(log) rescue StandardError => e ErrorRadar::Tracking.warn_internal("on_error callback failed: #{e.}") end |
.send_discord(log, event) ⇒ Object
94 95 96 97 |
# File 'lib/error_radar/notifier.rb', line 94 def self.send_discord(log, event) require 'error_radar/notifications/discord' Notifications::Discord.deliver(log, event) end |
.send_email(log, event) ⇒ Object
99 100 101 102 |
# File 'lib/error_radar/notifier.rb', line 99 def self.send_email(log, event) require 'error_radar/notifications/email' Notifications::Email.deliver(log, event) end |
.send_slack(log, event) ⇒ Object
89 90 91 92 |
# File 'lib/error_radar/notifier.rb', line 89 def self.send_slack(log, event) require 'error_radar/notifications/slack' Notifications::Slack.deliver(log, event) end |
.send_webhook(log, url, event) ⇒ Object
104 105 106 107 |
# File 'lib/error_radar/notifier.rb', line 104 def self.send_webhook(log, url, event) require 'error_radar/notifications/webhook' Notifications::Webhook.deliver(log, url: url, event: event) end |
.throttle_ok?(key, interval = THROTTLE_INTERVAL) ⇒ Boolean
69 70 71 72 73 74 75 76 |
# File 'lib/error_radar/notifier.rb', line 69 def self.throttle_ok?(key, interval = THROTTLE_INTERVAL) THROTTLE_MUTEX.synchronize do last = THROTTLE[key] ok = last.nil? || (Time.current - last) >= interval THROTTLE[key] = Time.current if ok ok end end |