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