add notification system
[god.git] / lib / god.rb
blob7be2493b5a81824ab6138de24d11e686d7046dcd
1 $:.unshift File.dirname(__FILE__)     # For use/testing when no gem is installed
3 # core
4 require 'logger'
6 # stdlib
7 require 'syslog'
9 # internal requires
10 require 'god/errors'
11 require 'god/logger'
12 require 'god/system/process'
13 require 'god/dependency_graph'
14 require 'god/timeline'
15 require 'god/configurable'
17 require 'god/behavior'
18 require 'god/behaviors/clean_pid_file'
19 require 'god/behaviors/notify_when_flapping'
21 require 'god/condition'
22 require 'god/conditions/process_running'
23 require 'god/conditions/process_exits'
24 require 'god/conditions/tries'
25 require 'god/conditions/memory_usage'
26 require 'god/conditions/cpu_usage'
27 require 'god/conditions/always'
28 require 'god/conditions/lambda'
29 require 'god/conditions/degrading_lambda'
30 require 'god/conditions/flapping'
32 require 'god/contact'
33 require 'god/contacts/email'
35 require 'god/reporter'
36 require 'god/server'
37 require 'god/timer'
38 require 'god/hub'
40 require 'god/metric'
41 require 'god/watch'
43 require 'god/trigger'
44 require 'god/event_handler'
45 require 'god/registry'
46 require 'god/process'
48 require 'god/sugar'
50 $:.unshift File.join(File.dirname(__FILE__), *%w[.. ext god])
52 begin
53   Syslog.open('god')
54 rescue RuntimeError
55   Syslog.reopen('god')
56 end
58 God::EventHandler.load
60 module Kernel
61   # Override abort to exit without executing the at_exit hook
62   def abort(text)
63     puts text
64     exit!
65   end
66 end
68 module God
69   VERSION = '0.4.0'
70   
71   LOG = Logger.new
72     
73   LOG_BUFFER_SIZE_DEFAULT = 100
74   PID_FILE_DIRECTORY_DEFAULT = '/var/run/god'
75   DRB_PORT_DEFAULT = 17165
76   DRB_ALLOW_DEFAULT = ['127.0.0.1']
77   
78   class << self
79     # user configurable
80     attr_accessor :host,
81                   :port,
82                   :allow,
83                   :log_buffer_size,
84                   :pid_file_directory
85     
86     # internal
87     attr_accessor :inited,
88                   :running,
89                   :pending_watches,
90                   :server,
91                   :watches,
92                   :groups,
93                   :contacts,
94                   :contact_groups
95   end
96   
97   def self.init
98     if self.inited
99       abort "God.init must be called before any Watches"
100     end
101     
102     self.internal_init
103     
104     # yield to the config file
105     yield self if block_given?
106   end
107   
108   def self.internal_init
109     # only do this once
110     return if self.inited
111     
112     # variable init
113     self.watches = {}
114     self.groups = {}
115     self.pending_watches = []
116     self.contacts = {}
117     self.contact_groups = {}
118     
119     # set defaults
120     self.log_buffer_size = LOG_BUFFER_SIZE_DEFAULT
121     self.pid_file_directory = PID_FILE_DIRECTORY_DEFAULT
122     self.port = DRB_PORT_DEFAULT
123     self.allow = DRB_ALLOW_DEFAULT
124     
125     # init has been executed
126     self.inited = true
127     
128     # not yet running
129     self.running = false
130   end
131     
132   # Instantiate a new, empty Watch object and pass it to the mandatory
133   # block. The attributes of the watch will be set by the configuration
134   # file.
135   def self.watch
136     self.internal_init
137     
138     w = Watch.new
139     yield(w)
140     
141     # if running, completely remove the watch (if necessary) to
142     # prepare for the reload
143     existing_watch = self.watches[w.name]
144     if self.running && existing_watch
145       self.unwatch(existing_watch)
146     end
147     
148     # ensure the new watch has a unique name
149     if self.watches[w.name] || self.groups[w.name]
150       abort "Watch name '#{w.name}' already used for a Watch or Group"
151     end
152     
153     # ensure watch is internally valid
154     w.valid? || abort("Watch '#{w.name}' is not valid (see above)")
155     
156     # add to list of watches
157     self.watches[w.name] = w
158     
159     # add to pending watches
160     self.pending_watches << w
161     
162     # add to group if specified
163     if w.group
164       # ensure group name hasn't been used for a watch already
165       if self.watches[w.group]
166         abort "Group name '#{w.group}' already used for a Watch"
167       end
168     
169       self.groups[w.group] ||= []
170       self.groups[w.group] << w
171     end
173     # register watch
174     w.register!
175   end
176   
177   def self.unwatch(watch)
178     # unmonitor
179     watch.unmonitor
180     
181     # unregister
182     watch.unregister!
183     
184     # remove from watches
185     self.watches.delete(watch.name)
186     
187     # remove from groups
188     if watch.group
189       self.groups[watch.group].delete(watch)
190     end
191   end
192   
193   def self.contact(kind)
194     self.internal_init
195     
196     # create the condition
197     begin
198       c = Contact.generate(kind)
199     rescue NoSuchContactError => e
200       abort e.message
201     end
202     
203     # send to block so config can set attributes
204     yield(c) if block_given?
205     
206     # call prepare on the contact
207     c.prepare
208     
209     # ensure the new contact has a unique name
210     if self.contacts[c.name] || self.contact_groups[c.name]
211       abort "Contact name '#{c.name}' already used for a Contact or Contact Group"
212     end
213     
214     # abort if the Contact is invalid, the Contact will have printed
215     # out its own error messages by now
216     unless Contact.valid?(c) && c.valid?
217       abort "Exiting on invalid contact"
218     end
219     
220     # add to list of contacts
221     self.contacts[c.name] = c
222     
223     # add to contact group if specified
224     if c.group
225       # ensure group name hasn't been used for a contact already
226       if self.contacts[c.group]
227         abort "Contact Group name '#{c.group}' already used for a Contact"
228       end
229     
230       self.contact_groups[c.group] ||= []
231       self.contact_groups[c.group] << c
232     end
233   end
234     
235   def self.control(name, command)
236     # get the list of watches
237     watches = Array(self.watches[name] || self.groups[name])
238   
239     jobs = []
240     
241     # do the command
242     case command
243       when "start", "monitor"
244         watches.each { |w| jobs << Thread.new { w.monitor } }
245       when "restart"
246         watches.each { |w| jobs << Thread.new { w.move(:restart) } }
247       when "stop"
248         watches.each { |w| jobs << Thread.new { w.unmonitor.action(:stop) } }
249       when "unmonitor"
250         watches.each { |w| jobs << Thread.new { w.unmonitor } }
251       else
252         raise InvalidCommandError.new
253     end
254     
255     jobs.each { |j| j.join }
256     
257     watches
258   end
259   
260   def self.stop_all
261     self.watches.sort.each do |name, w|
262       Thread.new do
263         w.unmonitor if w.state
264         w.action(:stop) if w.alive?
265       end
266     end
267     
268     10.times do
269       return true unless self.watches.map { |name, w| w.alive? }.any?
270       sleep 1
271     end
272     
273     return false
274   end
275   
276   def self.terminate
277     exit!(0)
278   end
279   
280   def self.status
281     info = {}
282     self.watches.map do |name, w|
283       info[name] = {:state => w.state}
284     end
285     info
286   end
287   
288   def self.running_log(watch_name, since)
289     unless self.watches[watch_name]
290       raise NoSuchWatchError.new
291     end
292     
293     LOG.watch_log_since(watch_name, since)
294   end
295   
296   def self.running_load(code)
297     eval(code)
298     self.pending_watches.each { |w| w.monitor if w.autostart? }
299     watches = self.pending_watches.dup
300     self.pending_watches.clear
301     watches
302   end
303   
304   def self.load(glob)
305     Dir[glob].each do |f|
306       Kernel.load f
307     end
308   end
309   
310   def self.setup
311     # Make pid directory
312     unless test(?d, self.pid_file_directory)
313       begin
314         FileUtils.mkdir_p(self.pid_file_directory)
315       rescue Errno::EACCES => e
316         abort "Failed to create pid file directory: #{e.message}"
317       end
318     end
319   end
320     
321   def self.validater
322     unless test(?w, self.pid_file_directory)
323       abort "The pid file directory (#{self.pid_file_directory}) is not writable by #{Etc.getlogin}"
324     end
325   end
326   
327   def self.start
328     self.internal_init
329     self.setup
330     self.validater
331     
332     # instantiate server
333     self.server = Server.new(self.host, self.port, self.allow)
334     
335     # start event handler system
336     EventHandler.start if EventHandler.loaded?
337     
338     # start the timer system
339     Timer.get
341     # start monitoring any watches set to autostart
342     self.watches.values.each { |w| w.monitor if w.autostart? }
343     
344     # clear pending watches
345     self.pending_watches.clear
346     
347     # mark as running
348     self.running = true
349     
350     # join the timer thread so we don't exit
351     Timer.get.join
352   end
353   
354   def self.at_exit
355     self.start
356   end
359 at_exit do
360   God.at_exit