aboutsummaryrefslogtreecommitdiff
path: root/lib/mauve/notification.rb
diff options
context:
space:
mode:
authorPatrick J Cherry <patrick@bytemark.co.uk>2011-04-13 17:03:16 +0100
committerPatrick J Cherry <patrick@bytemark.co.uk>2011-04-13 17:03:16 +0100
commit89a67770e66d11740948e90a41db6cee0482cf8e (patch)
treebe858515fb789a89d68f94975690ab019813726c /lib/mauve/notification.rb
new version.
Diffstat (limited to 'lib/mauve/notification.rb')
-rw-r--r--lib/mauve/notification.rb165
1 files changed, 165 insertions, 0 deletions
diff --git a/lib/mauve/notification.rb b/lib/mauve/notification.rb
new file mode 100644
index 0000000..2220211
--- /dev/null
+++ b/lib/mauve/notification.rb
@@ -0,0 +1,165 @@
+# encoding: UTF-8
+require 'mauve/person'
+require 'mauve/notifiers'
+
+module Mauve
+ # This class provides an execution context for the code found in 'during'
+ # blocks in the configuration file. This code specifies when an alert
+ # should cause notifications to be generated, and can access @time and
+ # @alert variables. There are also some helper methods to provide
+ # oft-needed functionality such as hours_in_day.
+ #
+ # e.g. to send alerts only between 10 and 11 am:
+ #
+ # during = Proc.new { @time.hour == 10 }
+ #
+ # ... later on ...
+ #
+ # DuringRunner.new(MauveTime.now, my_alert, &during).inside?
+ #
+ # ... or to ask when an alert will next be cued ...
+ #
+ # DuringRunner.new(MauveTime.now, my_alert, &during).find_next
+ #
+ # which will return a MauveTime object, or nil if the time period will
+ # not be valid again, at least not in the next week.
+ #
+ class DuringRunner
+
+ def initialize(time, alert=nil, &during)
+ raise ArgumentError.new("Must supply time (not #{time.inspect})") unless time.is_a?(Time)
+ @time = time
+ @alert = alert
+ @during = during || Proc.new { true }
+ @logger = Log4r::Logger.new "mauve::DuringRunner"
+ end
+
+ def now?
+ instance_eval(&@during)
+ end
+
+ def find_next(interval)
+ interval = 300 if true == interval.nil?
+ offset = (@time.nil?)? MauveTime.now : @time
+ plus_one_week = MauveTime.now + 604800 # ish
+ while offset < plus_one_week
+ offset += interval
+ if DuringRunner.new(offset, @alert, &@during).now?
+ @logger.debug("Found reminder time of #{offset}")
+ return offset
+ end
+ end
+ @logger.info("Could not find a reminder time less than a week "+
+ "for #{@alert}.")
+ nil # never again
+ end
+
+ protected
+ def hours_in_day(*hours)
+ x_in_list_of_y(@time.hour, hours.flatten)
+ end
+
+ def days_in_week(*days)
+ x_in_list_of_y(@time.wday, days.flatten)
+ end
+
+ ## Return true if the alert has not been acknowledged within a certain time.
+ #
+ def unacknowledged(seconds)
+ @alert &&
+ @alert.raised? &&
+ !@alert.acknowledged? &&
+ (@time - @alert.raised_at.to_time) > seconds
+ end
+
+ def x_in_list_of_y(x,y)
+ y.any? do |range|
+ if range.respond_to?("include?")
+ range.include?(x)
+ else
+ range == x
+ end
+ end
+ end
+
+ def working_hours?
+ now = (@time || MauveTime.now)
+ (8..17).include?(now.hour) and (1..5).include?(now.wday)
+ end
+
+ # Return true in the dead zone between 3 and 7 in the morning.
+ #
+ # Nota bene that this is used with different times in the reminder section.
+ #
+ # @return [Boolean] Whether now is a in the dead zone or not.
+ def dead_zone?
+ now = (@time || MauveTime.now)
+ (3..6).include?(now.hour)
+ end
+
+ end
+
+ # A Notification is an instruction to notify a list of people, at a
+ # particular alert level, on a periodic basis, and optionally under
+ # certain conditions specified by a block of code.
+ #
+ class Notification < Struct.new(:people, :level, :every, :during, :list)
+
+ def to_s
+ "#<Notification:of #{people.map { |p| p.username }.join(',')} at level #{level} every #{every}>"
+ end
+
+ attr_reader :thread_list
+
+ def initialize(people, level)
+
+ self.level = level
+ self.every = 300
+ self.people = people
+ end
+
+ def logger ; Log4r::Logger.new self.class.to_s ; end
+
+
+ # Updated code, now takes account of lists of people.
+ #
+ # @TODO refactor so we can test this more easily.
+ #
+ # @TODO Make sure that if no notifications is send at all, we log this
+ # as an error so that an email is send to the developers. Hum, we
+ # could have person.alert_changed return true if a notification was
+ # send (false otherwise) and add it to a queue. Then, dequeue till
+ # we see a "true" and abort. However, this needs a timeout loop
+ # around it and we will slow down the whole notificatin since it
+ # will have to wait untill such a time as it gets a true or timeout.
+ # Not ideal. A quick fix is to make sure that the clause in the
+ # configuration has a fall back that will send an alert in all cases.
+ #
+ def alert_changed(alert)
+
+ # Should we notificy at all?
+ is_relevant = DuringRunner.new(MauveTime.now, alert, &during).now?
+
+ to_notify = people.collect do |person|
+ case person
+ when Person
+ person
+ when PeopleList
+ person.people
+ else
+ logger.warn "Unable to notify #{person} (unrecognised class #{person.class})"
+ []
+ end
+ end.flatten.uniq.each do |person|
+ person.alert_changed(level, alert, is_relevant, remind_at_next(alert))
+ end
+ end
+
+ def remind_at_next(alert)
+ return nil unless alert.raised?
+ DuringRunner.new(MauveTime.now, alert, &during).find_next(every)
+ end
+
+ end
+
+end