prevent carry-over conditions
[god.git] / lib / god / hub.rb
blob382d40d1f89effd4c9baa96788d58df1d440f7e2
1 module God
2   
3   class Hub
4     class << self
5       # directory to hold conditions and their corresponding metric
6       # {condition => metric}
7       attr_accessor :directory
8       
9       # mutex to keep the directory consistent
10       attr_accessor :mutex
11     end
12     
13     self.directory = {}
14     self.mutex = Monitor.new
15     
16     # Attach the condition to the hub and schedule/register it
17     #   +condition+ is the Condition to attach
18     #   +metric+ is the Metric to which the condition belongs
19     #
20     # Returns nothing
21     def self.attach(condition, metric)
22       self.mutex.synchronize do
23         condition.phase = condition.watch.phase
24         condition.reset
25         self.directory[condition] = metric
26         
27         case condition
28           when PollCondition
29             Timer.get.schedule(condition, 0)
30           when EventCondition, TriggerCondition
31             condition.register
32         end
33       end
34     end
35     
36     # Detach the condition from the hub and unschedule/deregister it
37     #   +condition+ is the Condition to detach
38     #
39     # Returns nothing
40     def self.detach(condition)
41       self.mutex.synchronize do
42         self.directory.delete(condition)
43         
44         case condition
45           when PollCondition
46             Timer.get.unschedule(condition)
47           when EventCondition, TriggerCondition
48             condition.deregister
49         end
50       end
51     end
52     
53     # Trigger evaluation of the condition
54     #   +condition+ is the Condition to evaluate
55     #   +phase+ is the phase of the Watch at the time the condition was scheduled
56     #
57     # Returns nothing
58     def self.trigger(condition, phase = nil)
59       self.mutex.synchronize do
60         case condition
61           when PollCondition
62             self.handle_poll(condition, phase)
63           when EventCondition, TriggerCondition
64             self.handle_event(condition)
65         end
66       end
67     end
68     
69     # private
70     
71     # Asynchronously evaluate and handle the given poll condition. Handles logging
72     # notifications, and moving to the new state if necessary
73     #   +condition+ is the Condition to handle
74     #   +phase+ is the phase of the Watch that should be matched
75     #
76     # Returns nothing
77     def self.handle_poll(condition, phase)
78       metric = self.directory[condition]
79       
80       # it's possible that the timer will trigger an event before it can be cleared
81       # by an exiting metric, in which case it should be ignored
82       return if metric.nil?
83       
84       Thread.new do
85         begin
86           watch = metric.watch
87           
88           watch.mutex.synchronize do
89             # ensure this condition is still active when we finally get the mutex
90             if self.directory[condition] && phase == watch.phase
91               # run the test
92               result = condition.test
93               
94               # log
95               messages = self.log(watch, metric, condition, result)
96               
97               # notify
98               if condition.notify && self.trigger?(metric, result)
99                 self.notify(condition, messages.last)
100               end
101               
102               # after-condition
103               condition.after
104               
105               # get the destination
106               dest = 
107               if result && condition.transition
108                 # condition override
109                 condition.transition
110               else
111                 # regular
112                 metric.destination && metric.destination[result]
113               end
114               
115               # transition or reschedule
116               if dest
117                 # transition
118                 begin
119                   watch.move(dest)
120                 rescue EventRegistrationFailedError
121                   msg = watch.name + ' Event registration failed, moving back to previous state'
122                   applog(watch, :info, msg)
123                   
124                   dest = watch.state
125                   retry
126                 end
127               else
128                 # reschedule
129                 Timer.get.schedule(condition)
130               end
131             end
132           end
133         rescue Exception => e
134           message = format("Unhandled exception (%s): %s\n%s",
135                            e.class, e.message, e.backtrace.join("\n"))
136           applog(nil, :fatal, message)
137         end
138       end
139     end
140     
141     # Asynchronously evaluate and handle the given event condition. Handles logging
142     # notifications, and moving to the new state if necessary
143     #   +condition+ is the Condition to handle
144     #
145     # Returns nothing
146     def self.handle_event(condition)
147       metric = self.directory[condition]
148       
149       # it's possible that the timer will trigger an event before it can be cleared
150       # by an exiting metric, in which case it should be ignored
151       return if metric.nil?
152       
153       Thread.new do
154         begin
155           watch = metric.watch
156           
157           watch.mutex.synchronize do
158             # ensure this condition is still active when we finally get the mutex
159             if self.directory[condition]
160               # log
161               messages = self.log(watch, metric, condition, true)
162               
163               # notify
164               if condition.notify && self.trigger?(metric, true)
165                 self.notify(condition, messages.last)
166               end
167               
168               # get the destination
169               dest = 
170               if condition.transition
171                 # condition override
172                 condition.transition
173               else
174                 # regular
175                 metric.destination && metric.destination[true]
176               end
177               
178               if dest
179                 watch.move(dest)
180               end
181             end
182           end
183         rescue Exception => e
184           message = format("Unhandled exception (%s): %s\n%s",
185                            e.class, e.message, e.backtrace.join("\n"))
186           applog(nil, :fatal, message)
187         end
188       end
189     end
190     
191     # Determine whether a trigger happened
192     #   +metric+ is the Metric
193     #   +result+ is the result from the condition's test
194     #
195     # Returns Boolean
196     def self.trigger?(metric, result)
197       metric.destination && metric.destination[result]
198     end
199     
200     # Log info about the condition and return the list of messages logged
201     #   +watch+ is the Watch
202     #   +metric+ is the Metric
203     #   +condition+ is the Condition
204     #   +result+ is the Boolean result of the condition test evaluation
205     #
206     # Returns String[]
207     def self.log(watch, metric, condition, result)
208       status = 
209       if self.trigger?(metric, result)
210         "[trigger]"
211       else
212         "[ok]"
213       end
214       
215       messages = []
216       
217       # log info if available
218       if condition.info
219         Array(condition.info).each do |condition_info|
220           messages << "#{watch.name} #{status} #{condition_info} (#{condition.base_name})"
221           applog(watch, :info, messages.last)
222         end
223       else
224         messages << "#{watch.name} #{status} (#{condition.base_name})"
225         applog(watch, :info, messages.last)
226       end
227       
228       # log
229       debug_message = watch.name + ' ' + condition.base_name + " [#{result}] " + self.dest_desc(metric, condition)
230       applog(watch, :debug, debug_message)
231       
232       messages
233     end
234     
235     # Format the destination specification for use in debug logging
236     #   +metric+ is the Metric
237     #   +condition+ is the Condition
238     #
239     # Returns String
240     def self.dest_desc(metric, condition)
241       if condition.transition
242         {true => condition.transition}.inspect
243       else
244         if metric.destination
245           metric.destination.inspect
246         else
247           'none'
248         end
249       end
250     end
251     
252     # Notify all recipeients of the given condition with the specified message
253     #   +condition+ is the Condition
254     #   +message+ is the String message to send
255     #
256     # Returns nothing
257     def self.notify(condition, message)
258       spec = Contact.normalize(condition.notify)
259       unmatched = []
260       
261       # resolve contacts
262       resolved_contacts =
263       spec[:contacts].inject([]) do |acc, contact_name_or_group|
264         cons = Array(God.contacts[contact_name_or_group] || God.contact_groups[contact_name_or_group])
265         unmatched << contact_name_or_group if cons.empty?
266         acc += cons
267         acc
268       end
269       
270       # warn about unmatched contacts
271       unless unmatched.empty?
272         msg = "#{condition.watch.name} no matching contacts for '#{unmatched.join(", ")}'"
273         applog(condition.watch, :warn, msg)
274       end
275       
276       # notify each contact
277       resolved_contacts.each do |c|
278         host = `hostname`.chomp rescue 'none'
279         c.notify(message, Time.now, spec[:priority], spec[:category], host)
280         
281         msg = "#{condition.watch.name} #{c.info ? c.info : "notification sent for contact: #{c.name}"} (#{c.base_name})"
282         
283         applog(condition.watch, :info, msg % [])
284       end
285     end
286   end
287