diff --git a/lib/exception_notifier.rb b/lib/exception_notifier.rb index 3173cab9..2cc5268e 100644 --- a/lib/exception_notifier.rb +++ b/lib/exception_notifier.rb @@ -10,6 +10,7 @@ module ExceptionNotifier autoload :HipchatNotifier, 'exception_notifier/hipchat_notifier' autoload :WebhookNotifier, 'exception_notifier/webhook_notifier' autoload :IrcNotifier, 'exception_notifier/irc_notifier' + autoload :ThrottleNotifier, 'exception_notifier/throttle_notifier' class UndefinedNotifierError < StandardError; end @@ -74,6 +75,12 @@ def clear_ignore_conditions! @@ignores.clear end + def create_notifier(name, options) + notifier_classname = "#{name}_notifier".camelize + notifier_class = ExceptionNotifier.const_get(notifier_classname) + notifier_class.new(options) + end + private def ignored?(exception, options) @@ignores.any?{ |condition| condition.call(exception, options) } @@ -95,10 +102,7 @@ def fire_notification(notifier_name, exception, options) end def create_and_register_notifier(name, options) - notifier_classname = "#{name}_notifier".camelize - notifier_class = ExceptionNotifier.const_get(notifier_classname) - notifier = notifier_class.new(options) - register_exception_notifier(name, notifier) + register_exception_notifier(name, create_notifier(name, options)) rescue NameError => e raise UndefinedNotifierError, "No notifier named '#{name}' was found. Please, revise your configuration options. Cause: #{e.message}" end diff --git a/lib/exception_notifier/throttle_notifier.rb b/lib/exception_notifier/throttle_notifier.rb new file mode 100644 index 00000000..8f9dd4b3 --- /dev/null +++ b/lib/exception_notifier/throttle_notifier.rb @@ -0,0 +1,23 @@ +module ExceptionNotifier + class ThrottleNotifier + def initialize options={} + @notifier = ExceptionNotifier.create_notifier(options[:notifier], options[:notifier_options] || {}) + @max_notifications_per_hour = 3 + @past_exceptions = Hash.new {|h,k| h[k] = []} + end + + def call(exception, options={}) + key = exception.class.to_s.each_byte.reduce(&:+) + + past_times = @past_exceptions[key] + past_times.reject! {|t| (Time.now - t) >= 3600} + past_times << Time.now + + if past_times.size <= @max_notifications_per_hour + @notifier.call exception, options + else + ExceptionNotifier::logger.warn "An exception occurred (#{exception}) and the notifier #{@notifier} was throttled." + end + end + end +end diff --git a/test/exception_notifier/throttle_notifier_test.rb b/test/exception_notifier/throttle_notifier_test.rb new file mode 100644 index 00000000..916b592c --- /dev/null +++ b/test/exception_notifier/throttle_notifier_test.rb @@ -0,0 +1,41 @@ +require 'test_helper' + +class ThrottleNotifierTest < ActiveSupport::TestCase + setup do + @throttle = ExceptionNotifier::ThrottleNotifier.new notifier: :email + @notifier = @throttle.instance_variable_get "@notifier" + end + + test "should delegate to notifier by default" do + exception = Exception.new + options = {} + @notifier.expects(:call).with(exception, options) + + @throttle.call exception, options + end + + test "should stop delegating after three equivalent exceptions occur" do + exception = Exception.new + options = {} + @notifier.expects(:call).with(exception, options).times(3) + ExceptionNotifier::logger.expects(:warn).stubs(:warn) + + 4.times { @throttle.call exception, options } + end + + test "should resume delegating after an hour has passed" do + Time.stubs(:now).returns(Time.new 2000,1,1,1) + exception = Exception.new + options = {} + @notifier.stubs(:call) + ExceptionNotifier::logger.expects(:warn).times(2).stubs(:warn) + + 4.times { @throttle.call exception, options } + + Time.stubs(:now).returns(Time.new 2000,1,1,2) + @notifier.expects(:call).with(exception, options).times(3) + + 4.times { @throttle.call exception, options } + end +end +