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