diff --git a/README.md b/README.md index 4d35bca1..440d66ee 100644 --- a/README.md +++ b/README.md @@ -855,6 +855,17 @@ Rails.application.config.middleware.use ExceptionNotification::Rack, } ``` +### :min_notification_interval + +*Something that responds to `call` or `to_i`* + +Ignores repeated exceptions which happen in a set interval in seconds. Values of `0` or `nil` means do not ignore repeated exceptions. Only works if Rails cache is available. + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + :min_notification_interval = 600 # At most one exception every 10 minutes +``` + ### :ignore_if *Lambda, default: nil* diff --git a/lib/exception_notification/rack.rb b/lib/exception_notification/rack.rb index 6ac907f6..fcd4abe7 100644 --- a/lib/exception_notification/rack.rb +++ b/lib/exception_notification/rack.rb @@ -5,7 +5,13 @@ class CascadePassException < Exception; end def initialize(app, options = {}) @app = app - ExceptionNotifier.ignored_exceptions = options.delete(:ignore_exceptions) if options.key?(:ignore_exceptions) + if options.key?(:ignore_exceptions) + ExceptionNotifier.ignored_exceptions = options.delete(:ignore_exceptions) + end + + if options.key?(:min_notification_interval) + ExceptionNotifier.min_notification_interval = options.delete(:min_notification_interval) + end if options.key?(:ignore_if) rack_ignore = options.delete(:ignore_if) diff --git a/lib/exception_notifier.rb b/lib/exception_notifier.rb index 5e403856..3cdabad4 100644 --- a/lib/exception_notifier.rb +++ b/lib/exception_notifier.rb @@ -6,6 +6,7 @@ module ExceptionNotifier autoload :BacktraceCleaner, 'exception_notifier/modules/backtrace_cleaner' + autoload :Throttling, 'exception_notifier/modules/throttling' autoload :Notifier, 'exception_notifier/notifier' autoload :EmailNotifier, 'exception_notifier/email_notifier' @@ -86,9 +87,14 @@ def clear_ignore_conditions! @@ignores.clear end + def min_notification_interval=(interval) + Throttling.min_notification_interval = interval + end + private def ignored?(exception, options) - @@ignores.any?{ |condition| condition.call(exception, options) } + ignored = @@ignores.any? { |condition| condition.call(exception, options) } + ignored || Throttling.ignore_exception?(exception) rescue Exception => e raise e if @@testing_mode diff --git a/lib/exception_notifier/modules/throttling.rb b/lib/exception_notifier/modules/throttling.rb new file mode 100644 index 00000000..bf3551e4 --- /dev/null +++ b/lib/exception_notifier/modules/throttling.rb @@ -0,0 +1,61 @@ +require 'digest/sha1' + +module ExceptionNotifier::Throttling + + @min_notification_interval = 0 + + class << self + + def ignore_exception?(exception) + return false unless rails_cache_available? + + interval = min_notification_interval_for(exception) + return false if interval <= 0 + + key = ['exception-notification-throttling-', exception_signature(exception)].join('-') + + if Rails.cache.read(key) + true + else + Rails.cache.write(key, true, expires_in: interval) + false + end + end + + def min_notification_interval=(interval) + @min_notification_interval = normalize_min_notification_interval(interval) + end + + private + + attr_reader :min_notification_interval + + def rails_cache_available? + defined?(Rails) && Rails.respond_to?(:cache) + end + + def exception_signature(exception) + signature = [exception.class, exception.message, exception.backtrace].join('') + Digest::SHA1.hexdigest(signature) + end + + def normalize_min_notification_interval(interval) + return interval if interval.respond_to?(:call) + + begin + interval.to_i + rescue + raise "Invalid min notification interval: #{interval.inspect}" + end + end + + def min_notification_interval_for(exception) + interval = min_notification_interval + if interval.respond_to?(:call) + normalize_min_notification_interval(interval.call(exception)) + else + interval + end + end + end +end