├── site ├── CNAME ├── favicon.ico ├── images │ ├── bg.gif │ ├── banner.jpg │ ├── bullet.jpg │ ├── top_bg.gif │ ├── bg_grey.gif │ ├── god_logo.png │ ├── red_dot.gif │ ├── corner_pink.gif │ ├── header_bg.gif │ ├── header_bg.jpg │ ├── corner_green.gif │ └── corner_green.psd ├── install.html ├── javascripts │ └── ruby.js ├── index.template.html └── stylesheets │ ├── layout.css │ └── highlight.css ├── test ├── configs │ ├── task │ │ ├── logs │ │ │ └── .placeholder │ │ └── task.god │ ├── complex │ │ ├── simple_server.rb │ │ └── complex.god │ ├── contact │ │ ├── simple_server.rb │ │ └── contact.god │ ├── stress │ │ ├── simple_server.rb │ │ └── stress.god │ ├── child_events │ │ ├── simple_server.rb │ │ └── child_events.god │ ├── stop_options │ │ ├── simple_server.rb │ │ └── stop_options.god │ ├── daemon_polls │ │ ├── simple_server.rb │ │ └── daemon_polls.god │ ├── keepalive │ │ ├── keepalive.rb │ │ └── keepalive.god │ ├── child_polls │ │ ├── simple_server.rb │ │ └── child_polls.god │ ├── daemon_events │ │ ├── simple_server.rb │ │ ├── simple_server_stop.rb │ │ └── daemon_events.god │ ├── running_load │ │ └── running_load.god │ ├── degrading_lambda │ │ ├── tcp_server.rb │ │ └── degrading_lambda.god │ ├── lifecycle │ │ └── lifecycle.god │ ├── matias │ │ └── matias.god │ ├── real.rb │ └── test.rb ├── suite.rb ├── test_registry.rb ├── test_airbrake.rb ├── test_prowl.rb ├── test_handlers_kqueue_handler.rb ├── test_behavior.rb ├── test_campfire.rb ├── test_hipchat.rb ├── test_system_portable_poller.rb ├── test_statsd.rb ├── test_driver.rb ├── test_webhook.rb ├── test_system_process.rb ├── test_timeline.rb ├── test_socket.rb ├── test_sugar.rb ├── test_jabber.rb ├── test_email.rb ├── test_conditions_process_running.rb ├── test_conditions_disk_usage.rb ├── test_logger.rb ├── test_condition.rb ├── test_trigger.rb ├── test_conditions_tries.rb ├── test_slack.rb ├── test_metric.rb ├── test_event_handler.rb ├── test_contact.rb ├── test_conditions_http_response_code.rb ├── helper.rb └── test_conditions_socket_responding.rb ├── .travis.yml ├── Gemfile ├── ext └── god │ ├── .gitignore │ ├── extconf.rb │ └── kqueue_handler.c ├── .gitignore ├── ideas ├── execve │ ├── go.rb │ ├── extconf.rb │ └── execve.c └── future.god ├── lib └── god │ ├── event_handlers │ ├── dummy_handler.rb │ ├── netlink_handler.rb │ └── kqueue_handler.rb │ ├── cli │ ├── version.rb │ └── run.rb │ ├── errors.rb │ ├── behaviors │ ├── clean_pid_file.rb │ ├── clean_unix_socket.rb │ └── notify_when_flapping.rb │ ├── timeline.rb │ ├── registry.rb │ ├── conditions │ ├── lambda.rb │ ├── file_mtime.rb │ ├── always.rb │ ├── disk_usage.rb │ ├── file_touched.rb │ ├── tries.rb │ ├── degrading_lambda.rb │ ├── process_running.rb │ ├── process_exits.rb │ ├── complex.rb │ ├── cpu_usage.rb │ ├── memory_usage.rb │ ├── flapping.rb │ └── socket_responding.rb │ ├── compat19.rb │ ├── system │ ├── portable_poller.rb │ ├── process.rb │ └── slash_proc_poller.rb │ ├── trigger.rb │ ├── behavior.rb │ ├── sugar.rb │ ├── contacts │ ├── statsd.rb │ ├── airbrake.rb │ ├── scout.rb │ ├── prowl.rb │ ├── twitter.rb │ ├── webhook.rb │ ├── jabber.rb │ ├── slack.rb │ ├── hipchat.rb │ └── campfire.rb │ ├── sys_logger.rb │ ├── simple_logger.rb │ ├── configurable.rb │ ├── metric.rb │ ├── event_handler.rb │ ├── condition.rb │ ├── logger.rb │ ├── socket.rb │ └── contact.rb ├── doc └── intro.asciidoc ├── README.md ├── init ├── god └── lsb_compliant_god ├── LICENSE ├── examples ├── gravatar.god ├── single.god └── events.god ├── bin └── god ├── Announce.txt └── Rakefile /site/CNAME: -------------------------------------------------------------------------------- 1 | godrb.com 2 | -------------------------------------------------------------------------------- /test/configs/task/logs/.placeholder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/god/master/site/favicon.ico -------------------------------------------------------------------------------- /site/images/bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/god/master/site/images/bg.gif -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.8.7 4 | - 1.9.2 5 | - 1.9.3 6 | - ree 7 | -------------------------------------------------------------------------------- /site/images/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/god/master/site/images/banner.jpg -------------------------------------------------------------------------------- /site/images/bullet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/god/master/site/images/bullet.jpg -------------------------------------------------------------------------------- /site/images/top_bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/god/master/site/images/top_bg.gif -------------------------------------------------------------------------------- /site/images/bg_grey.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/god/master/site/images/bg_grey.gif -------------------------------------------------------------------------------- /site/images/god_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/god/master/site/images/god_logo.png -------------------------------------------------------------------------------- /site/images/red_dot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/god/master/site/images/red_dot.gif -------------------------------------------------------------------------------- /site/images/corner_pink.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/god/master/site/images/corner_pink.gif -------------------------------------------------------------------------------- /site/images/header_bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/god/master/site/images/header_bg.gif -------------------------------------------------------------------------------- /site/images/header_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/god/master/site/images/header_bg.jpg -------------------------------------------------------------------------------- /test/configs/complex/simple_server.rb: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | 3 | loop { puts 'server'; sleep 1 } 4 | -------------------------------------------------------------------------------- /test/configs/contact/simple_server.rb: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | 3 | loop { puts 'server'; sleep 1 } 4 | -------------------------------------------------------------------------------- /test/configs/stress/simple_server.rb: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | 3 | loop { puts 'server'; sleep 1 } 4 | -------------------------------------------------------------------------------- /site/images/corner_green.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/god/master/site/images/corner_green.gif -------------------------------------------------------------------------------- /site/images/corner_green.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/god/master/site/images/corner_green.psd -------------------------------------------------------------------------------- /test/configs/child_events/simple_server.rb: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | 3 | loop { puts 'server'; sleep 1 } 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | 4 | gem 'redcarpet', '< 3.0.0' 5 | gem 'sanitize', '2.0.3' 6 | -------------------------------------------------------------------------------- /ext/god/.gitignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | kqueue_handler_ext.bundle 3 | kqueue_handler.o 4 | netlink_handler_ext.bundle 5 | netlink_handler.o 6 | -------------------------------------------------------------------------------- /test/suite.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | tests = Dir["#{File.dirname(__FILE__)}/test_*.rb"] 4 | tests.each do |file| 5 | require file 6 | end 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | rdoc 2 | coverage 3 | pkg 4 | *.log 5 | logs 6 | *.rbc 7 | *~ 8 | .*.sw? 9 | .DS_Store 10 | Gemfile.lock 11 | bbin/ 12 | .bundle 13 | site/index.html 14 | gh-pages 15 | .idea/ 16 | -------------------------------------------------------------------------------- /site/install.html: -------------------------------------------------------------------------------- 1 | ln -s /usr/src/linux-headers-2.6.15-28/include/linux/connector.h /usr/include/linux/connector.h 2 | ln -s /usr/src/linux-headers-2.6.15-28/include/linux/cn_proc.h /usr/include/linux/cn_proc.h 3 | -------------------------------------------------------------------------------- /test/configs/stop_options/simple_server.rb: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | 3 | trap :USR1 do 4 | 5 | end 6 | 7 | loop do 8 | STDOUT.puts('server'); 9 | STDOUT.flush; 10 | 11 | sleep 10 12 | end 13 | -------------------------------------------------------------------------------- /test/configs/daemon_polls/simple_server.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'daemons' 3 | 4 | Daemons.run_proc('daemon-polls', {:dir_mode => :system}) do 5 | loop { STDOUT.puts('server'); STDOUT.flush; sleep 1 } 6 | end 7 | -------------------------------------------------------------------------------- /test/configs/keepalive/keepalive.rb: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | 3 | data = '' 4 | 5 | loop do 6 | STDOUT.puts('server'); 7 | STDOUT.flush; 8 | 9 | 100000.times { data << 'x' } 10 | 11 | # sleep 10 12 | end 13 | -------------------------------------------------------------------------------- /test/configs/child_polls/simple_server.rb: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | 3 | data = '' 4 | 5 | loop do 6 | STDOUT.puts('server'); 7 | STDOUT.flush; 8 | 9 | 100000.times { data << 'x' } 10 | 11 | sleep 10 12 | end 13 | -------------------------------------------------------------------------------- /test/configs/daemon_events/simple_server.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'daemons' 3 | 4 | puts 'simple server ahoy!' 5 | 6 | Daemons.run_proc('daemon-events', {:dir_mode => :system}) do 7 | loop { puts 'server'; sleep 1 } 8 | end 9 | -------------------------------------------------------------------------------- /ideas/execve/go.rb: -------------------------------------------------------------------------------- 1 | require 'execve' 2 | 3 | my_env = ENV.to_hash.merge('HOME' => '/foo') 4 | # my_env = ENV.to_hash 5 | 6 | env = my_env.keys.inject([]) { |acc, k| acc << "#{k}=#{my_env[k]}"; acc } 7 | 8 | execve(%Q{ruby -e "puts ENV['HOME']"}, env) 9 | -------------------------------------------------------------------------------- /test/configs/daemon_events/simple_server_stop.rb: -------------------------------------------------------------------------------- 1 | # exit!(2) 2 | 3 | 3.times do 4 | puts 'waiting' 5 | sleep 1 6 | end 7 | 8 | p ENV 9 | 10 | command = '/usr/local/bin/ruby ' + File.join(File.dirname(__FILE__), *%w[simple_server.rb]) + ' stop' 11 | system(command) 12 | -------------------------------------------------------------------------------- /ideas/execve/extconf.rb: -------------------------------------------------------------------------------- 1 | # Loads mkmf which is used to make makefiles for Ruby extensions 2 | require 'mkmf' 3 | 4 | # Give it a name 5 | extension_name = 'execve' 6 | 7 | # The destination 8 | dir_config(extension_name) 9 | 10 | # Do the work 11 | create_makefile(extension_name) 12 | -------------------------------------------------------------------------------- /lib/god/event_handlers/dummy_handler.rb: -------------------------------------------------------------------------------- 1 | module God 2 | class DummyHandler 3 | EVENT_SYSTEM = "none" 4 | 5 | def self.register_process(pid, events) 6 | raise NotImplementedError 7 | end 8 | 9 | def self.handle_events 10 | raise NotImplementedError 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/configs/keepalive/keepalive.god: -------------------------------------------------------------------------------- 1 | God.watch do |w| 2 | w.name = 'keepalive' 3 | w.start = File.join(GOD_ROOT, *%w[test configs keepalive keepalive.rb]) 4 | w.log = File.join(GOD_ROOT, *%w[test configs keepalive keepalive.log]) 5 | 6 | w.keepalive(:interval => 5.seconds, 7 | :memory_max => 10.megabytes, 8 | :cpu_max => 30.percent) 9 | end 10 | -------------------------------------------------------------------------------- /test/test_registry.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestRegistry < Test::Unit::TestCase 4 | def setup 5 | God.registry.reset 6 | end 7 | 8 | def test_add 9 | foo = God::Process.new 10 | foo.name = 'foo' 11 | God.registry.add(foo) 12 | assert_equal 1, God.registry.size 13 | assert_equal foo, God.registry['foo'] 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/god/event_handlers/netlink_handler.rb: -------------------------------------------------------------------------------- 1 | require 'netlink_handler_ext' 2 | 3 | module God 4 | class NetlinkHandler 5 | EVENT_SYSTEM = "netlink" 6 | 7 | def self.register_process(pid, events) 8 | # netlink doesn't need to do this 9 | # it just reads from the eventhandler actions to see if the pid 10 | # matches the list we're looking for -- Kev 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/configs/stress/stress.god: -------------------------------------------------------------------------------- 1 | ('01'..'08').each do |i| 2 | God.watch do |w| 3 | w.name = "stress-#{i}" 4 | w.start = "ruby " + File.join(File.dirname(__FILE__), *%w[simple_server.rb]) 5 | w.interval = 0 6 | w.grace = 2 7 | w.group = 'test' 8 | 9 | w.start_if do |start| 10 | start.condition(:process_running) do |c| 11 | c.running = false 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/god/event_handlers/kqueue_handler.rb: -------------------------------------------------------------------------------- 1 | require 'kqueue_handler_ext' 2 | 3 | module God 4 | class KQueueHandler 5 | EVENT_SYSTEM = "kqueue" 6 | 7 | def self.register_process(pid, events) 8 | monitor_process(pid, events_mask(events)) 9 | end 10 | 11 | def self.events_mask(events) 12 | events.inject(0) do |mask, event| 13 | mask |= event_mask(event) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/configs/running_load/running_load.god: -------------------------------------------------------------------------------- 1 | God.watch do |w| 2 | w.name = 'running-load' 3 | w.start = '/Users/tom/dev/god/test/configs/child_polls/simple_server.rb' 4 | w.stop = '' 5 | w.interval = 5 6 | w.grace = 2 7 | w.uid = 'tom' 8 | w.gid = 'tom' 9 | w.group = 'test' 10 | 11 | w.start_if do |start| 12 | start.condition(:process_running) do |c| 13 | c.running = false 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/configs/degrading_lambda/tcp_server.rb: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | 3 | require 'socket' 4 | server = TCPServer.new('127.0.0.1', 9090) 5 | while (session = server.accept) 6 | puts "Found a session" 7 | request = session.gets 8 | puts "Request: #{request}" 9 | time = request.to_i 10 | puts "Sleeping for #{time}" 11 | sleep time 12 | session.print "Slept for #{time} seconds" 13 | session.close 14 | puts "Session closed" 15 | end 16 | -------------------------------------------------------------------------------- /test/test_airbrake.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/helper' 3 | 4 | class TestAirbrake < Test::Unit::TestCase 5 | def test_notify 6 | airbrake = God::Contacts::Airbrake.new 7 | airbrake.apikey = "put_your_apikey_here" 8 | airbrake.name = "Airbrake" 9 | 10 | Airbrake.expects(:notify).returns "123" 11 | 12 | airbrake.notify("Test message for airbrake", Time.now, "airbrake priority", "airbrake category", "") 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/god/cli/version.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module CLI 3 | 4 | class Version 5 | def self.version 6 | require 'god' 7 | 8 | # print version 9 | puts "Version #{God.version}" 10 | exit 11 | end 12 | 13 | def self.version_extended 14 | puts "Version: #{God.version}" 15 | puts "Polls: enabled" 16 | puts "Events: " + God::EventHandler.event_system 17 | 18 | exit 19 | end 20 | end 21 | 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/test_prowl.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/helper' 3 | 4 | class TestProwl < Test::Unit::TestCase 5 | def test_live_notify 6 | prowl = God::Contacts::Prowl.new 7 | prowl.name = "Prowly" 8 | prowl.apikey = 'put_your_apikey_here' 9 | 10 | Prowly.expects(:notify).returns(mock(:succeeded? => true)) 11 | 12 | prowl.notify("Test", Time.now, "Test", "Test", "") 13 | assert_equal "sent prowl notification to #{prowl.name}", prowl.info 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/god/errors.rb: -------------------------------------------------------------------------------- 1 | module God 2 | 3 | class AbstractMethodNotOverriddenError < StandardError 4 | end 5 | 6 | class NoSuchWatchError < StandardError 7 | end 8 | 9 | class NoSuchConditionError < StandardError 10 | end 11 | 12 | class NoSuchBehaviorError < StandardError 13 | end 14 | 15 | class NoSuchContactError < StandardError 16 | end 17 | 18 | class InvalidCommandError < StandardError 19 | end 20 | 21 | class EventRegistrationFailedError < StandardError 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /lib/god/behaviors/clean_pid_file.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module Behaviors 3 | 4 | class CleanPidFile < Behavior 5 | def valid? 6 | valid = true 7 | valid &= complain("Attribute 'pid_file' must be specified", self) if self.watch.pid_file.nil? 8 | valid 9 | end 10 | 11 | def before_start 12 | File.delete(self.watch.pid_file) 13 | 14 | "deleted pid file" 15 | rescue 16 | "no pid file to delete" 17 | end 18 | end 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /site/javascripts/ruby.js: -------------------------------------------------------------------------------- 1 | CodeHighlighter.addStyle("ruby",{ 2 | comment : { 3 | exp : /#[^\n]+/ 4 | }, 5 | brackets : { 6 | exp : /\(|\)/ 7 | }, 8 | string : { 9 | exp : /'[^']*'|"[^"]*"/ 10 | }, 11 | keywords : { 12 | exp : /\b(do|end|self|class|def|if|module|yield|then|else|for|until|unless|while|elsif|case|when|break|retry|redo|rescue|require|raise)\b/ 13 | }, 14 | /* Added by Shelly Fisher (shelly@agileevolved.com) */ 15 | symbol : { 16 | exp : /([^:])(:[A-Za-z0-9_!?]+)/ 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /test/test_handlers_kqueue_handler.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | if God::EventHandler.event_system == "kqueue" 4 | 5 | class TestHandlersKqueueHandler < Test::Unit::TestCase 6 | def test_register_process 7 | KQueueHandler.expects(:monitor_process).with(1234, 2147483648) 8 | KQueueHandler.register_process(1234, [:proc_exit]) 9 | end 10 | 11 | def test_events_mask 12 | assert_equal 2147483648, KQueueHandler.events_mask([:proc_exit]) 13 | end 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /lib/god/behaviors/clean_unix_socket.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module Behaviors 3 | 4 | class CleanUnixSocket < Behavior 5 | def valid? 6 | valid = true 7 | valid &= complain("Attribute 'unix_socket' must be specified", self) if self.watch.unix_socket.nil? 8 | valid 9 | end 10 | 11 | def before_start 12 | File.delete(self.watch.unix_socket) 13 | 14 | "deleted unix socket" 15 | rescue 16 | "no unix socket to delete" 17 | end 18 | end 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/god/timeline.rb: -------------------------------------------------------------------------------- 1 | module God 2 | 3 | class Timeline < Array 4 | # Instantiate a new Timeline 5 | # +max_size+ is the maximum size to which the timeline should grow 6 | # 7 | # Returns Timeline 8 | def initialize(max_size) 9 | super() 10 | @max_size = max_size 11 | end 12 | 13 | # Push a value onto the Timeline 14 | # +val+ is the value to push 15 | # 16 | # Returns Timeline 17 | def push(val) 18 | self.concat([val]) 19 | shift if size > @max_size 20 | end 21 | 22 | alias_method :<<, :push 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /lib/god/registry.rb: -------------------------------------------------------------------------------- 1 | module God 2 | def self.registry 3 | @registry ||= Registry.new 4 | end 5 | 6 | class Registry 7 | def initialize 8 | @storage = {} 9 | end 10 | 11 | def add(item) 12 | # raise TypeError unless item.is_a? God::Process 13 | @storage[item.name] = item 14 | end 15 | 16 | def remove(item) 17 | @storage.delete(item.name) 18 | end 19 | 20 | def size 21 | @storage.size 22 | end 23 | 24 | def [](name) 25 | @storage[name] 26 | end 27 | 28 | def reset 29 | @storage.clear 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/test_behavior.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestBehavior < Test::Unit::TestCase 4 | def test_generate_should_return_an_object_corresponding_to_the_given_type 5 | assert_equal Behaviors::FakeBehavior, Behavior.generate(:fake_behavior, nil).class 6 | end 7 | 8 | def test_generate_should_raise_on_invalid_type 9 | assert_raise NoSuchBehaviorError do 10 | Behavior.generate(:foo, nil) 11 | end 12 | end 13 | 14 | def test_complain 15 | SysLogger.expects(:log).with(:error, 'foo') 16 | assert !Behavior.allocate.bypass.complain('foo') 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/god/conditions/lambda.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module Conditions 3 | 4 | class Lambda < PollCondition 5 | attr_accessor :lambda 6 | 7 | def valid? 8 | valid = true 9 | valid &= complain("Attribute 'lambda' must be specified", self) if self.lambda.nil? 10 | valid 11 | end 12 | 13 | def test 14 | if self.lambda.call() 15 | self.info = "lambda condition was satisfied" 16 | true 17 | else 18 | self.info = "lambda condition was not satisfied" 19 | false 20 | end 21 | end 22 | end 23 | 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/configs/daemon_polls/daemon_polls.god: -------------------------------------------------------------------------------- 1 | God.watch do |w| 2 | w.name = "daemon-polls" 3 | w.interval = 5.seconds 4 | w.start = 'ruby ' + File.join(File.dirname(__FILE__), *%w[simple_server.rb]) + ' start' 5 | w.stop = 'ruby ' + File.join(File.dirname(__FILE__), *%w[simple_server.rb]) + ' stop' 6 | w.pid_file = '/var/run/daemon-polls.pid' 7 | w.start_grace = 2.seconds 8 | w.log = File.join(File.dirname(__FILE__), *%w[out.log]) 9 | 10 | w.behavior(:clean_pid_file) 11 | 12 | w.start_if do |start| 13 | start.condition(:process_running) do |c| 14 | c.running = false 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/test_campfire.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestCampfire < Test::Unit::TestCase 4 | def setup 5 | @campfire = God::Contacts::Campfire.new 6 | end 7 | 8 | def test_exists 9 | God::Contacts::Campfire 10 | end 11 | 12 | def test_notify 13 | @campfire.subdomain = 'github' 14 | @campfire.token = 'abc' 15 | @campfire.room = 'danger' 16 | 17 | time = Time.now 18 | body = "[#{time.strftime('%H:%M:%S')}] host - msg" 19 | Marshmallow::Connection.any_instance.expects(:speak).with('danger', body) 20 | @campfire.notify('msg', time, 'prio', 'cat', 'host') 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/configs/lifecycle/lifecycle.god: -------------------------------------------------------------------------------- 1 | God::Contacts::Twitter.settings = { 2 | # this is for my 'mojombo2' twitter test account 3 | # feel free to use it for testing your conditions 4 | :username => 'mojombo@gmail.com', 5 | :password => 'gok9we3ot1av2e' 6 | } 7 | 8 | God.contact(:twitter) do |c| 9 | c.name = 'tom2' 10 | c.group = 'developers' 11 | end 12 | 13 | God.watch do |w| 14 | w.name = "lifecycle" 15 | w.interval = 5.seconds 16 | w.start = "/dev/null" 17 | 18 | # lifecycle 19 | w.lifecycle do |on| 20 | on.condition(:always) do |c| 21 | c.what = true 22 | c.notify = "tom2" 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/configs/task/task.god: -------------------------------------------------------------------------------- 1 | LOG_DIR = File.join(File.dirname(__FILE__), *%w[logs]) 2 | 3 | God.task do |t| 4 | t.name = 'task' 5 | t.valid_states = [:ok, :clean] 6 | t.initial_state = :ok 7 | t.interval = 5 8 | 9 | # t.clean = lambda do 10 | # Dir[File.join(LOG_DIR, '*.log')].each do |f| 11 | # File.delete(f) 12 | # end 13 | # end 14 | 15 | t.clean = "rm #{File.join(LOG_DIR, '*.log')}" 16 | 17 | t.transition(:clean, :ok) 18 | 19 | t.transition(:ok, :clean) do |on| 20 | on.condition(:lambda) do |c| 21 | c.lambda = lambda do 22 | Dir[File.join(LOG_DIR, '*.log')].size > 1 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/test_hipchat.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestHipchat < Test::Unit::TestCase 4 | def setup 5 | @hipchat = God::Contacts::Hipchat.new 6 | end 7 | 8 | def test_exists 9 | God::Contacts::Hipchat 10 | end 11 | 12 | def test_notify 13 | @hipchat.token = 'ee64d6e2337310af' 14 | @hipchat.ssl = 'true' 15 | @hipchat.room = 'testroom' 16 | @hipchat.from = 'test' 17 | 18 | time = Time.now 19 | body = "[#{time.strftime('%H:%M:%S')}] host - msg" 20 | Marshmallow::Connection.any_instance.expects(:speak).with('testroom', body) 21 | @hipchat.notify('msg', time, 'prio', 'cat', 'host') 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/test_system_portable_poller.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestSystemPortablePoller < Test::Unit::TestCase 4 | def setup 5 | pid = Process.pid 6 | @process = System::PortablePoller.new(pid) 7 | end 8 | 9 | def test_time_string_to_seconds 10 | assert_equal 0, @process.bypass.time_string_to_seconds('0:00:00') 11 | assert_equal 0, @process.bypass.time_string_to_seconds('0:00:55') 12 | assert_equal 27, @process.bypass.time_string_to_seconds('0:27:32') 13 | assert_equal 75, @process.bypass.time_string_to_seconds('1:15:13') 14 | assert_equal 735, @process.bypass.time_string_to_seconds('12:15:13') 15 | end 16 | end 17 | 18 | -------------------------------------------------------------------------------- /lib/god/conditions/file_mtime.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module Conditions 3 | 4 | class FileMtime < PollCondition 5 | attr_accessor :path, :max_age 6 | 7 | def initialize 8 | super 9 | self.path = nil 10 | self.max_age = nil 11 | end 12 | 13 | def valid? 14 | valid = true 15 | valid &= complain("Attribute 'path' must be specified", self) if self.path.nil? 16 | valid &= complain("Attribute 'max_age' must be specified", self) if self.max_age.nil? 17 | valid 18 | end 19 | 20 | def test 21 | (Time.now - File.mtime(self.path)) > self.max_age 22 | end 23 | end 24 | 25 | end 26 | end 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/test_statsd.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestStatsd < Test::Unit::TestCase 4 | def setup 5 | @statsd = God::Contacts::Statsd.new 6 | end 7 | 8 | def test_exists 9 | God::Contacts::Statsd 10 | end 11 | 12 | def test_notify 13 | [ 14 | 'cpu out of bounds', 15 | 'memory out of bounds', 16 | 'process is flapping' 17 | ].each do |event_type| 18 | ::Statsd.any_instance.expects(:increment).with("god.#{event_type.gsub(/\s/, '_')}.127_0_0_1.myapp-thin-1234") 19 | @statsd.notify("myapp-thin-1234 [trigger] #{event_type}", Time.now, 'some priority', 'and some category', '127.0.0.1') 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/test_driver.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestDriver < Test::Unit::TestCase 4 | def setup 5 | 6 | end 7 | 8 | def test_push_pop_wait 9 | eq = God::DriverEventQueue.new 10 | 11 | MonitorMixin::ConditionVariable.any_instance.expects(:wait).times(1) 12 | 13 | eq.push(God::TimedEvent.new(0)) 14 | eq.push(God::TimedEvent.new(0.1)) 15 | t = Thread.new do 16 | # This pop will see an event immediately available, so no wait. 17 | assert_equal TimedEvent, eq.pop.class 18 | 19 | # This pop will happen before the next event is due, so wait. 20 | assert_equal TimedEvent, eq.pop.class 21 | end 22 | 23 | t.join 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /doc/intro.asciidoc: -------------------------------------------------------------------------------- 1 | God: The Ruby Framework for Process Management 2 | ============================================== 3 | Tom Preston-Werner 4 | 5 | God is an easy to configure, easy to extend monitoring framework written in 6 | Ruby. 7 | 8 | Keeping your server processes and tasks running should be a simple part of 9 | your deployment process. God aims to be the simplest, most powerful monitoring 10 | application available. 11 | 12 | Features 13 | -------- 14 | 15 | * Config file is written in Ruby 16 | * Easily write your own custom conditions in Ruby 17 | * Supports both poll and event based conditions 18 | * Different poll conditions can have different intervals 19 | * Integrated notification system (write your own too!) 20 | * Easily control non-daemonizing scripts 21 | -------------------------------------------------------------------------------- /test/test_webhook.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestWebhook < Test::Unit::TestCase 4 | def setup 5 | @webhook = God::Contacts::Webhook.new 6 | end 7 | 8 | def test_notify 9 | @webhook.url = 'http://example.com/switch' 10 | Net::HTTP.any_instance.expects(:request).returns(Net::HTTPSuccess.new('a', 'b', 'c')) 11 | 12 | @webhook.notify('msg', Time.now, 'prio', 'cat', 'host') 13 | assert_equal "sent webhook to http://example.com/switch", @webhook.info 14 | end 15 | 16 | def test_notify_with_url_containing_query_parameters 17 | @webhook.url = 'http://example.com/switch?api_key=123' 18 | Net::HTTP::Post.expects(:new).with('/switch?api_key=123') 19 | 20 | @webhook.notify('msg', Time.now, 'prio', 'cat', 'host') 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/test_system_process.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestSystemProcess < Test::Unit::TestCase 4 | def setup 5 | pid = Process.pid 6 | @process = System::Process.new(pid) 7 | end 8 | 9 | def test_exists_should_return_true_for_running_process 10 | assert_equal true, @process.exists? 11 | end 12 | 13 | def test_exists_should_return_false_for_non_existant_process 14 | assert_equal false, System::Process.new(9999999).exists? 15 | end 16 | 17 | def test_memory 18 | assert_kind_of Integer, @process.memory 19 | assert @process.memory > 0 20 | end 21 | 22 | def test_percent_memory 23 | assert_kind_of Float, @process.percent_memory 24 | end 25 | 26 | def test_percent_cpu 27 | assert_kind_of Float, @process.percent_cpu 28 | end 29 | end 30 | 31 | -------------------------------------------------------------------------------- /ideas/execve/execve.c: -------------------------------------------------------------------------------- 1 | #include "ruby.h" 2 | 3 | static VALUE mKernel; 4 | void Init_execve(); 5 | VALUE method_execve(VALUE self, VALUE cmd, VALUE env); 6 | 7 | void Init_execve() { 8 | mKernel = rb_const_get(rb_cObject, rb_intern("Kernel")); 9 | rb_define_method(mKernel, "execve", method_execve, 2); 10 | } 11 | 12 | VALUE method_execve(VALUE self, VALUE r_cmd, VALUE r_env) { 13 | char *shell = (char *)dln_find_exe("sh", 0); 14 | char *arg[] = { "sh", "-c", StringValuePtr(r_cmd), (char *)0 }; 15 | 16 | struct RArray *env_array; 17 | env_array = RARRAY(r_env); 18 | char *env[env_array->len + 1]; 19 | 20 | int i; 21 | for(i = 0; i < env_array->len; i++) { 22 | env[i] = StringValuePtr(env_array->ptr[i]); 23 | } 24 | 25 | env[env_array->len] = (char *)0; 26 | 27 | execve(shell, arg, env); 28 | return Qnil; 29 | } 30 | -------------------------------------------------------------------------------- /test/configs/degrading_lambda/degrading_lambda.god: -------------------------------------------------------------------------------- 1 | God.watch do |w| 2 | w.name = 'degrading-lambda' 3 | w.start = 'ruby ' + File.join(File.dirname(__FILE__), *%w[tcp_server.rb]) 4 | w.interval = 5 5 | w.grace = 2 6 | w.group = 'test' 7 | 8 | w.start_if do |start| 9 | start.condition(:process_running) do |c| 10 | c.running = false 11 | end 12 | end 13 | 14 | w.restart_if do |restart| 15 | restart.condition(:degrading_lambda) do |c| 16 | require 'socket' 17 | c.lambda = lambda { 18 | begin 19 | sock = TCPSocket.open('127.0.0.1', 9090) 20 | sock.send "2\n", 0 21 | retval = sock.gets 22 | puts "Retval is #{retval}" 23 | sock.close 24 | retval 25 | rescue 26 | false 27 | end 28 | } 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | God: The Ruby Framework for Process Management 2 | ============================================== 3 | 4 | * Authors: Tom Preston-Werner, Kevin Clark, Eric Lindvall 5 | * Website: http://godrb.com 6 | 7 | Description 8 | ----------- 9 | 10 | God is an easy to configure, easy to extend monitoring framework written in 11 | Ruby. 12 | 13 | Keeping your server processes and tasks running should be a simple part of 14 | your deployment process. God aims to be the simplest, most powerful monitoring 15 | application available. 16 | 17 | Documentation 18 | ------------- 19 | 20 | See in-repo documentation at `REPO_ROOT/doc`. 21 | See online documentation at http://godrb.com. 22 | 23 | Community 24 | --------- 25 | 26 | Sign up for the god mailing list at http://groups.google.com/group/god-rb 27 | 28 | License 29 | ------- 30 | 31 | See LICENSE file. 32 | -------------------------------------------------------------------------------- /test/test_timeline.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestTimeline < Test::Unit::TestCase 4 | def setup 5 | @timeline = Timeline.new(5) 6 | end 7 | 8 | def test_new_should_be_empty 9 | assert_equal 0, @timeline.size 10 | end 11 | 12 | def test_should_not_grow_to_more_than_size 13 | (1..10).each do |i| 14 | @timeline.push(i) 15 | end 16 | 17 | assert_equal [6, 7, 8, 9, 10], @timeline 18 | end 19 | 20 | def test_clear_should_clear_array 21 | @timeline << 1 22 | assert_equal [1], @timeline 23 | assert_equal [], @timeline.clear 24 | end 25 | 26 | # def test_benchmark 27 | # require 'benchmark' 28 | # 29 | # count = 1_000_000 30 | # 31 | # t = Timeline.new(10) 32 | # 33 | # Benchmark.bmbm do |x| 34 | # x.report("go") { count.times { t.push(5) } } 35 | # end 36 | # end 37 | end 38 | -------------------------------------------------------------------------------- /lib/god/conditions/always.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module Conditions 3 | # Always trigger or never trigger. 4 | # 5 | # Examples 6 | # 7 | # # Always trigger. 8 | # on.condition(:always) do |c| 9 | # c.what = true 10 | # end 11 | # 12 | # # Never trigger. 13 | # on.condition(:always) do |c| 14 | # c.what = false 15 | # end 16 | class Always < PollCondition 17 | # The Boolean determining whether this condition will always trigger 18 | # (true) or never trigger (false). 19 | attr_accessor :what 20 | 21 | def initialize 22 | self.info = "always" 23 | end 24 | 25 | def valid? 26 | valid = true 27 | valid &= complain("Attribute 'what' must be specified", self) if self.what.nil? 28 | valid 29 | end 30 | 31 | def test 32 | @what 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/god/conditions/disk_usage.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module Conditions 3 | 4 | class DiskUsage < PollCondition 5 | attr_accessor :above, :mount_point 6 | 7 | def initialize 8 | super 9 | self.above = nil 10 | self.mount_point = nil 11 | end 12 | 13 | def valid? 14 | valid = true 15 | valid &= complain("Attribute 'mount_point' must be specified", self) if self.mount_point.nil? 16 | valid &= complain("Attribute 'above' must be specified", self) if self.above.nil? 17 | valid 18 | end 19 | 20 | def test 21 | self.info = [] 22 | usage = `df -P | grep -i " #{self.mount_point}$" | awk '{print $5}' | sed 's/%//'` 23 | if usage.to_i > self.above 24 | self.info = "disk space out of bounds" 25 | return true 26 | else 27 | return false 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/test_socket.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestSocket < Test::Unit::TestCase 4 | def setup 5 | silence_warnings do 6 | Object.const_set(:DRb, stub_everything) 7 | end 8 | end 9 | 10 | def test_should_start_a_drb_server 11 | DRb.expects(:start_service) 12 | God::Socket.new 13 | end 14 | 15 | def test_should_use_supplied_port_and_host 16 | DRb.expects(:start_service).with { |uri, object| uri == "drbunix:///tmp/god.9999.sock" && object.is_a?(God::Socket) } 17 | server = God::Socket.new(9999) 18 | end 19 | 20 | def test_should_forward_foreign_method_calls_to_god 21 | server = nil 22 | server = God::Socket.new 23 | God.expects(:send).with(:something_random) 24 | server.something_random 25 | end 26 | 27 | # ping 28 | 29 | def test_ping_should_return_true 30 | server = nil 31 | server = God::Socket.new 32 | assert server.ping 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/god/compat19.rb: -------------------------------------------------------------------------------- 1 | require 'monitor' 2 | 3 | # Taken from http://redmine.ruby-lang.org/repositories/entry/ruby-19/lib/monitor.rb 4 | 5 | module MonitorMixin 6 | class ConditionVariable 7 | def wait(timeout = nil) 8 | @monitor.__send__(:mon_check_owner) 9 | count = @monitor.__send__(:mon_exit_for_cond) 10 | begin 11 | @cond.wait(@monitor.instance_variable_get("@mon_mutex"), timeout) 12 | return true 13 | ensure 14 | @monitor.__send__(:mon_enter_for_cond, count) 15 | end 16 | end 17 | end 18 | end 19 | 20 | # Taken from http://redmine.ruby-lang.org/repositories/entry/ruby-19/lib/thread.rb 21 | 22 | class ConditionVariable 23 | def wait(mutex, timeout=nil) 24 | begin 25 | # TODO: mutex should not be used 26 | @waiters_mutex.synchronize do 27 | @waiters.push(Thread.current) 28 | end 29 | mutex.sleep timeout 30 | end 31 | self 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /init/god: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # god Startup script for god (http://god.rubyforge.org) 4 | # 5 | # chkconfig: - 85 15 6 | # description: God is an easy to configure, easy to extend monitoring \ 7 | # framework written in Ruby. 8 | # 9 | 10 | CONF_DIR=/etc/god 11 | 12 | RETVAL=0 13 | 14 | # Go no further if config directory is missing. 15 | [ -d "$CONF_DIR" ] || exit 0 16 | 17 | case "$1" in 18 | start) 19 | # Create pid directory 20 | ruby /usr/bin/god -c $CONF_DIR/master.conf 21 | RETVAL=$? 22 | ;; 23 | stop) 24 | ruby /usr/bin/god terminate 25 | RETVAL=$? 26 | ;; 27 | restart) 28 | ruby /usr/bin/god terminate 29 | ruby /usr/bin/god -c $CONF_DIR/master.conf 30 | RETVAL=$? 31 | ;; 32 | status) 33 | ruby /usr/bin/god status 34 | RETVAL=$? 35 | ;; 36 | *) 37 | echo "Usage: god {start|stop|restart|status}" 38 | exit 1 39 | ;; 40 | esac 41 | 42 | exit $RETVAL 43 | -------------------------------------------------------------------------------- /test/test_sugar.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestSugar < Test::Unit::TestCase 4 | def test_seconds 5 | assert_equal 1, 1.seconds 6 | assert_equal 1, 1.second 7 | end 8 | 9 | def test_minutes 10 | assert_equal 60, 1.minutes 11 | assert_equal 60, 1.minute 12 | end 13 | 14 | def test_hours 15 | assert_equal 3600, 1.hours 16 | assert_equal 3600, 1.hour 17 | end 18 | 19 | def test_days 20 | assert_equal 86400, 1.days 21 | assert_equal 86400, 1.day 22 | end 23 | 24 | def test_kilobytes 25 | assert_equal 1, 1.kilobytes 26 | assert_equal 1, 1.kilobyte 27 | end 28 | 29 | def test_megabytes 30 | assert_equal 1024, 1.megabytes 31 | assert_equal 1024, 1.megabyte 32 | end 33 | 34 | def test_gigabytes 35 | assert_equal 1024 ** 2, 1.gigabytes 36 | assert_equal 1024 ** 2, 1.gigabyte 37 | end 38 | 39 | def test_percent 40 | assert_equal 1, 1.percent 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/configs/child_polls/child_polls.god: -------------------------------------------------------------------------------- 1 | God.watch do |w| 2 | w.name = 'child-polls' 3 | w.start = File.join(GOD_ROOT, *%w[test configs child_polls simple_server.rb]) 4 | w.interval = 5 5 | w.grace = 2 6 | 7 | w.start_if do |start| 8 | start.condition(:process_running) do |c| 9 | c.running = false 10 | end 11 | end 12 | 13 | w.restart_if do |restart| 14 | restart.condition(:cpu_usage) do |c| 15 | c.above = 30.percent 16 | c.times = [3, 5] 17 | end 18 | 19 | restart.condition(:memory_usage) do |c| 20 | c.above = 10.megabytes 21 | c.times = [3, 5] 22 | end 23 | end 24 | 25 | # lifecycle 26 | w.lifecycle do |on| 27 | on.condition(:flapping) do |c| 28 | c.to_state = [:start, :restart] 29 | c.times = 3 30 | c.within = 60.seconds 31 | c.transition = :unmonitored 32 | c.retry_in = 10.seconds 33 | c.retry_times = 2 34 | c.retry_within = 5.minutes 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/test_jabber.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/helper' 3 | 4 | class TestJabber < Test::Unit::TestCase 5 | 6 | def setup 7 | @jabber = God::Contacts::Jabber.new 8 | end 9 | 10 | def test_notify 11 | @jabber.host = 'talk.google.com' 12 | @jabber.from_jid = 'god@jabber.org' 13 | @jabber.password = 'secret' 14 | @jabber.to_jid = 'dev@jabber.org' 15 | 16 | time = Time.now 17 | body = God::Contacts::Jabber.format.call('msg', time, 'prio', 'cat', 'host') 18 | 19 | assert_equal "Message: msg\nHost: host\nPriority: prio\nCategory: cat\n", body 20 | 21 | Jabber::Client.any_instance.expects(:connect).with('talk.google.com', 5222) 22 | Jabber::Client.any_instance.expects(:auth).with('secret') 23 | Jabber::Client.any_instance.expects(:send) 24 | Jabber::Client.any_instance.expects(:close) 25 | 26 | @jabber.notify('msg', Time.now, 'prio', 'cat', 'host') 27 | assert_equal "sent jabber message to dev@jabber.org", @jabber.info 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/configs/stop_options/stop_options.god: -------------------------------------------------------------------------------- 1 | God.watch do |w| 2 | w.name = 'stop-options' 3 | w.start = File.join(GOD_ROOT, *%w[test configs stop_options simple_server.rb]) 4 | w.stop_signal = 'USR1' 5 | w.stop_timeout = 5 6 | w.interval = 5 7 | w.grace = 2 8 | 9 | w.start_if do |start| 10 | start.condition(:process_running) do |c| 11 | c.running = false 12 | end 13 | end 14 | 15 | w.restart_if do |restart| 16 | restart.condition(:cpu_usage) do |c| 17 | c.above = 30.percent 18 | c.times = [3, 5] 19 | end 20 | 21 | restart.condition(:memory_usage) do |c| 22 | c.above = 10.megabytes 23 | c.times = [3, 5] 24 | end 25 | end 26 | 27 | # lifecycle 28 | w.lifecycle do |on| 29 | on.condition(:flapping) do |c| 30 | c.to_state = [:start, :restart] 31 | c.times = 3 32 | c.within = 60.seconds 33 | c.transition = :unmonitored 34 | c.retry_in = 10.seconds 35 | c.retry_times = 2 36 | c.retry_within = 5.minutes 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/god/system/portable_poller.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module System 3 | class PortablePoller 4 | def initialize(pid) 5 | @pid = pid 6 | end 7 | # Memory usage in kilobytes (resident set size) 8 | def memory 9 | ps_int('rss') 10 | end 11 | 12 | # Percentage memory usage 13 | def percent_memory 14 | ps_float('%mem') 15 | end 16 | 17 | # Percentage CPU usage 18 | def percent_cpu 19 | ps_float('%cpu') 20 | end 21 | 22 | private 23 | 24 | def ps_int(keyword) 25 | `ps -o #{keyword}= -p #{@pid}`.to_i 26 | end 27 | 28 | def ps_float(keyword) 29 | `ps -o #{keyword}= -p #{@pid}`.to_f 30 | end 31 | 32 | def ps_string(keyword) 33 | `ps -o #{keyword}= -p #{@pid}`.strip 34 | end 35 | 36 | def time_string_to_seconds(text) 37 | _, minutes, seconds, useconds = *text.match(/(\d+):(\d{2}).(\d{2})/) 38 | (minutes.to_i * 60) + seconds.to_i 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/god/trigger.rb: -------------------------------------------------------------------------------- 1 | module God 2 | 3 | class Trigger 4 | 5 | class << self 6 | attr_accessor :triggers # {task.name => condition} 7 | end 8 | 9 | # init 10 | self.triggers = {} 11 | @mutex = Mutex.new 12 | 13 | def self.register(condition) 14 | @mutex.synchronize do 15 | self.triggers[condition.watch.name] ||= [] 16 | self.triggers[condition.watch.name] << condition 17 | end 18 | end 19 | 20 | def self.deregister(condition) 21 | @mutex.synchronize do 22 | self.triggers[condition.watch.name].delete(condition) 23 | self.triggers.delete(condition.watch.name) if self.triggers[condition.watch.name].empty? 24 | end 25 | end 26 | 27 | def self.broadcast(task, message, payload) 28 | return unless self.triggers[task.name] 29 | 30 | @mutex.synchronize do 31 | self.triggers[task.name].each do |t| 32 | t.process(message, payload) 33 | end 34 | end 35 | end 36 | 37 | def self.reset 38 | self.triggers.clear 39 | end 40 | 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /lib/god/conditions/file_touched.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module Conditions 3 | 4 | # Condition Symbol :file_touched 5 | # Type: Poll 6 | # 7 | # Trigger when a specified file is touched. 8 | # 9 | # Paramaters 10 | # Required 11 | # +path+ is the path to the file to watch. 12 | # 13 | # Examples 14 | # 15 | # Trigger if 'tmp/restart.txt' file is touched (from a Watch): 16 | # 17 | # on.condition(:file_touched) do |c| 18 | # c.path = 'tmp/restart.txt' 19 | # end 20 | # 21 | class FileTouched < PollCondition 22 | attr_accessor :path 23 | 24 | def initialize 25 | super 26 | self.path = nil 27 | end 28 | 29 | def valid? 30 | valid = true 31 | valid &= complain("Attribute 'path' must be specified", self) if self.path.nil? 32 | valid 33 | end 34 | 35 | def test 36 | if File.exists?(self.path) 37 | (Time.now - File.mtime(self.path)) <= self.interval 38 | else 39 | false 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2007 Tom Preston-Werner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/configs/daemon_events/daemon_events.god: -------------------------------------------------------------------------------- 1 | God.watch do |w| 2 | w.name = "daemon-events" 3 | w.interval = 5.seconds 4 | w.start = 'ruby ' + File.join(File.dirname(__FILE__), *%w[simple_server.rb]) + ' start' 5 | w.stop = 'ruby ' + File.join(File.dirname(__FILE__), *%w[simple_server_stop.rb]) 6 | w.pid_file = '/var/run/daemon-events.pid' 7 | w.log = File.join(File.dirname(__FILE__), 'daemon_events.log') 8 | w.uid = 'tom' 9 | w.gid = 'tom' 10 | 11 | w.behavior(:clean_pid_file) 12 | 13 | # determine the state on startup 14 | w.transition(:init, { true => :up, false => :start }) do |on| 15 | on.condition(:process_running) do |c| 16 | c.running = true 17 | end 18 | end 19 | 20 | # determine when process has finished starting 21 | w.transition([:start, :restart], :up) do |on| 22 | on.condition(:process_running) do |c| 23 | c.running = true 24 | end 25 | 26 | # failsafe 27 | on.condition(:tries) do |c| 28 | c.times = 2 29 | c.transition = :start 30 | end 31 | end 32 | 33 | # start if process is not running 34 | w.transition(:up, :start) do |on| 35 | on.condition(:process_exits) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/god/system/process.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module System 3 | 4 | class Process 5 | def self.fetch_system_poller 6 | @@poller ||= if SlashProcPoller.usable? 7 | SlashProcPoller 8 | else 9 | PortablePoller 10 | end 11 | end 12 | 13 | def initialize(pid) 14 | @pid = pid.to_i 15 | @poller = self.class.fetch_system_poller.new(@pid) 16 | end 17 | 18 | # Return true if this process is running, false otherwise 19 | def exists? 20 | !!::Process.kill(0, @pid) rescue false 21 | end 22 | 23 | # Memory usage in kilobytes (resident set size) 24 | def memory 25 | @poller.memory 26 | end 27 | 28 | # Percentage memory usage 29 | def percent_memory 30 | @poller.percent_memory 31 | end 32 | 33 | # Percentage CPU usage 34 | def percent_cpu 35 | @poller.percent_cpu 36 | end 37 | 38 | private 39 | 40 | def fetch_system_poller 41 | if SlashProcPoller.usable? 42 | SlashProcPoller 43 | else 44 | PortablePoller 45 | end 46 | end 47 | end 48 | 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/god/conditions/tries.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module Conditions 3 | 4 | class Tries < PollCondition 5 | attr_accessor :times, :within 6 | 7 | def prepare 8 | @timeline = Timeline.new(self.times) 9 | end 10 | 11 | def reset 12 | @timeline.clear 13 | end 14 | 15 | def valid? 16 | valid = true 17 | valid &= complain("Attribute 'times' must be specified", self) if self.times.nil? 18 | valid 19 | end 20 | 21 | def test 22 | @timeline << Time.now 23 | 24 | concensus = (@timeline.size == self.times) 25 | duration = self.within.nil? || (@timeline.last - @timeline.first) < self.within 26 | 27 | if within 28 | history = "[#{@timeline.size}/#{self.times} within #{(@timeline.last - @timeline.first).to_i}s]" 29 | else 30 | history = "[#{@timeline.size}/#{self.times}]" 31 | end 32 | 33 | if concensus && duration 34 | self.info = "tries exceeded #{history}" 35 | return true 36 | else 37 | self.info = "tries within bounds #{history}" 38 | return false 39 | end 40 | end 41 | end 42 | 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/test_email.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestEmail < Test::Unit::TestCase 4 | def setup 5 | God::Contacts::Email.to_email = 'dev@example.com' 6 | God::Contacts::Email.from_email = 'god@example.com' 7 | @email = God::Contacts::Email.new 8 | end 9 | 10 | def test_validity_delivery 11 | @email.delivery_method = :brainwaves 12 | assert_equal false, @email.valid? 13 | end 14 | 15 | def test_smtp_delivery_method_for_notify 16 | @email.delivery_method = :smtp 17 | 18 | God::Contacts::Email.any_instance.expects(:notify_sendmail).never 19 | God::Contacts::Email.any_instance.expects(:notify_smtp).once.returns(nil) 20 | 21 | @email.notify('msg', Time.now, 'prio', 'cat', 'host') 22 | assert_equal "sent email to dev@example.com via smtp", @email.info 23 | end 24 | 25 | def test_sendmail_delivery_method_for_notify 26 | @email.delivery_method = :sendmail 27 | 28 | God::Contacts::Email.any_instance.expects(:notify_smtp).never 29 | God::Contacts::Email.any_instance.expects(:notify_sendmail).once.returns(nil) 30 | 31 | @email.notify('msg', Time.now, 'prio', 'cat', 'host') 32 | assert_equal "sent email to dev@example.com via sendmail", @email.info 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/test_conditions_process_running.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestConditionsProcessRunning < Test::Unit::TestCase 4 | def test_missing_pid_file_returns_opposite 5 | [true, false].each do |r| 6 | c = Conditions::ProcessRunning.new 7 | c.running = r 8 | c.stubs(:watch).returns(stub(:pid => 99999999, :name => 'foo')) 9 | assert_equal !r, c.test 10 | end 11 | end 12 | 13 | def test_not_running_returns_opposite 14 | [true, false].each do |r| 15 | c = Conditions::ProcessRunning.new 16 | c.running = r 17 | 18 | File.stubs(:exist?).returns(true) 19 | c.stubs(:watch).returns(stub(:pid => 123)) 20 | File.stubs(:read).returns('5') 21 | System::Process.any_instance.stubs(:exists?).returns(false) 22 | 23 | assert_equal !r, c.test 24 | end 25 | end 26 | 27 | def test_running_returns_same 28 | [true, false].each do |r| 29 | c = Conditions::ProcessRunning.new 30 | c.running = r 31 | 32 | File.stubs(:exist?).returns(true) 33 | c.stubs(:watch).returns(stub(:pid => 123)) 34 | File.stubs(:read).returns('5') 35 | System::Process.any_instance.stubs(:exists?).returns(true) 36 | 37 | assert_equal r, c.test 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/god/behavior.rb: -------------------------------------------------------------------------------- 1 | module God 2 | 3 | class Behavior 4 | include Configurable 5 | 6 | attr_accessor :watch 7 | 8 | # Generate a Behavior of the given kind. The proper class is found by camel casing the 9 | # kind (which is given as an underscored symbol). 10 | # +kind+ is the underscored symbol representing the class (e.g. foo_bar for God::Behaviors::FooBar) 11 | def self.generate(kind, watch) 12 | sym = kind.to_s.capitalize.gsub(/_(.)/){$1.upcase}.intern 13 | b = God::Behaviors.const_get(sym).new 14 | b.watch = watch 15 | b 16 | rescue NameError 17 | raise NoSuchBehaviorError.new("No Behavior found with the class name God::Behaviors::#{sym}") 18 | end 19 | 20 | def valid? 21 | true 22 | end 23 | 24 | ####### 25 | 26 | def before_start 27 | end 28 | 29 | def after_start 30 | end 31 | 32 | def before_restart 33 | end 34 | 35 | def after_restart 36 | end 37 | 38 | def before_stop 39 | end 40 | 41 | def after_stop 42 | end 43 | 44 | # Construct the friendly name of this Behavior, looks like: 45 | # 46 | # Behavior FooBar on Watch 'baz' 47 | def friendly_name 48 | "Behavior " + super + " on Watch '#{self.watch.name}'" 49 | end 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /test/configs/child_events/child_events.god: -------------------------------------------------------------------------------- 1 | God.watch do |w| 2 | w.name = "child-events" 3 | w.interval = 5.seconds 4 | w.start = File.join(GOD_ROOT, *%w[test configs child_events simple_server.rb]) 5 | # w.log = File.join(GOD_ROOT, *%w[test configs child_events god.log]) 6 | 7 | # determine the state on startup 8 | w.transition(:init, { true => :up, false => :start }) do |on| 9 | on.condition(:process_running) do |c| 10 | c.running = true 11 | end 12 | end 13 | 14 | # determine when process has finished starting 15 | w.transition([:start, :restart], :up) do |on| 16 | on.condition(:process_running) do |c| 17 | c.running = true 18 | end 19 | 20 | # failsafe 21 | on.condition(:tries) do |c| 22 | c.times = 2 23 | c.transition = :start 24 | end 25 | end 26 | 27 | # start if process is not running 28 | w.transition(:up, :start) do |on| 29 | on.condition(:process_exits) 30 | end 31 | 32 | # lifecycle 33 | w.lifecycle do |on| 34 | on.condition(:flapping) do |c| 35 | c.to_state = [:start, :restart] 36 | c.times = 5 37 | c.within = 20.seconds 38 | c.transition = :unmonitored 39 | c.retry_in = 10.seconds 40 | c.retry_times = 2 41 | c.retry_within = 5.minutes 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/configs/matias/matias.god: -------------------------------------------------------------------------------- 1 | $pid_file = "/tmp/matias.pid" 2 | 3 | God.task do |w| 4 | w.name = "watcher" 5 | w.interval = 5.seconds 6 | w.valid_states = [:init, :up, :down] 7 | w.initial_state = :init 8 | 9 | # determine the state on startup 10 | w.transition(:init, { true => :up, false => :down }) do |on| 11 | on.condition(:process_running) do |c| 12 | c.running = true 13 | c.pid_file = $pid_file 14 | end 15 | end 16 | 17 | # when process is up 18 | w.transition(:up, :down) do |on| 19 | # transition to 'start' if process goes down 20 | on.condition(:process_running) do |c| 21 | c.running = false 22 | c.pid_file = $pid_file 23 | end 24 | 25 | # send up info 26 | on.condition(:lambda) do |c| 27 | c.lambda = lambda do 28 | puts 'yay I am up' 29 | false 30 | end 31 | end 32 | end 33 | 34 | # when process is down 35 | w.transition(:down, :up) do |on| 36 | # transition to 'up' if process comes up 37 | on.condition(:process_running) do |c| 38 | c.running = true 39 | c.pid_file = $pid_file 40 | end 41 | 42 | # send down info 43 | on.condition(:lambda) do |c| 44 | c.lambda = lambda do 45 | puts 'boo I am down' 46 | false 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/god/sugar.rb: -------------------------------------------------------------------------------- 1 | class Numeric 2 | # Public: Units of seconds. 3 | def seconds 4 | self 5 | end 6 | 7 | # Public: Units of seconds. 8 | alias :second :seconds 9 | 10 | # Public: Units of minutes (60 seconds). 11 | def minutes 12 | self * 60 13 | end 14 | 15 | # Public: Units of minutes (60 seconds). 16 | alias :minute :minutes 17 | 18 | # Public: Units of hours (3600 seconds). 19 | def hours 20 | self * 3600 21 | end 22 | 23 | # Public: Units of hours (3600 seconds). 24 | alias :hour :hours 25 | 26 | # Public: Units of days (86400 seconds). 27 | def days 28 | self * 86400 29 | end 30 | 31 | # Public: Units of days (86400 seconds). 32 | alias :day :days 33 | 34 | # Units of kilobytes. 35 | def kilobytes 36 | self 37 | end 38 | 39 | # Units of kilobytes. 40 | alias :kilobyte :kilobytes 41 | 42 | # Units of megabytes (1024 kilobytes). 43 | def megabytes 44 | self * 1024 45 | end 46 | 47 | # Units of megabytes (1024 kilobytes). 48 | alias :megabyte :megabytes 49 | 50 | # Units of gigabytes (1,048,576 kilobytes). 51 | def gigabytes 52 | self * (1024 ** 2) 53 | end 54 | 55 | # Units of gigabytes (1,048,576 kilobytes). 56 | alias :gigabyte :gigabytes 57 | 58 | # Units of percent. e.g. 50.percent. 59 | def percent 60 | self 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/test_conditions_disk_usage.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestConditionsDiskUsage < Test::Unit::TestCase 4 | # valid? 5 | 6 | def test_valid_should_return_false_if_no_above_given 7 | c = Conditions::DiskUsage.new 8 | c.mount_point = '/' 9 | c.watch = stub(:name => 'foo') 10 | assert_equal false, c.valid? 11 | end 12 | 13 | def test_valid_should_return_false_if_no_mount_point_given 14 | c = Conditions::DiskUsage.new 15 | c.above = 90 16 | c.watch = stub(:name => 'foo') 17 | assert_equal false, c.valid? 18 | end 19 | 20 | def test_valid_should_return_true_if_required_options_all_set 21 | c = Conditions::DiskUsage.new 22 | c.above = 90 23 | c.mount_point = '/' 24 | c.watch = stub(:name => 'foo') 25 | 26 | assert_equal true, c.valid? 27 | end 28 | 29 | # test 30 | 31 | def test_test_should_return_true_if_above_limit 32 | c = Conditions::DiskUsage.new 33 | c.above = 90 34 | c.mount_point = '/' 35 | 36 | c.expects(:`).returns('91') 37 | 38 | assert_equal true, c.test 39 | end 40 | 41 | def test_test_should_return_false_if_below_limit 42 | c = Conditions::DiskUsage.new 43 | c.above = 90 44 | c.mount_point = '/' 45 | 46 | c.expects(:`).returns('90') 47 | 48 | assert_equal false, c.test 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/god/contacts/statsd.rb: -------------------------------------------------------------------------------- 1 | # Send a notice to statsd 2 | # 3 | # host - statsd host 4 | # port - statsd port (optional) 5 | 6 | require 'statsd-ruby' 7 | 8 | module God 9 | module Contacts 10 | 11 | class Statsd < Contact 12 | class << self 13 | attr_accessor :host, :port 14 | end 15 | 16 | attr_accessor :host, :port 17 | 18 | def valid? 19 | valid = true 20 | valid &= complain("Attribute 'statsd_host' must be specified", self) unless arg(:host) 21 | valid 22 | end 23 | 24 | def notify(message, time, priority, category, hostname) 25 | statsd = ::Statsd.new host, (port ? port.to_i : 8125) # 8125 is the default statsd port 26 | 27 | hostname.gsub! /\./, '_' 28 | app = message.gsub /([^\s]*).*/, '\1' 29 | 30 | [ 31 | 'cpu out of bounds', 32 | 'memory out of bounds', 33 | 'process is flapping' 34 | ].each do |event_type| 35 | statsd.increment "god.#{event_type.gsub(/\s/, '_')}.#{hostname}.#{app}" if message.include? event_type 36 | end 37 | 38 | self.info = 'sent statsd alert' 39 | rescue => e 40 | applog(nil, :info, "failed to send statsd alert: #{e.message}") 41 | applog(nil, :debug, e.backtrace.join("\n")) 42 | end 43 | end 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/god/contacts/airbrake.rb: -------------------------------------------------------------------------------- 1 | # Send a notice to Airbrake (http://airbrake.io/). 2 | # 3 | # apikey - The String API key. 4 | 5 | CONTACT_DEPS[:airbrake] = ['airbrake'] 6 | CONTACT_DEPS[:airbrake].each do |d| 7 | require d 8 | end 9 | 10 | module God 11 | module Contacts 12 | class Airbrake < Contact 13 | 14 | class << self 15 | attr_accessor :apikey 16 | end 17 | 18 | def valid? 19 | valid = true 20 | valid &= complain("Attribute 'apikey' must be specified", self) if self.apikey.nil? 21 | valid 22 | end 23 | 24 | attr_accessor :apikey 25 | 26 | def notify(message, time, priority, category, host) 27 | ::Airbrake.configure {} 28 | 29 | message = "God: #{message.to_s} at #{host}" 30 | message << " | #{[category, priority].join(" ")}" unless category.to_s.empty? or priority.to_s.empty? 31 | 32 | if ::Airbrake.notify nil, :error_message => message, :api_key => arg(:apikey) 33 | self.info = "sent airbrake notification to #{self.name}" 34 | else 35 | self.info = "failed to send airbrake notification to #{self.name}" 36 | end 37 | rescue Object => e 38 | applog(nil, :info, "failed to send airbrake notification: #{e.message}") 39 | applog(nil, :debug, e.backtrace.join("\n")) 40 | end 41 | 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/god/conditions/degrading_lambda.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module Conditions 3 | 4 | # This condition degrades its interval by a factor of two for 3 tries before failing 5 | class DegradingLambda < PollCondition 6 | attr_accessor :lambda 7 | 8 | def initialize 9 | super 10 | @tries = 0 11 | end 12 | 13 | def valid? 14 | valid = true 15 | valid &= complain("Attribute 'lambda' must be specified", self) if self.lambda.nil? 16 | valid 17 | end 18 | 19 | def test 20 | puts "Calling test. Interval at #{self.interval}" 21 | @original_interval ||= self.interval 22 | unless pass? 23 | if @tries == 2 24 | self.info = "lambda condition was satisfied" 25 | return true 26 | end 27 | self.interval = self.interval / 2.0 28 | @tries += 1 29 | else 30 | @tries = 0 31 | self.interval = @original_interval 32 | end 33 | 34 | self.info = "lambda condition was not satisfied" 35 | false 36 | end 37 | 38 | private 39 | 40 | def pass? 41 | begin 42 | Timeout::timeout(@interval) { 43 | self.lambda.call() 44 | } 45 | rescue Timeout::Error 46 | false 47 | end 48 | end 49 | end 50 | 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/god/sys_logger.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'syslog' 3 | 4 | # Ensure that Syslog is open 5 | begin 6 | Syslog.open('god') 7 | rescue RuntimeError 8 | Syslog.reopen('god') 9 | end 10 | 11 | Syslog.info("Syslog enabled.") 12 | 13 | module God 14 | 15 | class SysLogger 16 | SYMBOL_EQUIVALENTS = { :fatal => Syslog::LOG_CRIT, 17 | :error => Syslog::LOG_ERR, 18 | :warn => Syslog::LOG_WARNING, 19 | :info => Syslog::LOG_INFO, 20 | :debug => Syslog::LOG_DEBUG } 21 | 22 | # Set the log level 23 | # +level+ is the Symbol level to set as maximum. One of: 24 | # [:fatal | :error | :warn | :info | :debug ] 25 | # 26 | # Returns Nothing 27 | def self.level=(level) 28 | Syslog.mask = Syslog::LOG_UPTO(SYMBOL_EQUIVALENTS[level]) 29 | end 30 | 31 | # Log a message to syslog. 32 | # +level+ is the Symbol level of the message. One of: 33 | # [:fatal | :error | :warn | :info | :debug ] 34 | # +text+ is the String text of the message 35 | # 36 | # Returns Nothing 37 | def self.log(level, text) 38 | Syslog.log(SYMBOL_EQUIVALENTS[level], '%s', text) 39 | end 40 | end 41 | 42 | end 43 | rescue Object => e 44 | puts "Syslog could not be enabled: #{e.message}" 45 | end 46 | -------------------------------------------------------------------------------- /lib/god/simple_logger.rb: -------------------------------------------------------------------------------- 1 | module God 2 | 3 | class SimpleLogger 4 | DEBUG = 2 5 | INFO = 4 6 | WARN = 8 7 | ERROR = 16 8 | FATAL = 32 9 | 10 | SEV_LABEL = {DEBUG => 'DEBUG', 11 | INFO => 'INFO', 12 | WARN => 'WARN', 13 | ERROR => 'ERROR', 14 | FATAL => 'FATAL'} 15 | 16 | CONSTANT_TO_SYMBOL = { DEBUG => :debug, 17 | INFO => :info, 18 | WARN => :warn, 19 | ERROR => :error, 20 | FATAL => :fatal } 21 | 22 | attr_accessor :datetime_format, :level 23 | 24 | def initialize(io) 25 | @io = io 26 | @level = INFO 27 | @datetime_format = "%Y-%m-%d %H:%M:%S" 28 | end 29 | 30 | def output(level, msg) 31 | return if level < self.level 32 | 33 | time = Time.now.strftime(self.datetime_format) 34 | label = SEV_LABEL[level] 35 | @io.print("#{label[0..0]} [#{time}] #{label.rjust(5)}: #{msg}\n") 36 | end 37 | 38 | def fatal(msg) 39 | self.output(FATAL, msg) 40 | end 41 | 42 | def error(msg) 43 | self.output(ERROR, msg) 44 | end 45 | 46 | def warn(msg) 47 | self.output(WARN, msg) 48 | end 49 | 50 | def info(msg) 51 | self.output(INFO, msg) 52 | end 53 | 54 | def debug(msg) 55 | self.output(DEBUG, msg) 56 | end 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /test/test_logger.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestLogger < Test::Unit::TestCase 4 | def setup 5 | @log = God::Logger.new(StringIO.new('/dev/null')) 6 | end 7 | 8 | # log 9 | 10 | def test_log_should_keep_logs_when_wanted 11 | @log.watch_log_since('foo', Time.now) 12 | @log.expects(:info).with("qux") 13 | 14 | @log.log(stub(:name => 'foo'), :info, "qux") 15 | 16 | assert_equal 1, @log.logs.size 17 | assert_instance_of Time, @log.logs['foo'][0][0] 18 | assert_match(/qux/, @log.logs['foo'][0][1]) 19 | end 20 | 21 | def test_log_should_send_to_syslog 22 | SysLogger.expects(:log).with(:fatal, 'foo') 23 | @log.log(stub(:name => 'foo'), :fatal, "foo") 24 | end 25 | 26 | # watch_log_since 27 | 28 | def test_watch_log_since 29 | t1 = Time.now 30 | 31 | @log.watch_log_since('foo', t1) 32 | 33 | @log.log(stub(:name => 'foo'), :info, "one") 34 | @log.log(stub(:name => 'foo'), :info, "two") 35 | 36 | assert_match(/one.*two/m, @log.watch_log_since('foo', t1)) 37 | 38 | t2 = Time.now 39 | 40 | @log.log(stub(:name => 'foo'), :info, "three") 41 | 42 | out = @log.watch_log_since('foo', t2) 43 | 44 | assert_no_match(/one/, out) 45 | assert_no_match(/two/, out) 46 | assert_match(/three/, out) 47 | end 48 | 49 | # regular methods 50 | 51 | def test_fatal 52 | @log.fatal('foo') 53 | assert_equal 0, @log.logs.size 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/test_condition.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class BadlyImplementedCondition < PollCondition 4 | end 5 | 6 | class TestCondition < Test::Unit::TestCase 7 | 8 | # generate 9 | 10 | def test_generate_should_return_an_object_corresponding_to_the_given_type 11 | assert_equal Conditions::ProcessRunning, Condition.generate(:process_running, nil).class 12 | end 13 | 14 | def test_generate_should_raise_on_invalid_type 15 | assert_raise NoSuchConditionError do 16 | Condition.generate(:foo, nil) 17 | end 18 | end 19 | 20 | def test_generate_should_abort_on_event_condition_without_loaded_event_system 21 | God::EventHandler.stubs(:operational?).returns(false) 22 | assert_abort do 23 | God::EventHandler.start 24 | Condition.generate(:process_exits, nil).class 25 | end 26 | ensure 27 | God::EventHandler.stop 28 | end 29 | 30 | def test_generate_should_return_a_good_error_message_for_invalid_types 31 | emsg = "No Condition found with the class name God::Conditions::FooBar" 32 | rmsg = nil 33 | 34 | begin 35 | Condition.generate(:foo_bar, nil) 36 | rescue => e 37 | rmsg = e.message 38 | end 39 | 40 | assert_equal emsg, rmsg 41 | end 42 | 43 | # test 44 | 45 | def test_test_should_raise_if_not_defined_in_subclass 46 | c = BadlyImplementedCondition.new 47 | 48 | assert_raise AbstractMethodNotOverriddenError do 49 | c.test 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/test_trigger.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestTrigger < Test::Unit::TestCase 4 | def setup 5 | Trigger.reset 6 | end 7 | 8 | # base case 9 | 10 | def test_should_have_empty_triggers 11 | assert_equal({}, Trigger.triggers) 12 | end 13 | 14 | # register 15 | 16 | def test_register_should_add_condition_to_triggers 17 | c = Condition.new 18 | c.watch = stub(:name => 'foo') 19 | Trigger.register(c) 20 | 21 | assert_equal({'foo' => [c]}, Trigger.triggers) 22 | end 23 | 24 | def test_register_should_add_condition_to_triggers_twice 25 | watch = stub(:name => 'foo') 26 | c = Condition.new 27 | c.watch = watch 28 | Trigger.register(c) 29 | 30 | c2 = Condition.new 31 | c2.watch = watch 32 | Trigger.register(c2) 33 | 34 | assert_equal({'foo' => [c, c2]}, Trigger.triggers) 35 | end 36 | 37 | # deregister 38 | 39 | def test_deregister_should_remove_condition_from_triggers 40 | c = Condition.new 41 | c.watch = stub(:name => 'foo') 42 | Trigger.register(c) 43 | Trigger.deregister(c) 44 | 45 | assert_equal({}, Trigger.triggers) 46 | end 47 | 48 | # broadcast 49 | 50 | def test_broadcast_should_call_process_on_each_condition 51 | c = Condition.new 52 | c.watch = stub(:name => 'foo') 53 | Trigger.register(c) 54 | 55 | c.expects(:process).with(:state_change, [:up, :start]) 56 | 57 | Trigger.broadcast(c.watch, :state_change, [:up, :start]) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/test_conditions_tries.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestConditionsTries < Test::Unit::TestCase 4 | # valid? 5 | 6 | def test_valid_should_return_false_if_times_not_set 7 | c = Conditions::Tries.new 8 | c.watch = stub(:name => 'foo') 9 | assert !c.valid? 10 | end 11 | end 12 | 13 | 14 | class TestConditionsTries < Test::Unit::TestCase 15 | def setup 16 | @c = Conditions::Tries.new 17 | @c.times = 3 18 | @c.prepare 19 | end 20 | 21 | # prepare 22 | 23 | def test_prepare_should_create_timeline 24 | assert_equal 3, @c.instance_variable_get(:@timeline).instance_variable_get(:@max_size) 25 | end 26 | 27 | # test 28 | 29 | def test_test_should_return_true_if_called_three_times_within_one_second 30 | assert !@c.test 31 | assert !@c.test 32 | assert @c.test 33 | end 34 | 35 | # reset 36 | 37 | def test_test_should_return_false_on_fourth_call_if_called_three_times_within_one_second 38 | 3.times { @c.test } 39 | @c.reset 40 | assert !@c.test 41 | end 42 | end 43 | 44 | 45 | class TestConditionsTriesWithin < Test::Unit::TestCase 46 | def setup 47 | @c = Conditions::Tries.new 48 | @c.times = 3 49 | @c.within = 1.seconds 50 | @c.prepare 51 | end 52 | 53 | # test 54 | 55 | def test_test_should_return_true_if_called_three_times_within_one_second 56 | assert !@c.test 57 | assert !@c.test 58 | assert @c.test 59 | end 60 | 61 | def test_test_should_return_false_if_called_three_times_within_two_seconds 62 | assert !@c.test 63 | assert !@c.test 64 | assert sleep(1.1) 65 | assert !@c.test 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/configs/complex/complex.god: -------------------------------------------------------------------------------- 1 | God.watch do |w| 2 | w.name = "complex" 3 | w.interval = 5.seconds 4 | w.start = File.join(GOD_ROOT, *%w[test configs complex simple_server.rb]) 5 | # w.log = File.join(GOD_ROOT, *%w[test configs child_events god.log]) 6 | 7 | # determine the state on startup 8 | w.transition(:init, { true => :up, false => :start }) do |on| 9 | on.condition(:process_running) do |c| 10 | c.running = true 11 | end 12 | end 13 | 14 | # determine when process has finished starting 15 | w.transition([:start, :restart], :up) do |on| 16 | on.condition(:process_running) do |c| 17 | c.running = true 18 | end 19 | 20 | # failsafe 21 | on.condition(:tries) do |c| 22 | c.times = 2 23 | c.transition = :start 24 | end 25 | end 26 | 27 | # start if process is not running 28 | w.transition(:up, :start) do |on| 29 | on.condition(:process_exits) 30 | end 31 | 32 | # restart if process is misbehaving 33 | w.transition(:up, :restart) do |on| 34 | on.condition(:complex) do |cc| 35 | cc.and(:cpu_usage) do |c| 36 | c.above = 0.percent 37 | c.times = 1 38 | end 39 | 40 | cc.and(:memory_usage) do |c| 41 | c.above = 0.megabytes 42 | c.times = 3 43 | end 44 | end 45 | end 46 | 47 | # lifecycle 48 | w.lifecycle do |on| 49 | on.condition(:flapping) do |c| 50 | c.to_state = [:start, :restart] 51 | c.times = 5 52 | c.within = 20.seconds 53 | c.transition = :unmonitored 54 | c.retry_in = 10.seconds 55 | c.retry_times = 2 56 | c.retry_within = 5.minutes 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/configs/real.rb: -------------------------------------------------------------------------------- 1 | if $0 == __FILE__ 2 | require File.join(File.dirname(__FILE__), *%w[.. .. lib god]) 3 | end 4 | 5 | RAILS_ROOT = "/Users/tom/dev/git/helloworld" 6 | 7 | God.watch do |w| 8 | w.name = "local-3000" 9 | w.interval = 5 # seconds 10 | w.start = "mongrel_rails start -P ./log/mongrel.pid -c #{RAILS_ROOT} -d" 11 | w.stop = "mongrel_rails stop -P ./log/mongrel.pid -c #{RAILS_ROOT}" 12 | w.grace = 5 13 | 14 | pid_file = File.join(RAILS_ROOT, "log/mongrel.pid") 15 | 16 | # clean pid files before start if necessary 17 | w.behavior(:clean_pid_file) do |b| 18 | b.pid_file = pid_file 19 | end 20 | 21 | # start if process is not running 22 | w.start_if do |start| 23 | start.condition(:process_running) do |c| 24 | c.running = false 25 | c.pid_file = pid_file 26 | end 27 | end 28 | 29 | # restart if memory or cpu is too high 30 | w.restart_if do |restart| 31 | restart.condition(:memory_usage) do |c| 32 | c.interval = 20 33 | c.pid_file = pid_file 34 | c.above = (50 * 1024) # 50mb 35 | c.times = [3, 5] 36 | end 37 | 38 | restart.condition(:cpu_usage) do |c| 39 | c.interval = 10 40 | c.pid_file = pid_file 41 | c.above = 10 # percent 42 | c.times = [3, 5] 43 | end 44 | end 45 | end 46 | 47 | # clear old session files 48 | # god.watch do |w| 49 | # w.name = "local-session-cleanup" 50 | # w.start = lambda do 51 | # Dir["#{RAILS_ROOT}/tmp/sessions/ruby_sess.*"].select do |f| 52 | # File.mtime(f) < Time.now - (7 * 24 * 60 * 60) 53 | # end.each { |f| File.delete(f) } 54 | # end 55 | # 56 | # w.start_if do |start| 57 | # start.condition(:always) 58 | # end 59 | # end 60 | -------------------------------------------------------------------------------- /examples/gravatar.god: -------------------------------------------------------------------------------- 1 | # run with: god -c /path/to/gravatar.god 2 | # 3 | # This is the actual config file used to keep the mongrels of 4 | # gravatar.com running. 5 | 6 | RAILS_ROOT = "/Users/tom/dev/gravatar2" 7 | 8 | %w{8200 8201 8202}.each do |port| 9 | God.watch do |w| 10 | w.name = "gravatar2-mongrel-#{port}" 11 | w.interval = 30.seconds # default 12 | w.start = "mongrel_rails start -c #{RAILS_ROOT} -p #{port} \ 13 | -P #{RAILS_ROOT}/log/mongrel.#{port}.pid -d" 14 | w.stop = "mongrel_rails stop -P #{RAILS_ROOT}/log/mongrel.#{port}.pid" 15 | w.restart = "mongrel_rails restart -P #{RAILS_ROOT}/log/mongrel.#{port}.pid" 16 | w.start_grace = 10.seconds 17 | w.restart_grace = 10.seconds 18 | w.pid_file = File.join(RAILS_ROOT, "log/mongrel.#{port}.pid") 19 | 20 | w.behavior(:clean_pid_file) 21 | 22 | w.start_if do |start| 23 | start.condition(:process_running) do |c| 24 | c.interval = 5.seconds 25 | c.running = false 26 | end 27 | end 28 | 29 | w.restart_if do |restart| 30 | restart.condition(:memory_usage) do |c| 31 | c.above = 150.megabytes 32 | c.times = [3, 5] # 3 out of 5 intervals 33 | end 34 | 35 | restart.condition(:cpu_usage) do |c| 36 | c.above = 50.percent 37 | c.times = 5 38 | end 39 | end 40 | 41 | # lifecycle 42 | w.lifecycle do |on| 43 | on.condition(:flapping) do |c| 44 | c.to_state = [:start, :restart] 45 | c.times = 5 46 | c.within = 5.minute 47 | c.transition = :unmonitored 48 | c.retry_in = 10.minutes 49 | c.retry_times = 5 50 | c.retry_within = 2.hours 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /ext/god/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | 3 | fail = false 4 | 5 | def create_dummy_makefile 6 | File.open("Makefile", 'w') do |f| 7 | f.puts "all:" 8 | f.puts "install:" 9 | end 10 | end 11 | 12 | case RUBY_PLATFORM 13 | when /bsd/i, /darwin/i 14 | unless have_header('sys/event.h') 15 | puts 16 | puts "Missing 'sys/event.h' header" 17 | fail = true 18 | end 19 | 20 | if fail 21 | puts 22 | puts "Events handler could not be compiled (see above error). Your god installation will not support event conditions." 23 | create_dummy_makefile 24 | else 25 | create_makefile 'kqueue_handler_ext' 26 | end 27 | when /linux/i 28 | unless have_header('linux/netlink.h') 29 | puts 30 | puts "Missing 'linux/netlink.h' header(s)" 31 | puts "You may need to install a header package for your system" 32 | fail = true 33 | end 34 | 35 | unless have_header('linux/connector.h') && have_header('linux/cn_proc.h') 36 | puts 37 | puts "Missing 'linux/connector.h', or 'linux/cn_proc.h' header(s)" 38 | puts "These are only available in Linux kernel 2.6.15 and later (run `uname -a` to find yours)" 39 | puts "You may need to install a header package for your system" 40 | fail = true 41 | end 42 | 43 | if fail 44 | puts 45 | puts "Events handler could not be compiled (see above error). Your god installation will not support event conditions." 46 | create_dummy_makefile 47 | else 48 | create_makefile 'netlink_handler_ext' 49 | end 50 | else 51 | puts 52 | puts "Unsupported platform '#{RUBY_PLATFORM}'. Supported platforms are BSD, DARWIN, and LINUX." 53 | puts "Your god installation will not support event conditions." 54 | create_dummy_makefile 55 | end 56 | -------------------------------------------------------------------------------- /lib/god/configurable.rb: -------------------------------------------------------------------------------- 1 | module God 2 | 3 | module Configurable 4 | # Override this method in your Configurable (optional) 5 | # 6 | # Called once after the Configurable has been sent to the block and attributes have been 7 | # set. Do any post-processing on attributes here 8 | def prepare 9 | 10 | end 11 | 12 | def reset 13 | 14 | end 15 | 16 | # Override this method in your Configurable (optional) 17 | # 18 | # Called once during evaluation of the config file. Return true if valid, false otherwise 19 | # 20 | # A convenience method 'complain' is available that will print out a message and return false, 21 | # making it easy to report multiple validation errors: 22 | # 23 | # def valid? 24 | # valid = true 25 | # valid &= complain("You must specify the 'pid_file' attribute for :memory_usage") if self.pid_file.nil? 26 | # valid &= complain("You must specify the 'above' attribute for :memory_usage") if self.above.nil? 27 | # valid 28 | # end 29 | def valid? 30 | true 31 | end 32 | 33 | def base_name 34 | x = 1 # fix for MRI's local scope optimization bug DO NOT REMOVE! 35 | @base_name ||= self.class.name.split('::').last 36 | end 37 | 38 | def friendly_name 39 | base_name 40 | end 41 | 42 | def self.complain(text, c = nil) 43 | watch = c.watch rescue nil 44 | msg = "" 45 | msg += "#{watch.name}: " if watch 46 | msg += text 47 | msg += " for #{c.friendly_name}" if c 48 | applog(watch, :error, msg) 49 | false 50 | end 51 | 52 | def complain(text, c = nil) 53 | Configurable.complain(text, c) 54 | end 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /lib/god/behaviors/notify_when_flapping.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module Behaviors 3 | 4 | class NotifyWhenFlapping < Behavior 5 | attr_accessor :failures # number of failures 6 | attr_accessor :seconds # number of seconds 7 | attr_accessor :notifier # class to notify with 8 | 9 | def initialize 10 | super 11 | @startup_times = [] 12 | end 13 | 14 | def valid? 15 | valid = true 16 | valid &= complain("Attribute 'failures' must be specified", self) unless self.failures 17 | valid &= complain("Attribute 'seconds' must be specified", self) unless self.seconds 18 | valid &= complain("Attribute 'notifier' must be specified", self) unless self.notifier 19 | 20 | # Must take one arg or variable args 21 | unless self.notifier.respond_to?(:notify) and [1,-1].include?(self.notifier.method(:notify).arity) 22 | valid &= complain("The 'notifier' must have a method 'notify' which takes 1 or variable args", self) 23 | end 24 | 25 | valid 26 | end 27 | 28 | def before_start 29 | now = Time.now.to_i 30 | @startup_times << now 31 | check_for_flapping(now) 32 | end 33 | 34 | def before_restart 35 | now = Time.now.to_i 36 | @startup_times << now 37 | check_for_flapping(now) 38 | end 39 | 40 | private 41 | 42 | def check_for_flapping(now) 43 | @startup_times.select! {|time| time >= now - self.seconds } 44 | if @startup_times.length >= self.failures 45 | self.notifier.notify("#{self.watch.name} has called start/restart #{@startup_times.length} times in #{self.seconds} seconds") 46 | end 47 | end 48 | end 49 | 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/test_slack.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestSlack < Test::Unit::TestCase 4 | def setup 5 | @slack = God::Contacts::Slack.new 6 | @slack.account = "foo" 7 | @slack.token = "foo" 8 | 9 | @sample_data = { 10 | :message => "a sample message", 11 | :time => "2038-01-01 00:00:00", 12 | :priority => "High", 13 | :category => "Test", 14 | :host => "example.com" 15 | } 16 | end 17 | 18 | def test_api_url 19 | assert_equal "https://foo.slack.com/services/hooks/incoming-webhook?token=foo", @slack.api_url.to_s 20 | end 21 | 22 | def test_notify 23 | Net::HTTP.any_instance.expects(:request).returns(Net::HTTPSuccess.new('a', 'b', 'c')) 24 | 25 | @slack.channel = "#ops" 26 | 27 | @slack.notify('msg', Time.now, 'prio', 'cat', 'host') 28 | assert_equal "successfully notified slack on channel #ops", @slack.info 29 | end 30 | 31 | def test_default_channel 32 | Net::HTTP.any_instance.expects(:request).returns(Net::HTTPSuccess.new('a', 'b', 'c')) 33 | 34 | @slack.notify('msg', Time.now, 'prio', 'cat', 'host') 35 | assert_equal "successfully notified slack on channel #general", @slack.info 36 | end 37 | 38 | def test_default_formatting 39 | text = @slack.text(@sample_data) 40 | assert_equal "High alert on example.com: a sample message (Test, 2038-01-01 00:00:00)", text 41 | end 42 | 43 | def test_custom_formatting 44 | @slack.format = "%{host}: %{message}" 45 | text = @slack.text(@sample_data) 46 | assert_equal "example.com: a sample message", text 47 | end 48 | 49 | def test_notify_channel 50 | @slack.notify_channel = true 51 | @slack.format = "" 52 | text = @slack.text(@sample_data) 53 | assert_equal " ", text 54 | end 55 | end 56 | 57 | -------------------------------------------------------------------------------- /lib/god/contacts/scout.rb: -------------------------------------------------------------------------------- 1 | # Send a notice to Scout (http://scoutapp.com/). 2 | # 3 | # client_key - The String client key. 4 | # plugin_id - The String plugin id. 5 | 6 | require 'net/http' 7 | require 'uri' 8 | 9 | module God 10 | module Contacts 11 | 12 | class Scout < Contact 13 | class << self 14 | attr_accessor :client_key, :plugin_id 15 | attr_accessor :format 16 | end 17 | 18 | self.format = lambda do |message, priority, category, host| 19 | text = "Message: #{message}\n" 20 | text += "Host: #{host}\n" if host 21 | text += "Priority: #{priority}\n" if priority 22 | text += "Category: #{category}\n" if category 23 | return text 24 | end 25 | 26 | attr_accessor :client_key, :plugin_id 27 | 28 | def valid? 29 | valid = true 30 | valid &= complain("Attribute 'client_key' must be specified", self) unless arg(:client_key) 31 | valid &= complain("Attribute 'plugin_id' must be specified", self) unless arg(:plugin_id) 32 | valid 33 | end 34 | 35 | def notify(message, time, priority, category, host) 36 | data = { 37 | :client_key => arg(:client_key), 38 | :plugin_id => arg(:plugin_id), 39 | :format => 'xml', 40 | 'alert[subject]' => message, 41 | 'alert[body]' => Scout.format.call(message, priority, category, host) 42 | } 43 | 44 | uri = URI.parse('http://scoutapp.com/alerts/create') 45 | Net::HTTP.post_form(uri, data) 46 | 47 | self.info = "sent scout alert to plugin ##{plugin_id}" 48 | rescue => e 49 | applog(nil, :info, "failed to send scout alert to plugin ##{plugin_id}: #{e.message}") 50 | applog(nil, :debug, e.backtrace.join("\n")) 51 | end 52 | end 53 | 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/configs/test.rb: -------------------------------------------------------------------------------- 1 | ENV['GOD_TEST_RAILS_ROOT'] || abort("Set a rails root for testing in an environment variable called GOD_TEST_RAILS_ROOT") 2 | 3 | RAILS_ROOT = ENV['GOD_TEST_RAILS_ROOT'] 4 | 5 | port = 5000 6 | 7 | God.watch do |w| 8 | w.name = "local-#{port}" 9 | w.interval = 5.seconds 10 | w.start = "mongrel_rails start -P ./log/mongrel.pid -c #{RAILS_ROOT} -p #{port} -d" 11 | w.restart = "mongrel_rails restart -P ./log/mongrel.pid -c #{RAILS_ROOT}" 12 | w.stop = "mongrel_rails stop -P ./log/mongrel.pid -c #{RAILS_ROOT}" 13 | w.group = 'mongrels' 14 | w.pid_file = File.join(RAILS_ROOT, "log/mongrel.pid") 15 | 16 | # clean pid files before start if necessary 17 | w.behavior(:clean_pid_file) 18 | 19 | # determine the state on startup 20 | w.transition(:init, { true => :up, false => :start }) do |on| 21 | on.condition(:process_running) do |c| 22 | c.running = true 23 | end 24 | end 25 | 26 | # determine when process has finished starting 27 | w.transition([:start, :restart], :up) do |on| 28 | on.condition(:process_running) do |c| 29 | c.running = true 30 | end 31 | end 32 | 33 | # start if process is not running 34 | w.transition(:up, :start) do |on| 35 | on.condition(:process_exits) 36 | end 37 | 38 | # restart if memory or cpu is too high 39 | w.transition(:up, :restart) do |on| 40 | on.condition(:memory_usage) do |c| 41 | c.interval = 1 42 | c.above = 50.megabytes 43 | c.times = [3, 5] 44 | end 45 | 46 | on.condition(:cpu_usage) do |c| 47 | c.interval = 1 48 | c.above = 10.percent 49 | c.times = [3, 5] 50 | end 51 | 52 | on.condition(:http_response_code) do |c| 53 | c.host = 'localhost' 54 | c.port = port 55 | c.path = '/' 56 | c.code_is_not = 200 57 | c.timeout = 10.seconds 58 | c.times = [3, 5] 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/god/contacts/prowl.rb: -------------------------------------------------------------------------------- 1 | # Send a notice to Prowl (http://prowl.weks.net/). 2 | # 3 | # apikey - The String API key. 4 | 5 | CONTACT_DEPS[:prowl] = ['prowly'] 6 | CONTACT_DEPS[:prowl].each do |d| 7 | require d 8 | end 9 | 10 | module God 11 | module Contacts 12 | class Prowl < Contact 13 | 14 | class << self 15 | attr_accessor :apikey 16 | end 17 | 18 | def valid? 19 | valid = true 20 | valid &= complain("Attribute 'apikey' must be specified", self) if self.apikey.nil? 21 | valid 22 | end 23 | 24 | attr_accessor :apikey 25 | 26 | def notify(message, time, priority, category, host) 27 | result = Prowly.notify do |n| 28 | n.apikey = arg(:apikey) 29 | n.priority = map_priority(priority.to_i) 30 | n.application = category || "God" 31 | n.event = "on " + host.to_s 32 | n.description = message.to_s + " at " + time.to_s 33 | end 34 | 35 | if result.succeeded? 36 | self.info = "sent prowl notification to #{self.name}" 37 | else 38 | self.info = "failed to send prowl notification to #{self.name}: #{result.message}" 39 | end 40 | rescue Object => e 41 | applog(nil, :info, "failed to send prowl notification to #{self.name}: #{e.message}") 42 | applog(nil, :debug, e.backtrace.join("\n")) 43 | end 44 | 45 | def map_priority(priority) 46 | case priority 47 | when 1 then Prowly::Notification::Priority::EMERGENCY 48 | when 2 then Prowly::Notification::Priority::HIGH 49 | when 3 then Prowly::Notification::Priority::NORMAL 50 | when 4 then Prowly::Notification::Priority::MODERATE 51 | when 5 then Prowly::Notification::Priority::VERY_LOW 52 | else Prowly::Notification::Priority::NORMAL 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /examples/single.god: -------------------------------------------------------------------------------- 1 | RAILS_ROOT = "/Users/tom/dev/gravatar2" 2 | 3 | God.watch do |w| 4 | w.name = "local-3000" 5 | w.interval = 5.seconds # default 6 | w.start = "mongrel_rails start -c #{RAILS_ROOT} -P #{RAILS_ROOT}/log/mongrel.pid -p 3000 -d" 7 | w.stop = "mongrel_rails stop -P #{RAILS_ROOT}/log/mongrel.pid" 8 | w.restart = "mongrel_rails restart -P #{RAILS_ROOT}/log/mongrel.pid" 9 | w.pid_file = File.join(RAILS_ROOT, "log/mongrel.pid") 10 | 11 | # clean pid files before start if necessary 12 | w.behavior(:clean_pid_file) 13 | 14 | # determine the state on startup 15 | w.transition(:init, { true => :up, false => :start }) do |on| 16 | on.condition(:process_running) do |c| 17 | c.running = true 18 | end 19 | end 20 | 21 | # determine when process has finished starting 22 | w.transition([:start, :restart], :up) do |on| 23 | on.condition(:process_running) do |c| 24 | c.running = true 25 | end 26 | 27 | # failsafe 28 | on.condition(:tries) do |c| 29 | c.times = 5 30 | c.transition = :start 31 | end 32 | end 33 | 34 | # start if process is not running 35 | w.transition(:up, :start) do |on| 36 | on.condition(:process_exits) 37 | end 38 | 39 | # restart if memory or cpu is too high 40 | w.transition(:up, :restart) do |on| 41 | on.condition(:memory_usage) do |c| 42 | c.interval = 20 43 | c.above = 50.megabytes 44 | c.times = [3, 5] 45 | end 46 | 47 | on.condition(:cpu_usage) do |c| 48 | c.interval = 10 49 | c.above = 10.percent 50 | c.times = [3, 5] 51 | end 52 | end 53 | 54 | # lifecycle 55 | w.lifecycle do |on| 56 | on.condition(:flapping) do |c| 57 | c.to_state = [:start, :restart] 58 | c.times = 5 59 | c.within = 5.minute 60 | c.transition = :unmonitored 61 | c.retry_in = 10.minutes 62 | c.retry_times = 5 63 | c.retry_within = 2.hours 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/god/contacts/twitter.rb: -------------------------------------------------------------------------------- 1 | # Send a notice to a Twitter account (http://twitter.com/). 2 | # 3 | # consumer_token - The String OAuth consumer token (defaults to God's 4 | # existing consumer token). 5 | # consumer_secret - The String OAuth consumer secret (defaults to God's 6 | # existing consumer secret). 7 | # access_token - The String OAuth access token. 8 | # access_secret - The String OAuth access secret. 9 | 10 | CONTACT_DEPS[:twitter] = ['twitter'] 11 | CONTACT_DEPS[:twitter].each do |d| 12 | require d 13 | end 14 | 15 | module God 16 | module Contacts 17 | class Twitter < Contact 18 | class << self 19 | attr_accessor :consumer_token, :consumer_secret, 20 | :access_token, :access_secret 21 | end 22 | 23 | self.consumer_token = 'gOhjax6s0L3mLeaTtBWPw' 24 | self.consumer_secret = 'yz4gpAVXJHKxvsGK85tEyzQJ7o2FEy27H1KEWL75jfA' 25 | 26 | def valid? 27 | valid = true 28 | valid &= complain("Attribute 'consumer_token' must be specified", self) unless arg(:consumer_token) 29 | valid &= complain("Attribute 'consumer_secret' must be specified", self) unless arg(:consumer_secret) 30 | valid &= complain("Attribute 'access_token' must be specified", self) unless arg(:access_token) 31 | valid &= complain("Attribute 'access_secret' must be specified", self) unless arg(:access_secret) 32 | valid 33 | end 34 | 35 | attr_accessor :consumer_token, :consumer_secret, 36 | :access_token, :access_secret 37 | 38 | def notify(message, time, priority, category, host) 39 | oauth = ::Twitter::OAuth.new(arg(:consumer_token), arg(:consumer_secret)) 40 | oauth.authorize_from_access(arg(:access_token), arg(:access_secret)) 41 | 42 | ::Twitter::Base.new(oauth).update(message) 43 | 44 | self.info = "sent twitter update" 45 | rescue => e 46 | applog(nil, :info, "failed to send twitter update: #{e.message}") 47 | applog(nil, :debug, e.backtrace.join("\n")) 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /init/lsb_compliant_god: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # see http://refspecs.freestandards.org/LSB_3.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html 4 | 5 | PATH="/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin" 6 | DESC="god daemon" 7 | NAME="god" 8 | DAEMON="/usr/bin/#{NAME}" 9 | CONFIGFILE="/etc/god/god.conf" 10 | PIDFILE="/var/run/#{NAME}.pid" 11 | SCRIPTNAME="/etc/init.d/#{NAME}" 12 | START_FLAGS="--no-syslog -P #{PIDFILE}" 13 | 14 | ENV["PATH"] = PATH 15 | 16 | def read_pid 17 | begin 18 | @pid = File.read(PIDFILE).to_i 19 | @pid = nil if @pid == 0 20 | rescue 21 | @pid = nil 22 | end 23 | end 24 | 25 | def kill(code) 26 | Process.kill(code, @pid) 27 | true 28 | rescue 29 | false 30 | end 31 | 32 | def running? 33 | @pid && kill(0) 34 | end 35 | 36 | def dead? 37 | @pid && !kill(0) 38 | end 39 | 40 | def start 41 | if running? 42 | puts "already running (#{@pid})" 43 | exit 44 | end 45 | 46 | if dead? 47 | clean_pid 48 | end 49 | 50 | puts "starting #{NAME}" 51 | system("#{DAEMON} -c #{CONFIGFILE} #{START_FLAGS}") 52 | end 53 | 54 | def stop 55 | if not running? 56 | puts "not running" 57 | exit 58 | end 59 | 60 | system("god quit") 61 | end 62 | 63 | def restart 64 | if running? 65 | stop 66 | read_pid 67 | end 68 | start 69 | end 70 | 71 | def force_reload 72 | if running? 73 | restart 74 | end 75 | end 76 | 77 | def clean_pid 78 | File.delete(PIDFILE) 79 | end 80 | 81 | read_pid 82 | case ARGV[0] 83 | when 'start' 84 | start 85 | when 'stop' 86 | stop 87 | when 'restart' 88 | if not running? 89 | start 90 | else 91 | restart 92 | end 93 | when 'force-reload' 94 | force_reload 95 | when 'status' 96 | if running? 97 | puts "running (#{@pid})" 98 | elsif dead? 99 | puts "dead (#{@pid})" 100 | exit!(1) 101 | else 102 | puts "not running" 103 | exit!(3) 104 | end 105 | else 106 | puts "Usage: #{SCRIPTNAME} start|stop|restart|force-reload|status" 107 | end 108 | 109 | exit 110 | -------------------------------------------------------------------------------- /lib/god/conditions/process_running.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module Conditions 3 | # Trigger when a process is running or not running depending on attributes. 4 | # 5 | # Examples 6 | # 7 | # # Trigger if process IS NOT running. 8 | # on.condition(:process_running) do |c| 9 | # c.running = false 10 | # end 11 | # 12 | # # Trigger if process IS running. 13 | # on.condition(:process_running) do |c| 14 | # c.running = true 15 | # end 16 | # 17 | # # Non-Watch Tasks must specify a PID file. 18 | # on.condition(:process_running) do |c| 19 | # c.running = false 20 | # c.pid_file = "/var/run/mongrel.3000.pid" 21 | # end 22 | class ProcessRunning < PollCondition 23 | # Public: The Boolean specifying whether you want to trigger if the 24 | # process is running (true) or if it is not running (false). 25 | attr_accessor :running 26 | 27 | # Public: The String PID file location of the process in question. 28 | # Automatically populated for Watches. 29 | attr_accessor :pid_file 30 | 31 | def pid 32 | self.pid_file ? File.read(self.pid_file).strip.to_i : self.watch.pid 33 | end 34 | 35 | def valid? 36 | valid = true 37 | valid &= complain("Attribute 'pid_file' must be specified", self) if self.pid_file.nil? && self.watch.pid_file.nil? 38 | valid &= complain("Attribute 'running' must be specified", self) if self.running.nil? 39 | valid 40 | end 41 | 42 | def test 43 | self.info = [] 44 | 45 | pid = self.pid 46 | active = pid && System::Process.new(pid).exists? 47 | 48 | if (self.running && active) 49 | self.info.concat(["process is running"]) 50 | true 51 | elsif (!self.running && !active) 52 | self.info.concat(["process is not running"]) 53 | true 54 | else 55 | if self.running 56 | self.info.concat(["process is not running"]) 57 | end 58 | false 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/test_metric.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestMetric < Test::Unit::TestCase 4 | def setup 5 | @metric = Metric.new(stub(:interval => 10), nil) 6 | end 7 | 8 | # watch 9 | 10 | def test_watch 11 | w = stub() 12 | m = Metric.new(w, nil) 13 | assert_equal w, m.watch 14 | end 15 | 16 | # destination 17 | 18 | def test_destination 19 | d = stub() 20 | m = Metric.new(nil, d) 21 | assert_equal d, m.destination 22 | end 23 | 24 | # condition 25 | 26 | def test_condition_should_be_block_optional 27 | @metric.condition(:fake_poll_condition) 28 | assert_equal 1, @metric.conditions.size 29 | end 30 | 31 | def test_poll_condition_should_inherit_interval_from_watch_if_not_specified 32 | @metric.condition(:fake_poll_condition) 33 | assert_equal 10, @metric.conditions.first.interval 34 | end 35 | 36 | def test_poll_condition_should_abort_if_no_interval_and_no_watch_interval 37 | metric = Metric.new(stub(:name => 'foo', :interval => nil), nil) 38 | 39 | assert_abort do 40 | metric.condition(:fake_poll_condition) 41 | end 42 | end 43 | 44 | # This doesn't currently work: 45 | # 46 | # def test_condition_should_allow_generation_of_subclasses_of_poll_or_event 47 | # metric = Metric.new(stub(:name => 'foo', :interval => 10), nil) 48 | # 49 | # assert_nothing_raised do 50 | # metric.condition(:fake_poll_condition) 51 | # metric.condition(:fake_event_condition) 52 | # end 53 | # end 54 | 55 | def test_condition_should_abort_if_not_subclass_of_poll_or_event 56 | metric = Metric.new(stub(:name => 'foo', :interval => 10), nil) 57 | 58 | assert_abort do 59 | metric.condition(:fake_condition) { |c| } 60 | end 61 | end 62 | 63 | def test_condition_should_abort_on_invalid_condition 64 | assert_abort do 65 | @metric.condition(:fake_poll_condition) { |c| c.stubs(:valid?).returns(false) } 66 | end 67 | end 68 | 69 | def test_condition_should_abort_on_no_such_condition 70 | assert_abort do 71 | @metric.condition(:invalid) { } 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/god/contacts/webhook.rb: -------------------------------------------------------------------------------- 1 | # Send a notice to a webhook. 2 | # 3 | # url - The String webhook URL. 4 | # format - The Symbol format [ :form | :json ] (default: :form). 5 | 6 | require 'net/http' 7 | require 'uri' 8 | 9 | CONTACT_DEPS[:webhook] = ['json'] 10 | CONTACT_DEPS[:webhook].each do |d| 11 | require d 12 | end 13 | 14 | module God 15 | module Contacts 16 | 17 | class Webhook < Contact 18 | class << self 19 | attr_accessor :url, :format 20 | end 21 | 22 | self.format = :form 23 | 24 | def valid? 25 | valid = true 26 | valid &= complain("Attribute 'url' must be specified", self) unless arg(:url) 27 | valid &= complain("Attribute 'format' must be one of [ :form | :json ]", self) unless [:form, :json].include?(arg(:format)) 28 | valid 29 | end 30 | 31 | attr_accessor :url, :format 32 | 33 | def notify(message, time, priority, category, host) 34 | data = { 35 | :message => message, 36 | :time => time, 37 | :priority => priority, 38 | :category => category, 39 | :host => host 40 | } 41 | 42 | uri = URI.parse(arg(:url)) 43 | http = Net::HTTP.new(uri.host, uri.port) 44 | http.use_ssl = true if uri.scheme == "https" 45 | 46 | req = nil 47 | res = nil 48 | 49 | case arg(:format) 50 | when :form 51 | req = Net::HTTP::Post.new(uri.request_uri) 52 | req.set_form_data(data) 53 | when :json 54 | req = Net::HTTP::Post.new(uri.request_uri) 55 | req.body = data.to_json 56 | end 57 | 58 | res = http.request(req) 59 | 60 | case res 61 | when Net::HTTPSuccess 62 | self.info = "sent webhook to #{arg(:url)}" 63 | else 64 | self.info = "failed to send webhook to #{arg(:url)}: #{res.error!}" 65 | end 66 | rescue Object => e 67 | applog(nil, :info, "failed to send webhook to #{arg(:url)}: #{e.message}") 68 | applog(nil, :debug, e.backtrace.join("\n")) 69 | end 70 | 71 | end 72 | 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/god/conditions/process_exits.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module Conditions 3 | # Trigger when a process exits. 4 | # 5 | # +pid_file+ is the pid file of the process in question. Automatically 6 | # populated for Watches. 7 | # 8 | # Examples 9 | # 10 | # # Trigger if process exits (from a Watch). 11 | # on.condition(:process_exits) 12 | # 13 | # # Trigger if process exits (non-Watch). 14 | # on.condition(:process_exits) do |c| 15 | # c.pid_file = "/var/run/mongrel.3000.pid" 16 | # end 17 | class ProcessExits < EventCondition 18 | # The String PID file location of the process in question. Automatically 19 | # populated for Watches. 20 | attr_accessor :pid_file 21 | 22 | def initialize 23 | self.info = "process exited" 24 | end 25 | 26 | def valid? 27 | true 28 | end 29 | 30 | def pid 31 | self.pid_file ? File.read(self.pid_file).strip.to_i : self.watch.pid 32 | end 33 | 34 | def register 35 | pid = self.pid 36 | 37 | begin 38 | EventHandler.register(pid, :proc_exit) do |extra| 39 | formatted_extra = extra.size > 0 ? " #{extra.inspect}" : "" 40 | self.info = "process #{pid} exited#{formatted_extra}" 41 | self.watch.trigger(self) 42 | end 43 | 44 | msg = "#{self.watch.name} registered 'proc_exit' event for pid #{pid}" 45 | applog(self.watch, :info, msg) 46 | rescue StandardError 47 | raise EventRegistrationFailedError.new 48 | end 49 | end 50 | 51 | def deregister 52 | pid = self.pid 53 | if pid 54 | EventHandler.deregister(pid, :proc_exit) 55 | 56 | msg = "#{self.watch.name} deregistered 'proc_exit' event for pid #{pid}" 57 | applog(self.watch, :info, msg) 58 | else 59 | pid_file_location = self.pid_file || self.watch.pid_file 60 | applog(self.watch, :error, "#{self.watch.name} could not deregister: no cached PID or PID file #{pid_file_location} (#{self.base_name})") 61 | end 62 | end 63 | end 64 | 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/test_event_handler.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | module God 4 | class EventHandler 5 | 6 | def self.actions=(value) 7 | @@actions = value 8 | end 9 | 10 | def self.actions 11 | @@actions 12 | end 13 | 14 | def self.handler=(value) 15 | @@handler = value 16 | end 17 | end 18 | end 19 | 20 | class TestEventHandler < Test::Unit::TestCase 21 | def setup 22 | @h = God::EventHandler 23 | end 24 | 25 | def test_register_one_event 26 | pid = 4445 27 | event = :proc_exit 28 | block = lambda { 29 | puts "Hi" 30 | } 31 | 32 | mock_handler = mock() 33 | mock_handler.expects(:register_process).with(pid, [event]) 34 | @h.handler = mock_handler 35 | 36 | @h.register(pid, event, &block) 37 | assert_equal @h.actions, {pid => {event => block}} 38 | end 39 | 40 | def test_register_multiple_events_per_process 41 | pid = 4445 42 | exit_block = lambda { puts "Hi" } 43 | @h.actions = {pid => {:proc_exit => exit_block}} 44 | 45 | mock_handler = mock() 46 | mock_handler.expects(:register_process).with do |a, b| 47 | a == pid && 48 | b.to_set == [:proc_exit, :proc_fork].to_set 49 | end 50 | @h.handler = mock_handler 51 | 52 | fork_block = lambda { puts "Forking" } 53 | @h.register(pid, :proc_fork, &fork_block) 54 | assert_equal @h.actions, {pid => {:proc_exit => exit_block, 55 | :proc_fork => fork_block }} 56 | end 57 | 58 | # JIRA PLATFORM-75 59 | def test_call_should_check_for_pid_and_action_before_executing 60 | exit_block = mock() 61 | exit_block.expects(:call).times 1 62 | @h.actions = {4445 => {:proc_exit => exit_block}} 63 | @h.call(4446, :proc_exit) # shouldn't call, bad pid 64 | @h.call(4445, :proc_fork) # shouldn't call, bad event 65 | @h.call(4445, :proc_exit) # should call 66 | end 67 | 68 | def teardown 69 | # Reset handler 70 | @h.actions = {} 71 | @h.load 72 | end 73 | end 74 | 75 | # This doesn't currently work: 76 | # 77 | # class TestEventHandlerOperational < Test::Unit::TestCase 78 | # def test_operational 79 | # God::EventHandler.start 80 | # assert God::EventHandler.loaded? 81 | # end 82 | # end 83 | -------------------------------------------------------------------------------- /ideas/future.god: -------------------------------------------------------------------------------- 1 | # This example shows how you might keep a local development Rails server up 2 | # and running on your Mac. 3 | 4 | # Run with: 5 | # god -c /path/to/events.god 6 | 7 | RAILS_ROOT = "/Users/tom/dev/helloworld" 8 | 9 | God::Contacts::Email.delivery_method = :smtp 10 | God::Contacts::Email.server_settings = {} 11 | 12 | God.contact(:email) do |c| 13 | c.name = 'tom' 14 | c.email = 'tom@powerset.com' 15 | c.group = 'developers' 16 | c.throttle = 30.minutes 17 | end 18 | 19 | God.watch do |w| 20 | w.name = "local-3000" 21 | w.interval = 5.seconds 22 | w.start = "mongrel_rails start -P ./log/mongrel.pid -c #{RAILS_ROOT} -d" 23 | w.stop = "mongrel_rails stop -P ./log/mongrel.pid -c #{RAILS_ROOT}" 24 | w.pid_file = File.join(RAILS_ROOT, "log/mongrel.pid") 25 | 26 | # clean pid files before start if necessary 27 | w.behavior(:clean_pid_file) 28 | 29 | # determine the state on startup 30 | w.transition(:init, { true => :up, false => :start }) do |on| 31 | on.condition(:process_running) do |c| 32 | c.running = true 33 | end 34 | end 35 | 36 | # determine when process has finished starting 37 | w.transition([:start, :restart], :up) do |on| 38 | on.condition(:process_running) do |c| 39 | c.running = true 40 | end 41 | end 42 | 43 | # start if process is not running 44 | w.transition(:up, :start) do |on| 45 | on.condition(:process_exits) 46 | end 47 | 48 | # restart if memory or cpu is too high 49 | w.transition(:up, :restart) do |on| 50 | on.condition(:memory_usage) do |c| 51 | c.interval = 20 52 | c.above = 50.megabytes 53 | c.times = [3, 5] 54 | end 55 | 56 | on.condition(:cpu_usage) do |c| 57 | c.interval = 10 58 | c.above = 10.percent 59 | c.times = [3, 5] 60 | end 61 | end 62 | 63 | # lifecycle 64 | w.lifecycle do |on| 65 | on.condition(:flapping) do |c| 66 | c.to_state = [:start, :restart] 67 | c.times = 5 68 | c.within = 30.seconds 69 | c.transition = nil 70 | c.retry = 60.seconds 71 | c.giveup_tries = 5 72 | c.notify = 'tom' 73 | end 74 | 75 | on.condition(:memory_usage) do |c| 76 | c.interval = 20 77 | c.above = 40.megabytes 78 | c.times = [3, 5] 79 | c.notify = 'tom' 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/god/conditions/complex.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module Conditions 3 | 4 | class Complex < PollCondition 5 | AND = 0x1 6 | OR = 0x2 7 | NOT = 0x4 8 | 9 | def initialize() 10 | super 11 | 12 | @oper_stack = [] 13 | @op_stack = [] 14 | 15 | @this = nil 16 | end 17 | 18 | def valid? 19 | @oper_stack.inject(true) { |acc, oper| acc & oper.valid? } 20 | end 21 | 22 | def prepare 23 | @oper_stack.each { |oper| oper.prepare } 24 | end 25 | 26 | def new_oper(kind, op) 27 | oper = Condition.generate(kind, self.watch) 28 | @oper_stack.push(oper) 29 | @op_stack.push(op) 30 | oper 31 | end 32 | 33 | def this(kind) 34 | @this = Condition.generate(kind, self.watch) 35 | yield @this if block_given? 36 | end 37 | 38 | def and(kind) 39 | oper = new_oper(kind, 0x1) 40 | yield oper if block_given? 41 | end 42 | 43 | def and_not(kind) 44 | oper = new_oper(kind, 0x5) 45 | yield oper if block_given? 46 | end 47 | 48 | def or(kind) 49 | oper = new_oper(kind, 0x2) 50 | yield oper if block_given? 51 | end 52 | 53 | def or_not(kind) 54 | oper = new_oper(kind, 0x6) 55 | yield oper if block_given? 56 | end 57 | 58 | def test 59 | if @this.nil? 60 | # Although this() makes sense semantically and therefore 61 | # encourages easy-to-read conditions, being able to omit it 62 | # allows for more DRY code in some cases, so we deal with a 63 | # nil @this here by initially setting res to true or false, 64 | # depending on whether the first operator used is AND or OR 65 | # respectively. 66 | if 0 < @op_stack[0] & AND 67 | res = true 68 | else 69 | res = false 70 | end 71 | else 72 | res = @this.test 73 | end 74 | 75 | @op_stack.each do |op| 76 | cond = @oper_stack.shift 77 | eval "res " + ((0 < op & AND) ? "&&" : "||") + "= " + ((0 < op & NOT) ? "!" : "") + "cond.test" 78 | @oper_stack.push cond 79 | end 80 | 81 | res 82 | end 83 | end 84 | 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/god/conditions/cpu_usage.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module Conditions 3 | 4 | # Condition Symbol :cpu_usage 5 | # Type: Poll 6 | # 7 | # Trigger when the percent of CPU use of a process is above a specified limit. 8 | # On multi-core systems, this number could conceivably be above 100. 9 | # 10 | # Paramaters 11 | # Required 12 | # +pid_file+ is the pid file of the process in question. Automatically 13 | # populated for Watches. 14 | # +above+ is the percent CPU above which to trigger the condition. You 15 | # may use #percent to clarify this amount (see examples). 16 | # 17 | # Examples 18 | # 19 | # Trigger if the process is using more than 25 percent of the cpu (from a Watch): 20 | # 21 | # on.condition(:cpu_usage) do |c| 22 | # c.above = 25.percent 23 | # end 24 | # 25 | # Non-Watch Tasks must specify a PID file: 26 | # 27 | # on.condition(:cpu_usage) do |c| 28 | # c.above = 25.percent 29 | # c.pid_file = "/var/run/mongrel.3000.pid" 30 | # end 31 | class CpuUsage < PollCondition 32 | attr_accessor :above, :times, :pid_file 33 | 34 | def initialize 35 | super 36 | self.above = nil 37 | self.times = [1, 1] 38 | end 39 | 40 | def prepare 41 | if self.times.kind_of?(Integer) 42 | self.times = [self.times, self.times] 43 | end 44 | 45 | @timeline = Timeline.new(self.times[1]) 46 | end 47 | 48 | def reset 49 | @timeline.clear 50 | end 51 | 52 | def pid 53 | self.pid_file ? File.read(self.pid_file).strip.to_i : self.watch.pid 54 | end 55 | 56 | def valid? 57 | valid = true 58 | valid &= complain("Attribute 'pid_file' must be specified", self) if self.pid_file.nil? && self.watch.pid_file.nil? 59 | valid &= complain("Attribute 'above' must be specified", self) if self.above.nil? 60 | valid 61 | end 62 | 63 | def test 64 | process = System::Process.new(self.pid) 65 | @timeline.push(process.percent_cpu) 66 | self.info = [] 67 | 68 | history = "[" + @timeline.map { |x| "#{x > self.above ? '*' : ''}#{x}%%" }.join(", ") + "]" 69 | 70 | if @timeline.select { |x| x > self.above }.size >= self.times.first 71 | self.info = "cpu out of bounds #{history}" 72 | return true 73 | else 74 | return false 75 | end 76 | end 77 | end 78 | 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /examples/events.god: -------------------------------------------------------------------------------- 1 | # This example shows how you might keep a local development Rails server up 2 | # and running on your Mac. 3 | 4 | # Run with: 5 | # god -c /path/to/events.god 6 | 7 | RAILS_ROOT = ENV['GOD_TEST_RAILS_ROOT'] 8 | 9 | %w{3002}.each do |port| 10 | God.watch do |w| 11 | w.name = "local-#{port}" 12 | w.interval = 5.seconds 13 | w.start = "mongrel_rails start -p #{port} -P #{RAILS_ROOT}/log/mongrel.#{port}.pid -c #{RAILS_ROOT} -d" 14 | w.stop = "mongrel_rails stop -P #{RAILS_ROOT}/log/mongrel.#{port}.pid -c #{RAILS_ROOT}" 15 | w.pid_file = File.join(RAILS_ROOT, "log/mongrel.#{port}.pid") 16 | w.log = File.join(RAILS_ROOT, "log/commands.#{port}.log") 17 | 18 | # clean pid files before start if necessary 19 | w.behavior(:clean_pid_file) 20 | 21 | # determine the state on startup 22 | w.transition(:init, { true => :up, false => :start }) do |on| 23 | on.condition(:process_running) do |c| 24 | c.running = true 25 | end 26 | end 27 | 28 | # determine when process has finished starting 29 | w.transition([:start, :restart], :up) do |on| 30 | on.condition(:process_running) do |c| 31 | c.running = true 32 | end 33 | 34 | # failsafe 35 | on.condition(:tries) do |c| 36 | c.times = 8 37 | c.within = 2.minutes 38 | c.transition = :start 39 | end 40 | end 41 | 42 | # start if process is not running 43 | w.transition(:up, :start) do |on| 44 | on.condition(:process_exits) 45 | end 46 | 47 | # restart if memory or cpu is too high 48 | w.transition(:up, :restart) do |on| 49 | on.condition(:memory_usage) do |c| 50 | c.interval = 20 51 | c.above = 50.megabytes 52 | c.times = [3, 5] 53 | end 54 | 55 | on.condition(:cpu_usage) do |c| 56 | c.interval = 10 57 | c.above = 10.percent 58 | c.times = 5 59 | end 60 | 61 | on.condition(:http_response_code) do |c| 62 | c.host = 'localhost' 63 | c.port = port 64 | c.path = '/' 65 | c.code_is = 500 66 | c.timeout = 10.seconds 67 | c.times = [3, 5] 68 | end 69 | end 70 | 71 | # lifecycle 72 | w.lifecycle do |on| 73 | on.condition(:flapping) do |c| 74 | c.to_state = [:start, :restart] 75 | c.times = 5 76 | c.within = 1.minute 77 | c.transition = :unmonitored 78 | c.retry_in = 10.minutes 79 | c.retry_times = 5 80 | c.retry_within = 2.hours 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/god/conditions/memory_usage.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module Conditions 3 | 4 | # Condition Symbol :memory_usage 5 | # Type: Poll 6 | # 7 | # Trigger when the resident memory of a process is above a specified limit. 8 | # 9 | # Paramaters 10 | # Required 11 | # +pid_file+ is the pid file of the process in question. Automatically 12 | # populated for Watches. 13 | # +above+ is the amount of resident memory (in kilobytes) above which 14 | # the condition should trigger. You can also use the sugar 15 | # methods #kilobytes, #megabytes, and #gigabytes to clarify 16 | # this amount (see examples). 17 | # 18 | # Examples 19 | # 20 | # Trigger if the process is using more than 100 megabytes of resident 21 | # memory (from a Watch): 22 | # 23 | # on.condition(:memory_usage) do |c| 24 | # c.above = 100.megabytes 25 | # end 26 | # 27 | # Non-Watch Tasks must specify a PID file: 28 | # 29 | # on.condition(:memory_usage) do |c| 30 | # c.above = 100.megabytes 31 | # c.pid_file = "/var/run/mongrel.3000.pid" 32 | # end 33 | class MemoryUsage < PollCondition 34 | attr_accessor :above, :times, :pid_file 35 | 36 | def initialize 37 | super 38 | self.above = nil 39 | self.times = [1, 1] 40 | end 41 | 42 | def prepare 43 | if self.times.kind_of?(Integer) 44 | self.times = [self.times, self.times] 45 | end 46 | 47 | @timeline = Timeline.new(self.times[1]) 48 | end 49 | 50 | def reset 51 | @timeline.clear 52 | end 53 | 54 | def pid 55 | self.pid_file ? File.read(self.pid_file).strip.to_i : self.watch.pid 56 | end 57 | 58 | def valid? 59 | valid = true 60 | valid &= complain("Attribute 'pid_file' must be specified", self) if self.pid_file.nil? && self.watch.pid_file.nil? 61 | valid &= complain("Attribute 'above' must be specified", self) if self.above.nil? 62 | valid 63 | end 64 | 65 | def test 66 | process = System::Process.new(self.pid) 67 | @timeline.push(process.memory) 68 | self.info = [] 69 | 70 | history = "[" + @timeline.map { |x| "#{x > self.above ? '*' : ''}#{x}kb" }.join(", ") + "]" 71 | 72 | if @timeline.select { |x| x > self.above }.size >= self.times.first 73 | self.info = "memory out of bounds #{history}" 74 | return true 75 | else 76 | return false 77 | end 78 | end 79 | end 80 | 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /site/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | God - A Process Monitoring Framework in Ruby 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 |
18 | 21 | 22 | 30 | 31 |
32 |

A Better Way to Monitor

33 |

God is an easy to configure, easy to extend monitoring framework written in Ruby.

34 |

Keeping your server processes and tasks running should be a simple part of your deployment process. God aims to be the simplest, most powerful monitoring application available.

35 |

Tom Preston-Werner
tom@mojombo.com

36 |

Google Group: http://groups.google.com/group/god-rb

37 |
38 | 39 |
40 |

Features

41 |
    42 |
  • Config file is written in Ruby
  • 43 |
  • Easily write your own custom conditions in Ruby
  • 44 |
  • Supports both poll and event based conditions
  • 45 |
  • Different poll conditions can have different intervals
  • 46 |
  • Integrated notification system (write your own too!)
  • 47 |
  • Easily control non-daemonizing scripts
  • 48 |
49 |
50 | 51 |
52 | {{ content }} 53 |
54 | 55 |
56 | 57 | 60 | 61 | 63 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /lib/god/metric.rb: -------------------------------------------------------------------------------- 1 | module God 2 | # Metrics are responsible for holding watch conditions. An instance of 3 | # Metric is yielded to blocks in the start_if, restart_if, stop_if, and 4 | # transition methods. 5 | class Metric 6 | # The Watch. 7 | attr_accessor :watch 8 | 9 | # The destination Hash in canonical hash form. Example: 10 | # { true => :up, false => :restart} 11 | attr_accessor :destination 12 | 13 | # The Array of Condition instances. 14 | attr_accessor :conditions 15 | 16 | # Initialize a new Metric. 17 | # 18 | # watch - The Watch. 19 | # destination - The optional destination Hash in canonical hash form. 20 | def initialize(watch, destination = nil) 21 | self.watch = watch 22 | self.destination = destination 23 | self.conditions = [] 24 | end 25 | 26 | # Public: Instantiate the given Condition and pass it into the optional 27 | # block. Attributes of the condition must be set in the config file. 28 | # 29 | # kind - The Symbol name of the condition. 30 | # 31 | # Returns nothing. 32 | def condition(kind) 33 | # Create the condition. 34 | begin 35 | c = Condition.generate(kind, self.watch) 36 | rescue NoSuchConditionError => e 37 | abort e.message 38 | end 39 | 40 | # Send to block so config can set attributes. 41 | yield(c) if block_given? 42 | 43 | # Prepare the condition. 44 | c.prepare 45 | 46 | # Test generic and specific validity. 47 | unless Condition.valid?(c) && c.valid? 48 | abort "Exiting on invalid condition" 49 | end 50 | 51 | # Inherit interval from watch if no poll condition specific interval was 52 | # set. 53 | if c.kind_of?(PollCondition) && !c.interval 54 | if self.watch.interval 55 | c.interval = self.watch.interval 56 | else 57 | abort "No interval set for Condition '#{c.class.name}' in Watch " + 58 | "'#{self.watch.name}', and no default Watch interval from " + 59 | "which to inherit." 60 | end 61 | end 62 | 63 | # Add the condition to the list. 64 | self.conditions << c 65 | end 66 | 67 | # Enable all of this Metric's conditions. Poll conditions will be 68 | # scheduled and event/trigger conditions will be registered. 69 | # 70 | # Returns nothing. 71 | def enable 72 | self.conditions.each do |c| 73 | self.watch.attach(c) 74 | end 75 | end 76 | 77 | # Disable all of this Metric's conditions. Poll conditions will be 78 | # halted and event/trigger conditions will be deregistered. 79 | # 80 | # Returns nothing. 81 | def disable 82 | self.conditions.each do |c| 83 | self.watch.detach(c) 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/god/contacts/jabber.rb: -------------------------------------------------------------------------------- 1 | # Send a notice to a Jabber address. 2 | # 3 | # host - The String hostname of the Jabber server. 4 | # port - The Integer port of the Jabber server (default: 5222). 5 | # from_jid - The String Jabber ID of the sender. 6 | # password - The String password of the sender. 7 | # to_jid - The String Jabber ID of the recipient. 8 | # subject - The String subject of the message (default: "God Notification"). 9 | 10 | CONTACT_DEPS[:jabber] = ['xmpp4r'] 11 | CONTACT_DEPS[:jabber].each do |d| 12 | require d 13 | end 14 | 15 | module God 16 | module Contacts 17 | 18 | class Jabber < Contact 19 | class << self 20 | attr_accessor :host, :port, :from_jid, :password, :to_jid, :subject 21 | attr_accessor :format 22 | end 23 | 24 | self.port = 5222 25 | self.subject = 'God Notification' 26 | 27 | self.format = lambda do |message, time, priority, category, host| 28 | text = "Message: #{message}\n" 29 | text += "Host: #{host}\n" if host 30 | text += "Priority: #{priority}\n" if priority 31 | text += "Category: #{category}\n" if category 32 | text 33 | end 34 | 35 | attr_accessor :host, :port, :from_jid, :password, :to_jid, :subject 36 | 37 | def valid? 38 | valid = true 39 | valid &= complain("Attribute 'host' must be specified", self) unless arg(:host) 40 | valid &= complain("Attribute 'port' must be specified", self) unless arg(:port) 41 | valid &= complain("Attribute 'from_jid' must be specified", self) unless arg(:from_jid) 42 | valid &= complain("Attribute 'to_jid' must be specified", self) unless arg(:to_jid) 43 | valid &= complain("Attribute 'password' must be specified", self) unless arg(:password) 44 | valid 45 | end 46 | 47 | def notify(message, time, priority, category, host) 48 | body = Jabber.format.call(message, time, priority, category, host) 49 | 50 | message = ::Jabber::Message.new(arg(:to_jid), body) 51 | message.set_type(:normal) 52 | message.set_id('1') 53 | message.set_subject(arg(:subject)) 54 | 55 | jabber_id = ::Jabber::JID.new("#{arg(:from_jid)}/God") 56 | 57 | client = ::Jabber::Client.new(jabber_id) 58 | client.connect(arg(:host), arg(:port)) 59 | client.auth(arg(:password)) 60 | client.send(message) 61 | client.close 62 | 63 | self.info = "sent jabber message to #{self.to_jid}" 64 | rescue Object => e 65 | if e.respond_to?(:message) 66 | applog(nil, :info, "failed to send jabber message to #{arg(:to_jid)}: #{e.message}") 67 | else 68 | applog(nil, :info, "failed to send jabber message to #{arg(:to_jid)}: #{e.class}") 69 | end 70 | applog(nil, :debug, e.backtrace.join("\n")) 71 | end 72 | 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/god/event_handler.rb: -------------------------------------------------------------------------------- 1 | module God 2 | class EventHandler 3 | @@actions = {} 4 | @@handler = nil 5 | @@loaded = false 6 | 7 | def self.loaded? 8 | @@loaded 9 | end 10 | 11 | def self.event_system 12 | @@handler::EVENT_SYSTEM 13 | end 14 | 15 | def self.load 16 | begin 17 | case RUBY_PLATFORM 18 | when /darwin/i, /bsd/i 19 | require 'god/event_handlers/kqueue_handler' 20 | @@handler = KQueueHandler 21 | when /linux/i 22 | require 'god/event_handlers/netlink_handler' 23 | @@handler = NetlinkHandler 24 | else 25 | raise NotImplementedError, "Platform not supported for EventHandler" 26 | end 27 | @@loaded = true 28 | rescue Exception 29 | require 'god/event_handlers/dummy_handler' 30 | @@handler = DummyHandler 31 | @@loaded = false 32 | end 33 | end 34 | 35 | def self.register(pid, event, &block) 36 | @@actions[pid] ||= {} 37 | @@actions[pid][event] = block 38 | @@handler.register_process(pid, @@actions[pid].keys) 39 | end 40 | 41 | def self.deregister(pid, event) 42 | if watching_pid? pid 43 | running = ::Process.kill(0, pid.to_i) rescue false 44 | @@actions[pid].delete(event) 45 | @@handler.register_process(pid, @@actions[pid].keys) if running 46 | @@actions.delete(pid) if @@actions[pid].empty? 47 | end 48 | end 49 | 50 | def self.call(pid, event, extra_data = {}) 51 | @@actions[pid][event].call(extra_data) if watching_pid?(pid) && @@actions[pid][event] 52 | end 53 | 54 | def self.watching_pid?(pid) 55 | @@actions[pid] 56 | end 57 | 58 | def self.start 59 | @@thread = Thread.new do 60 | loop do 61 | begin 62 | @@handler.handle_events 63 | rescue Exception => e 64 | message = format("Unhandled exception (%s): %s\n%s", 65 | e.class, e.message, e.backtrace.join("\n")) 66 | applog(nil, :fatal, message) 67 | end 68 | end 69 | end 70 | 71 | # do a real test to make sure events are working properly 72 | @@loaded = self.operational? 73 | end 74 | 75 | def self.stop 76 | @@thread.kill if @@thread 77 | end 78 | 79 | def self.operational? 80 | com = [false] 81 | 82 | Thread.new do 83 | begin 84 | event_system = God::EventHandler.event_system 85 | 86 | pid = fork do 87 | loop { sleep(1) } 88 | end 89 | 90 | self.register(pid, :proc_exit) do 91 | com[0] = true 92 | end 93 | 94 | ::Process.kill('KILL', pid) 95 | ::Process.waitpid(pid) 96 | 97 | sleep(0.1) 98 | 99 | self.deregister(pid, :proc_exit) rescue nil 100 | rescue => e 101 | puts e.message 102 | puts e.backtrace.join("\n") 103 | end 104 | end.join 105 | 106 | sleep(0.1) 107 | 108 | com.first 109 | end 110 | 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/god/condition.rb: -------------------------------------------------------------------------------- 1 | module God 2 | 3 | class Condition < Behavior 4 | attr_accessor :transition, :notify, :info, :phase 5 | 6 | # Generate a Condition of the given kind. The proper class if found by camel casing the 7 | # kind (which is given as an underscored symbol). 8 | # +kind+ is the underscored symbol representing the class (e.g. :foo_bar for God::Conditions::FooBar) 9 | def self.generate(kind, watch) 10 | sym = kind.to_s.capitalize.gsub(/_(.)/){$1.upcase}.intern 11 | c = God::Conditions.const_get(sym).new 12 | 13 | unless c.kind_of?(PollCondition) || c.kind_of?(EventCondition) || c.kind_of?(TriggerCondition) 14 | abort "Condition '#{c.class.name}' must subclass God::PollCondition, God::EventCondition, or God::TriggerCondition" 15 | end 16 | 17 | if !EventHandler.loaded? && c.kind_of?(EventCondition) 18 | abort "Condition '#{c.class.name}' requires an event system but none has been loaded" 19 | end 20 | 21 | c.watch = watch 22 | c 23 | rescue NameError 24 | raise NoSuchConditionError.new("No Condition found with the class name God::Conditions::#{sym}") 25 | end 26 | 27 | def self.valid?(condition) 28 | valid = true 29 | if condition.notify 30 | begin 31 | Contact.normalize(condition.notify) 32 | rescue ArgumentError => e 33 | valid &= Configurable.complain("Attribute 'notify' " + e.message, condition) 34 | end 35 | end 36 | valid 37 | end 38 | 39 | # Construct the friendly name of this Condition, looks like: 40 | # 41 | # Condition FooBar on Watch 'baz' 42 | def friendly_name 43 | "Condition #{self.class.name.split('::').last} on Watch '#{self.watch.name}'" 44 | end 45 | end 46 | 47 | class PollCondition < Condition 48 | # all poll conditions can specify a poll interval 49 | attr_accessor :interval 50 | 51 | # Override this method in your Conditions (optional) 52 | def before 53 | end 54 | 55 | # Override this method in your Conditions (mandatory) 56 | # 57 | # Return true if the test passes (everything is ok) 58 | # Return false otherwise 59 | def test 60 | raise AbstractMethodNotOverriddenError.new("PollCondition#test must be overridden in subclasses") 61 | end 62 | 63 | # Override this method in your Conditions (optional) 64 | def after 65 | end 66 | end 67 | 68 | class EventCondition < Condition 69 | def register 70 | raise AbstractMethodNotOverriddenError.new("EventCondition#register must be overridden in subclasses") 71 | end 72 | 73 | def deregister 74 | raise AbstractMethodNotOverriddenError.new("EventCondition#deregister must be overridden in subclasses") 75 | end 76 | end 77 | 78 | class TriggerCondition < Condition 79 | def process(event, payload) 80 | raise AbstractMethodNotOverriddenError.new("TriggerCondition#process must be overridden in subclasses") 81 | end 82 | 83 | def trigger 84 | self.watch.trigger(self) 85 | end 86 | 87 | def register 88 | Trigger.register(self) 89 | end 90 | 91 | def deregister 92 | Trigger.deregister(self) 93 | end 94 | end 95 | 96 | end 97 | -------------------------------------------------------------------------------- /site/stylesheets/layout.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | font-size: 100%; 4 | } 5 | 6 | body { 7 | font: normal .8em/1.5em "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif; 8 | color: #484848; 9 | background: #E6EAE9 url(../images/bg_grey.gif); 10 | } 11 | 12 | a { 13 | color: #c75f3e; 14 | text-decoration: none; 15 | } 16 | 17 | a:hover, 18 | a:active { 19 | text-decoration: underline; 20 | } 21 | 22 | #mothership { 23 | width: 307px; 24 | height: 117px; 25 | margin: 0 auto; 26 | background: url(../images/god_logo.png); 27 | } 28 | 29 | #content { 30 | width: 700px; 31 | margin: 3px auto; 32 | background: white; 33 | border: 1px solid #444; 34 | padding: 0 24px; 35 | background: #f8f8ff; 36 | overflow: hidden; 37 | } 38 | 39 | .banner { 40 | margin-top: 24px; 41 | border: 1px solid #ddd; 42 | width: 698px; 43 | height: 150px; 44 | background: url(../images/banner.jpg); 45 | } 46 | 47 | #menu { 48 | margin-top: 5px; 49 | } 50 | 51 | #menu div.dots { 52 | background: url(../images/red_dot.gif) repeat; 53 | height: 5px; 54 | width: 700px; 55 | font-size: 0; 56 | } 57 | 58 | #menu ul { 59 | font-family: "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif; 60 | font-weight: bold; 61 | text-transform: uppercase; 62 | color: #4D4D4D; 63 | font-size: 12px; 64 | padding: 0; 65 | margin: 0; 66 | margin-top: 0 !important; 67 | margin-top: -2px; 68 | } 69 | 70 | #menu li { 71 | display: inline; 72 | margin: 0 30px 0 0; 73 | } 74 | 75 | #menu a:link, 76 | #menu a:visited { 77 | color: #4D4D4D; 78 | text-decoration: none; 79 | } 80 | 81 | #menu a:hover, 82 | #menu a:active { 83 | color: black; 84 | text-decoration: none; 85 | } 86 | 87 | #page_home #menu li.menu_home a { 88 | color: #A70000; 89 | } 90 | 91 | .columnleft { 92 | float: left; 93 | width: 345px; 94 | margin-bottom: 20px; 95 | } 96 | 97 | .columnleft p { 98 | text-align: justify; 99 | } 100 | 101 | .columnright { 102 | float: right; 103 | width: 325px; 104 | margin-bottom: 20px; 105 | } 106 | 107 | .main { 108 | clear: both; 109 | } 110 | 111 | h2 { 112 | font: bold 1.5em "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif; 113 | color: #f36e21; 114 | text-transform: uppercase; 115 | margin: 1.5em 0 .5em 0; 116 | } 117 | 118 | h3 { 119 | font: bold 1.25em "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif; 120 | color: #f36e21; 121 | margin: 1.5em 0 .5em 0; 122 | } 123 | 124 | p { 125 | margin-bottom: 1em; 126 | } 127 | 128 | ul { 129 | margin-bottom: 1em; 130 | } 131 | 132 | ul.features { 133 | padding: 0; 134 | margin-left: 1.5em !important; 135 | margin-left: 1.3em; 136 | } 137 | 138 | ul.features li { 139 | list-style-position: outside; 140 | list-style-type: circle; 141 | list-style-image: url(../images/bullet.jpg); 142 | line-height: 1.4em; 143 | } 144 | 145 | #footer { 146 | text-align: center; 147 | color: white; 148 | margin-bottom: 50px; 149 | } 150 | 151 | 152 | 153 | pre { 154 | line-height: 1.3; 155 | border: 1px solid #ccc; 156 | padding: 1em; 157 | background-color: #efefef; 158 | margin: 1em 0; 159 | font-size: 1.2em; 160 | } 161 | 162 | tt { 163 | font-size: 1.2em; 164 | } 165 | -------------------------------------------------------------------------------- /lib/god/logger.rb: -------------------------------------------------------------------------------- 1 | module God 2 | 3 | class Logger < SimpleLogger 4 | 5 | attr_accessor :logs 6 | 7 | class << self 8 | attr_accessor :syslog 9 | end 10 | 11 | self.syslog = defined?(Syslog) 12 | 13 | # Instantiate a new Logger object 14 | def initialize(io = $stdout) 15 | super(io) 16 | self.logs = {} 17 | @mutex = Mutex.new 18 | @capture = nil 19 | @spool = Time.now - 10 20 | @templogio = StringIO.new 21 | @templog = SimpleLogger.new(@templogio) 22 | @templog.level = Logger::INFO 23 | end 24 | 25 | 26 | def level=(lev) 27 | SysLogger.level = SimpleLogger::CONSTANT_TO_SYMBOL[lev] if Logger.syslog 28 | super(lev) 29 | end 30 | 31 | # Log a message 32 | # +watch+ is the String name of the Watch (may be nil if not Watch is applicable) 33 | # +level+ is the log level [:debug|:info|:warn|:error|:fatal] 34 | # +text+ is the String message 35 | # 36 | # Returns nothing 37 | def log(watch, level, text) 38 | # initialize watch log if necessary 39 | self.logs[watch.name] ||= Timeline.new(God::LOG_BUFFER_SIZE_DEFAULT) if watch 40 | 41 | # push onto capture and timeline for the given watch 42 | if @capture || (watch && (Time.now - @spool < 2)) 43 | @mutex.synchronize do 44 | @templogio.truncate(0) 45 | @templogio.rewind 46 | @templog.send(level, text) 47 | 48 | message = @templogio.string.dup 49 | 50 | if @capture 51 | @capture.puts(message) 52 | else 53 | self.logs[watch.name] << [Time.now, message] 54 | end 55 | end 56 | end 57 | 58 | # send to regular logger 59 | self.send(level, text) 60 | 61 | # send to syslog 62 | SysLogger.log(level, text) if Logger.syslog 63 | end 64 | 65 | # Get all log output for a given Watch since a certain Time. 66 | # +watch_name+ is the String name of the Watch 67 | # +since+ is the Time since which to fetch log lines 68 | # 69 | # Returns String 70 | def watch_log_since(watch_name, since) 71 | # initialize watch log if necessary 72 | self.logs[watch_name] ||= Timeline.new(God::LOG_BUFFER_SIZE_DEFAULT) 73 | 74 | # get and join lines since given time 75 | @mutex.synchronize do 76 | @spool = Time.now 77 | self.logs[watch_name].select do |x| 78 | x.first > since 79 | end.map do |x| 80 | x[1] 81 | end.join 82 | end 83 | end 84 | 85 | # private 86 | 87 | # Enable capturing of log 88 | # 89 | # Returns nothing 90 | def start_capture 91 | @mutex.synchronize do 92 | @capture = StringIO.new 93 | end 94 | end 95 | 96 | # Disable capturing of log and return what was captured since 97 | # capturing was enabled with Logger#start_capture 98 | # 99 | # Returns String 100 | def finish_capture 101 | @mutex.synchronize do 102 | cap = @capture.string if @capture 103 | @capture = nil 104 | cap 105 | end 106 | end 107 | end 108 | 109 | end 110 | -------------------------------------------------------------------------------- /lib/god/socket.rb: -------------------------------------------------------------------------------- 1 | require 'drb' 2 | 3 | module God 4 | 5 | # The God::Server oversees the DRb server which dishes out info on this God daemon. 6 | class Socket 7 | attr_reader :port 8 | 9 | # The location of the socket for a given port 10 | # +port+ is the port number 11 | # 12 | # Returns String (file location) 13 | def self.socket_file(port) 14 | "/tmp/god.#{port}.sock" 15 | end 16 | 17 | # The address of the socket for a given port 18 | # +port+ is the port number 19 | # 20 | # Returns String (drb address) 21 | def self.socket(port) 22 | "drbunix://#{self.socket_file(port)}" 23 | end 24 | 25 | # The location of the socket for this Server 26 | # 27 | # Returns String (file location) 28 | def socket_file 29 | self.class.socket_file(@port) 30 | end 31 | 32 | # The address of the socket for this Server 33 | # 34 | # Returns String (drb address) 35 | def socket 36 | self.class.socket(@port) 37 | end 38 | 39 | # Create a new Server and star the DRb server 40 | # +port+ is the port on which to start the DRb service (default nil) 41 | def initialize(port = nil, user = nil, group = nil, perm = nil) 42 | @port = port 43 | @user = user 44 | @group = group 45 | @perm = perm 46 | start 47 | end 48 | 49 | # Returns true 50 | def ping 51 | true 52 | end 53 | 54 | # Forward API calls to God 55 | # 56 | # Returns whatever the forwarded call returns 57 | def method_missing(*args, &block) 58 | God.send(*args, &block) 59 | end 60 | 61 | # Stop the DRb server and delete the socket file 62 | # 63 | # Returns nothing 64 | def stop 65 | DRb.stop_service 66 | FileUtils.rm_f(self.socket_file) 67 | end 68 | 69 | private 70 | 71 | # Start the DRb server. Abort if there is already a running god instance 72 | # on the socket. 73 | # 74 | # Returns nothing 75 | def start 76 | begin 77 | @drb ||= DRb.start_service(self.socket, self) 78 | applog(nil, :info, "Started on #{DRb.uri}") 79 | rescue Errno::EADDRINUSE 80 | applog(nil, :info, "Socket already in use") 81 | DRb.start_service 82 | server = DRbObject.new(nil, self.socket) 83 | 84 | begin 85 | Timeout.timeout(5) do 86 | server.ping 87 | end 88 | abort "Socket #{self.socket} already in use by another instance of god" 89 | rescue StandardError, Timeout::Error 90 | applog(nil, :info, "Socket is stale, reopening") 91 | File.delete(self.socket_file) rescue nil 92 | @drb ||= DRb.start_service(self.socket, self) 93 | applog(nil, :info, "Started on #{DRb.uri}") 94 | end 95 | end 96 | 97 | if File.exists?(self.socket_file) 98 | uid = Etc.getpwnam(@user).uid if @user 99 | gid = Etc.getgrnam(@group).gid if @group 100 | 101 | File.chmod(Integer(@perm), socket_file) if @perm 102 | File.chown(uid, gid, socket_file) if uid or gid 103 | end 104 | end 105 | end 106 | 107 | end 108 | -------------------------------------------------------------------------------- /lib/god/system/slash_proc_poller.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module System 3 | class SlashProcPoller < PortablePoller 4 | @@kb_per_page = 4 # TODO: Need to make this portable 5 | @@hertz = 100 6 | @@total_mem = nil 7 | 8 | MeminfoPath = '/proc/meminfo' 9 | UptimePath = '/proc/uptime' 10 | 11 | RequiredPaths = [MeminfoPath, UptimePath] 12 | 13 | # FreeBSD has /proc by default, but nothing mounted there! 14 | # So we should check for the actual required paths! 15 | # Returns true if +RequiredPaths+ are readable. 16 | def self.usable? 17 | RequiredPaths.all? do |path| 18 | test(?r, path) && readable?(path) 19 | end 20 | end 21 | 22 | def initialize(pid) 23 | super(pid) 24 | 25 | unless @@total_mem # in K 26 | File.open(MeminfoPath) do |f| 27 | @@total_mem = f.gets.split[1] 28 | end 29 | end 30 | end 31 | 32 | def memory 33 | stat[:rss].to_i * @@kb_per_page 34 | rescue # This shouldn't fail is there's an error (or proc doesn't exist) 35 | 0 36 | end 37 | 38 | def percent_memory 39 | (memory / @@total_mem.to_f) * 100 40 | rescue # This shouldn't fail is there's an error (or proc doesn't exist) 41 | 0 42 | end 43 | 44 | # TODO: Change this to calculate the wma instead 45 | def percent_cpu 46 | stats = stat 47 | total_time = stats[:utime].to_i + stats[:stime].to_i # in jiffies 48 | seconds = uptime - stats[:starttime].to_i / @@hertz 49 | if seconds == 0 50 | 0 51 | else 52 | ((total_time * 1000 / @@hertz) / seconds) / 10 53 | end 54 | rescue # This shouldn't fail is there's an error (or proc doesn't exist) 55 | 0 56 | end 57 | 58 | private 59 | 60 | # Some systems (CentOS?) have a /proc, but they can hang when trying to 61 | # read from them. Try to use this sparingly as it is expensive. 62 | def self.readable?(path) 63 | begin 64 | timeout(1) { File.read(path) } 65 | rescue Timeout::Error 66 | false 67 | end 68 | end 69 | 70 | # in seconds 71 | def uptime 72 | File.read(UptimePath).split[0].to_f 73 | end 74 | 75 | def stat 76 | stats = {} 77 | stats[:pid], stats[:comm], stats[:state], stats[:ppid], stats[:pgrp], 78 | stats[:session], stats[:tty_nr], stats[:tpgid], stats[:flags], 79 | stats[:minflt], stats[:cminflt], stats[:majflt], stats[:cmajflt], 80 | stats[:utime], stats[:stime], stats[:cutime], stats[:cstime], 81 | stats[:priority], stats[:nice], _, stats[:itrealvalue], 82 | stats[:starttime], stats[:vsize], stats[:rss], stats[:rlim], 83 | stats[:startcode], stats[:endcode], stats[:startstack], stats[:kstkesp], 84 | stats[:kstkeip], stats[:signal], stats[:blocked], stats[:sigignore], 85 | stats[:sigcatch], stats[:wchan], stats[:nswap], stats[:cnswap], 86 | stats[:exit_signal], stats[:processor], stats[:rt_priority], 87 | stats[:policy] = File.read("/proc/#{@pid}/stat").scan(/\(.*?\)|\w+/) 88 | stats 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/test_contact.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestContact < Test::Unit::TestCase 4 | def test_exists 5 | God::Contact 6 | end 7 | 8 | # generate 9 | 10 | def test_generate_should_raise_on_invalid_kind 11 | assert_raise(NoSuchContactError) do 12 | Contact.generate(:invalid) 13 | end 14 | end 15 | 16 | def test_generate_should_abort_on_invalid_contact 17 | assert_abort do 18 | Contact.generate(:invalid_contact) 19 | end 20 | end 21 | 22 | # normalize 23 | 24 | def test_normalize_should_accept_a_string 25 | input = 'tom' 26 | output = {:contacts => ['tom']} 27 | assert_equal(output, Contact.normalize(input)) 28 | end 29 | 30 | def test_normalize_should_accept_an_array_of_strings 31 | input = ['tom', 'kevin'] 32 | output = {:contacts => ['tom', 'kevin']} 33 | assert_equal(output, Contact.normalize(input)) 34 | end 35 | 36 | def test_normalize_should_accept_a_hash_with_contacts_string 37 | input = {:contacts => 'tom'} 38 | output = {:contacts => ['tom']} 39 | assert_equal(output, Contact.normalize(input)) 40 | end 41 | 42 | def test_normalize_should_accept_a_hash_with_contacts_array_of_strings 43 | input = {:contacts => ['tom', 'kevin']} 44 | output = {:contacts => ['tom', 'kevin']} 45 | assert_equal(output, Contact.normalize(input)) 46 | end 47 | 48 | def test_normalize_should_stringify_priority 49 | input = {:contacts => 'tom', :priority => 1} 50 | output = {:contacts => ['tom'], :priority => '1'} 51 | assert_equal(output, Contact.normalize(input)) 52 | end 53 | 54 | def test_normalize_should_stringify_category 55 | input = {:contacts => 'tom', :category => :product} 56 | output = {:contacts => ['tom'], :category => 'product'} 57 | assert_equal(output, Contact.normalize(input)) 58 | end 59 | 60 | def test_normalize_should_raise_on_non_string_array_hash 61 | input = 1 62 | assert_raise ArgumentError do 63 | Contact.normalize(input) 64 | end 65 | end 66 | 67 | def test_normalize_should_raise_on_non_string_array_contacts_key 68 | input = {:contacts => 1} 69 | assert_raise ArgumentError do 70 | Contact.normalize(input) 71 | end 72 | end 73 | 74 | def test_normalize_should_raise_on_non_string_containing_array 75 | input = [1] 76 | assert_raise ArgumentError do 77 | Contact.normalize(input) 78 | end 79 | end 80 | 81 | def test_normalize_should_raise_on_non_string_containing_array_contacts_key 82 | input = {:contacts => [1]} 83 | assert_raise ArgumentError do 84 | Contact.normalize(input) 85 | end 86 | end 87 | 88 | def test_normalize_should_raise_on_absent_contacts_key 89 | input = {} 90 | assert_raise ArgumentError do 91 | Contact.normalize(input) 92 | end 93 | end 94 | 95 | def test_normalize_should_raise_on_extra_keys 96 | input = {:contacts => ['tom'], :priority => 1, :category => 'product', :extra => 'foo'} 97 | assert_raise ArgumentError do 98 | Contact.normalize(input) 99 | end 100 | end 101 | 102 | # notify 103 | 104 | def test_notify_should_be_abstract 105 | assert_raise(AbstractMethodNotOverriddenError) do 106 | Contact.new.notify(:a, :b, :c, :d, :e) 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/god/contacts/slack.rb: -------------------------------------------------------------------------------- 1 | # Send a message to a Slack channel 2 | # 3 | # account - The name of your Slack account (visible in URL, e.g. foo.slack.com) 4 | # token - The token of the webhook created in Slack 5 | # channel - The name of the channel to send the message to, prefixed with # 6 | # notify_channel - Whether to send an "@channel" in the message, to alert everyone in the channel 7 | # format - An optional format string to change how the alert is displayed 8 | 9 | require 'net/http' 10 | require 'uri' 11 | 12 | CONTACT_DEPS[:slack] = ['json'] 13 | CONTACT_DEPS[:slack].each do |d| 14 | require d 15 | end 16 | 17 | module God 18 | module Contacts 19 | 20 | class Slack < Contact 21 | class << self 22 | attr_accessor :account, :token, :channel, :notify_channel, :format, :username, :emoji 23 | end 24 | 25 | self.channel = "#general" 26 | self.notify_channel = false 27 | self.format = "%{priority} alert on %{host}: %{message} (%{category}, %{time})" 28 | 29 | def valid? 30 | valid = true 31 | valid &= complain("Attribute 'account' must be specified", self) unless arg(:account) 32 | valid &= complain("Attribute 'token' must be specified", self) unless arg(:token) 33 | valid 34 | end 35 | 36 | attr_accessor :account, :token, :channel, :notify_channel, :format, :username, :emoji 37 | 38 | def text(data) 39 | text = "" 40 | text << " " if arg(:notify_channel) 41 | 42 | if RUBY_VERSION =~ /^1\.8/ 43 | text << arg(:format).gsub(/%\{(\w+)\}/) do |match| 44 | data[$1.to_sym] 45 | end 46 | else 47 | text << arg(:format) % data 48 | end 49 | 50 | text 51 | end 52 | 53 | def notify(message, time, priority, category, host) 54 | text = text({ 55 | :message => message, 56 | :time => time, 57 | :priority => priority, 58 | :category => category, 59 | :host => host 60 | }) 61 | 62 | request(text) 63 | end 64 | 65 | def api_url 66 | URI.parse("https://#{arg(:account)}.slack.com/services/hooks/incoming-webhook?token=#{arg(:token)}") 67 | end 68 | 69 | def request(text) 70 | http = Net::HTTP.new(api_url.host, api_url.port) 71 | http.use_ssl = true 72 | 73 | req = Net::HTTP::Post.new(api_url.request_uri) 74 | req.body = { 75 | :link_names => 1, 76 | :text => text, 77 | :channel => arg(:channel) 78 | }.tap { |payload| 79 | payload[:username] = arg(:username) if arg(:username) 80 | payload[:icon_emoji] = arg(:emoji) if arg(:emoji) 81 | }.to_json 82 | 83 | res = http.request(req) 84 | 85 | case res 86 | when Net::HTTPSuccess 87 | self.info = "successfully notified slack on channel #{arg(:channel)}" 88 | else 89 | self.info = "failed to send webhook to #{arg(:url)}: #{res.error!}" 90 | end 91 | rescue Object => e 92 | applog(nil, :info, "failed to send webhook to #{arg(:url)}: #{e.message}") 93 | applog(nil, :debug, e.backtrace.join("\n")) 94 | end 95 | 96 | end 97 | 98 | end 99 | end 100 | 101 | -------------------------------------------------------------------------------- /test/configs/contact/contact.god: -------------------------------------------------------------------------------- 1 | # God::Contacts::Campfire.defaults do |d| 2 | # d.subdomain = 'github' 3 | # d.token = '9fb768e421975cc1c6ff3f4f8306f890cb46e24f' 4 | # d.room = 'Notices' 5 | # d.ssl = true 6 | # end 7 | # 8 | # God.contact(:campfire) do |c| 9 | # c.name = 'tom4' 10 | # end 11 | 12 | # God::Contacts::Hipchat.defaults do |d| 13 | # d.token = '9fb768e421975cc1c6ff3f4f8306f890cb46e24f' 14 | # d.room = 'Notices' 15 | # d.ssl = true 16 | # end 17 | # 18 | # God.contact(:hipchat) do |c| 19 | # c.name = 'hip1' 20 | # end 21 | 22 | # God.contact(:email) do |c| 23 | # c.name = 'tom' 24 | # c.group = 'developers' 25 | # c.to_email = 'tom@lepton.local' 26 | # c.from_email = 'god@github.com' 27 | # c.from_name = 'God' 28 | # c.delivery_method = :sendmail 29 | # end 30 | 31 | # God.contact(:email) do |c| 32 | # c.name = 'tom' 33 | # c.group = 'developers' 34 | # c.to_email = 'tom@mojombo.com' 35 | # c.from_email = 'god@github.com' 36 | # c.from_name = 'God' 37 | # c.server_host = 'smtp.rs.github.com' 38 | # end 39 | 40 | # God.contact(:prowl) do |c| 41 | # c.name = 'tom3' 42 | # c.apikey = 'f0fc8e1f3121672686337a631527eac2f1b6031c' 43 | # c.group = 'developers' 44 | # end 45 | 46 | # God.contact(:twitter) do |c| 47 | # c.name = 'tom6' 48 | # c.consumer_token = 'gOhjax6s0L3mLeaTtBWPw' 49 | # c.consumer_secret = 'yz4gpAVXJHKxvsGK85tEyzQJ7o2FEy27H1KEWL75jfA' 50 | # c.access_token = '17376380-qS391nCrgaP4HKXAmZtM38gB56xUXMhx1NYbjT6mQ' 51 | # c.access_secret = 'uMwCDeU4OXlEBWFQBc3KwGyY8OdWCtAV0Jg5KVB0' 52 | # end 53 | 54 | # God.contact(:scout) do |c| 55 | # c.name = 'tom5' 56 | # c.client_key = '583a51b5-acbc-2421-a830-b6f3f8e4b04e' 57 | # c.plugin_id = '230641' 58 | # end 59 | 60 | # God.contact(:webhook) do |c| 61 | # c.name = 'tom' 62 | # c.url = "http://www.postbin.org/wk7guh" 63 | # end 64 | 65 | # God.contact(:jabber) do |c| 66 | # c.name = 'tom' 67 | # c.host = 'talk.google.com' 68 | # c.to_jid = 'mojombo@jabber.org' 69 | # c.from_jid = 'mojombo@gmail.com' 70 | # c.password = 'secret' 71 | # end 72 | 73 | God.watch do |w| 74 | w.name = "contact" 75 | w.interval = 5.seconds 76 | w.start = "ruby " + File.join(File.dirname(__FILE__), *%w[simple_server.rb]) 77 | w.log = "/Users/tom/contact.log" 78 | 79 | # determine the state on startup 80 | w.transition(:init, { true => :up, false => :start }) do |on| 81 | on.condition(:process_running) do |c| 82 | c.running = true 83 | end 84 | end 85 | 86 | # determine when process has finished starting 87 | w.transition([:start, :restart], :up) do |on| 88 | on.condition(:process_running) do |c| 89 | c.running = true 90 | end 91 | 92 | # failsafe 93 | on.condition(:tries) do |c| 94 | c.times = 2 95 | c.transition = :start 96 | end 97 | end 98 | 99 | # start if process is not running 100 | w.transition(:up, :start) do |on| 101 | on.condition(:process_exits) do |c| 102 | c.notify = {:contacts => ['tom'], :priority => 1, :category => 'product'} 103 | end 104 | end 105 | 106 | # lifecycle 107 | w.lifecycle do |on| 108 | on.condition(:flapping) do |c| 109 | c.to_state = [:start, :restart] 110 | c.times = 5 111 | c.within = 20.seconds 112 | c.transition = :unmonitored 113 | c.retry_in = 10.seconds 114 | c.retry_times = 2 115 | c.retry_within = 5.minutes 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /ext/god/kqueue_handler.c: -------------------------------------------------------------------------------- 1 | #if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | static VALUE mGod; 9 | static VALUE cKQueueHandler; 10 | static VALUE cEventHandler; 11 | 12 | static ID proc_exit; 13 | static ID proc_fork; 14 | static ID m_call; 15 | static ID m_size; 16 | static ID m_deregister; 17 | 18 | static int kq; 19 | int num_events; 20 | 21 | #define NUM_EVENTS FIX2INT(rb_funcall(rb_cv_get(cEventHandler, "@@actions"), m_size, 0)) 22 | 23 | VALUE 24 | kqh_event_mask(VALUE klass, VALUE sym) 25 | { 26 | ID id = SYM2ID(sym); 27 | if (proc_exit == id) { 28 | return UINT2NUM(NOTE_EXIT); 29 | } else if (proc_fork == id) { 30 | return UINT2NUM(NOTE_FORK); 31 | } else { 32 | rb_raise(rb_eNotImpError, "Event `%s` not implemented", rb_id2name(id)); 33 | } 34 | 35 | return Qnil; 36 | } 37 | 38 | 39 | VALUE 40 | kqh_monitor_process(VALUE klass, VALUE pid, VALUE mask) 41 | { 42 | struct kevent new_event; 43 | ID event; 44 | 45 | (void)event; //!< Silence warning about unused var, should be removed? 46 | 47 | u_int fflags = NUM2UINT(mask); 48 | 49 | EV_SET(&new_event, FIX2UINT(pid), EVFILT_PROC, 50 | EV_ADD | EV_ENABLE, fflags, 0, 0); 51 | 52 | if (-1 == kevent(kq, &new_event, 1, NULL, 0, NULL)) { 53 | rb_raise(rb_eStandardError, "%s", strerror(errno)); 54 | } 55 | 56 | num_events = NUM_EVENTS; 57 | 58 | return Qnil; 59 | } 60 | 61 | VALUE 62 | kqh_handle_events() 63 | { 64 | int nevents, i, num_to_fetch; 65 | struct kevent *events; 66 | fd_set read_set; 67 | 68 | FD_ZERO(&read_set); 69 | FD_SET(kq, &read_set); 70 | 71 | // Don't actually run this method until we've got an event 72 | rb_thread_select(kq + 1, &read_set, NULL, NULL, NULL); 73 | 74 | // Grabbing num_events once for thread safety 75 | num_to_fetch = num_events; 76 | events = (struct kevent*)malloc(num_to_fetch * sizeof(struct kevent)); 77 | 78 | if (NULL == events) { 79 | rb_raise(rb_eStandardError, "%s", strerror(errno)); 80 | } 81 | 82 | nevents = kevent(kq, NULL, 0, events, num_to_fetch, NULL); 83 | 84 | if (-1 == nevents) { 85 | free(events); 86 | rb_raise(rb_eStandardError, "%s", strerror(errno)); 87 | } else { 88 | for (i = 0; i < nevents; i++) { 89 | if (events[i].fflags & NOTE_EXIT) { 90 | rb_funcall(cEventHandler, m_call, 2, INT2NUM(events[i].ident), ID2SYM(proc_exit)); 91 | } else if (events[i].fflags & NOTE_FORK) { 92 | rb_funcall(cEventHandler, m_call, 2, INT2NUM(events[i].ident), ID2SYM(proc_fork)); 93 | } 94 | } 95 | } 96 | 97 | free(events); 98 | 99 | return INT2FIX(nevents); 100 | } 101 | 102 | void 103 | Init_kqueue_handler_ext() 104 | { 105 | kq = kqueue(); 106 | 107 | if (kq == -1) { 108 | rb_raise(rb_eStandardError, "kqueue initilization failed"); 109 | } 110 | 111 | proc_exit = rb_intern("proc_exit"); 112 | proc_fork = rb_intern("proc_fork"); 113 | m_call = rb_intern("call"); 114 | m_size = rb_intern("size"); 115 | m_deregister = rb_intern("deregister"); 116 | 117 | mGod = rb_const_get(rb_cObject, rb_intern("God")); 118 | cEventHandler = rb_const_get(mGod, rb_intern("EventHandler")); 119 | cKQueueHandler = rb_define_class_under(mGod, "KQueueHandler", rb_cObject); 120 | rb_define_singleton_method(cKQueueHandler, "monitor_process", kqh_monitor_process, 2); 121 | rb_define_singleton_method(cKQueueHandler, "handle_events", kqh_handle_events, 0); 122 | rb_define_singleton_method(cKQueueHandler, "event_mask", kqh_event_mask, 1); 123 | } 124 | 125 | #endif 126 | -------------------------------------------------------------------------------- /test/test_conditions_http_response_code.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestHttpResponseCode < Test::Unit::TestCase 4 | def valid_condition 5 | c = Conditions::HttpResponseCode.new() 6 | c.watch = stub(:name => 'foo') 7 | c.host = 'localhost' 8 | c.port = 8080 9 | c.path = '/' 10 | c.timeout = 10 11 | c.code_is = 200 12 | c.times = 1 13 | yield(c) if block_given? 14 | c.prepare 15 | c 16 | end 17 | 18 | # valid? 19 | 20 | def test_valid_condition_is_valid 21 | c = valid_condition 22 | assert c.valid? 23 | end 24 | 25 | def test_valid_should_return_false_if_both_code_is_and_code_is_not_are_set 26 | c = valid_condition do |cc| 27 | cc.code_is_not = 500 28 | end 29 | assert !c.valid? 30 | end 31 | 32 | def test_valid_should_return_false_if_no_host_set 33 | c = valid_condition do |cc| 34 | cc.host = nil 35 | end 36 | assert !c.valid? 37 | end 38 | 39 | # test 40 | 41 | def test_test_should_return_false_if_code_is_is_set_to_200_but_response_is_500 42 | c = valid_condition 43 | Net::HTTP.any_instance.expects(:start).yields(mock(:read_timeout= => nil, :get => mock(:code => 500))) 44 | assert_equal false, c.test 45 | end 46 | 47 | def test_test_should_return_false_if_code_is_not_is_set_to_200_and_response_is_200 48 | c = valid_condition do |cc| 49 | cc.code_is = nil 50 | cc.code_is_not = [200] 51 | end 52 | Net::HTTP.any_instance.expects(:start).yields(mock(:read_timeout= => nil, :get => mock(:code => 200))) 53 | assert_equal false, c.test 54 | end 55 | 56 | def test_test_should_return_true_if_code_is_is_set_to_200_and_response_is_200 57 | c = valid_condition 58 | Net::HTTP.any_instance.expects(:start).yields(mock(:read_timeout= => nil, :get => mock(:code => 200))) 59 | assert_equal true, c.test 60 | end 61 | 62 | def test_test_should_return_false_if_code_is_not_is_set_to_200_but_response_is_500 63 | c = valid_condition do |cc| 64 | cc.code_is = nil 65 | cc.code_is_not = [200] 66 | end 67 | Net::HTTP.any_instance.expects(:start).yields(mock(:read_timeout= => nil, :get => mock(:code => 500))) 68 | assert_equal true, c.test 69 | end 70 | 71 | def test_test_should_return_false_if_code_is_is_set_to_200_but_response_times_out 72 | c = valid_condition 73 | Net::HTTP.any_instance.expects(:start).raises(Timeout::Error, '') 74 | assert_equal false, c.test 75 | end 76 | 77 | def test_test_should_return_true_if_code_is_not_is_set_to_200_and_response_times_out 78 | c = valid_condition do |cc| 79 | cc.code_is = nil 80 | cc.code_is_not = [200] 81 | end 82 | Net::HTTP.any_instance.expects(:start).raises(Timeout::Error, '') 83 | assert_equal true, c.test 84 | end 85 | 86 | def test_test_should_return_false_if_code_is_is_set_to_200_but_cant_connect 87 | c = valid_condition 88 | Net::HTTP.any_instance.expects(:start).raises(Errno::ECONNREFUSED, '') 89 | assert_equal false, c.test 90 | end 91 | 92 | def test_test_should_return_true_if_code_is_not_is_set_to_200_and_cant_connect 93 | c = valid_condition do |cc| 94 | cc.code_is = nil 95 | cc.code_is_not = [200] 96 | end 97 | Net::HTTP.any_instance.expects(:start).raises(Errno::ECONNREFUSED, '') 98 | assert_equal true, c.test 99 | end 100 | 101 | def test_test_should_return_true_if_code_is_is_set_to_200_and_response_is_200_twice_for_times_two_of_two 102 | c = valid_condition do |cc| 103 | cc.times = [2, 2] 104 | end 105 | Net::HTTP.any_instance.expects(:start).yields(stub(:read_timeout= => nil, :get => stub(:code => 200))).times(2) 106 | assert_equal false, c.test 107 | assert_equal true, c.test 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/god/contacts/hipchat.rb: -------------------------------------------------------------------------------- 1 | # Send a notice to a Hipchat room (http://hipchat.com). 2 | # 3 | # token - The String token used for authentication. 4 | # room - The String room name to which the message should be sent. 5 | # ssl - A Boolean determining whether or not to use SSL 6 | # (default: false). 7 | # from - The String representing who the message should be sent as. 8 | 9 | require 'net/http' 10 | require 'net/https' 11 | 12 | CONTACT_DEPS[:hipchat] = ['json'] 13 | CONTACT_DEPS[:hipchat].each do |d| 14 | require d 15 | end 16 | 17 | module Marshmallow 18 | class Connection 19 | def initialize(options) 20 | raise "Required option :token not set." unless options[:token] 21 | @options = options 22 | end 23 | 24 | def base_url 25 | scheme = @options[:ssl] ? 'https' : 'http' 26 | "#{scheme}://api.hipchat.com/v1/rooms" 27 | end 28 | 29 | def find_room_id_by_name(room_name) 30 | url = URI.parse("#{base_url}/list?format=json&auth_token=#{@options[:token]}") 31 | http = Net::HTTP.new(url.host, url.port) 32 | http.use_ssl = true if @options[:ssl] 33 | 34 | req = Net::HTTP::Get.new(url.request_uri) 35 | req.set_content_type('application/json') 36 | 37 | res = http.request(req) 38 | case res 39 | when Net::HTTPSuccess 40 | rooms = JSON.parse(res.body) 41 | room = rooms['rooms'].select { |x| x['name'] == room_name } 42 | rooms.empty? ? nil : room.first['room_id'].to_i 43 | else 44 | raise res.error! 45 | end 46 | end 47 | 48 | def speak(room, message) 49 | room_id = find_room_id_by_name(room) 50 | puts "in spark: room_id = #{room_id}" 51 | raise "No such room: #{room}." unless room_id 52 | 53 | escaped_message = URI.escape(message) 54 | 55 | url = URI.parse("#{base_url}/message?message_format=text&format=json&auth_token=#{@options[:token]}&from=#{@options[:from]}&room_id=#{room}&message=#{escaped_message}") 56 | 57 | http = Net::HTTP.new(url.host, url.port) 58 | http.use_ssl = true if @options[:ssl] 59 | 60 | req = Net::HTTP::Post.new(url.request_uri) 61 | req.set_content_type('application/json') 62 | res = http.request(req) 63 | case res 64 | when Net::HTTPSuccess 65 | true 66 | else 67 | raise res.error! 68 | end 69 | end 70 | end 71 | end 72 | 73 | module God 74 | module Contacts 75 | 76 | class Hipchat < Contact 77 | class << self 78 | attr_accessor :token, :room, :ssl, :from 79 | attr_accessor :format 80 | end 81 | 82 | self.ssl = false 83 | 84 | self.format = lambda do |message, time, priority, category, host| 85 | "[#{time.strftime('%H:%M:%S')}] #{host} - #{message}" 86 | end 87 | 88 | attr_accessor :token, :room, :ssl, :from 89 | 90 | def valid? 91 | valid = true 92 | valid &= complain("Attribute 'token' must be specified", self) unless arg(:token) 93 | valid &= complain("Attribute 'room' must be specified", self) unless arg(:room) 94 | valid &= complain("Attribute 'from' must be specified", self) unless arg(:from) 95 | valid 96 | end 97 | 98 | def notify(message, time, priority, category, host) 99 | body = Hipchat.format.call(message, time, priority, category, host) 100 | 101 | conn = Marshmallow::Connection.new( 102 | :token => arg(:token), 103 | :ssl => arg(:ssl), 104 | :from => arg(:from) 105 | ) 106 | 107 | conn.speak(arg(:room), body) 108 | 109 | self.info = "notified hipchat: #{arg(:room)}" 110 | rescue Object => e 111 | applog(nil, :info, "failed to notify hipchat: #{e.message}") 112 | applog(nil, :debug, e.backtrace.join("\n")) 113 | end 114 | end 115 | 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/god/contacts/campfire.rb: -------------------------------------------------------------------------------- 1 | # Send a notice to a Campfire room (http://campfirenow.com). 2 | # 3 | # subdomain - The String subdomain of the Campfire account. If your URL is 4 | # "foo.campfirenow.com" then your subdomain is "foo". 5 | # token - The String token used for authentication. 6 | # room - The String room name to which the message should be sent. 7 | # ssl - A Boolean determining whether or not to use SSL 8 | # (default: false). 9 | 10 | require 'net/http' 11 | require 'net/https' 12 | 13 | CONTACT_DEPS[:campfire] = ['json'] 14 | CONTACT_DEPS[:campfire].each do |d| 15 | require d 16 | end 17 | 18 | module Marshmallow 19 | class Connection 20 | def initialize(options) 21 | raise "Required option :subdomain not set." unless options[:subdomain] 22 | raise "Required option :token not set." unless options[:token] 23 | @options = options 24 | end 25 | 26 | def base_url 27 | scheme = @options[:ssl] ? 'https' : 'http' 28 | subdomain = @options[:subdomain] 29 | "#{scheme}://#{subdomain}.campfirenow.com" 30 | end 31 | 32 | def find_room_id_by_name(room) 33 | url = URI.parse("#{base_url}/rooms.json") 34 | 35 | http = Net::HTTP.new(url.host, url.port) 36 | http.use_ssl = true if @options[:ssl] 37 | 38 | req = Net::HTTP::Get.new(url.path) 39 | req.basic_auth(@options[:token], 'X') 40 | 41 | res = http.request(req) 42 | case res 43 | when Net::HTTPSuccess 44 | rooms = JSON.parse(res.body) 45 | room = rooms['rooms'].select { |x| x['name'] == room } 46 | rooms.empty? ? nil : room.first['id'] 47 | else 48 | raise res.error! 49 | end 50 | end 51 | 52 | def speak(room, message) 53 | room_id = find_room_id_by_name(room) 54 | raise "No such room: #{room}." unless room_id 55 | 56 | url = URI.parse("#{base_url}/room/#{room_id}/speak.json") 57 | 58 | http = Net::HTTP.new(url.host, url.port) 59 | http.use_ssl = true if @options[:ssl] 60 | 61 | req = Net::HTTP::Post.new(url.path) 62 | req.basic_auth(@options[:token], 'X') 63 | req.set_content_type('application/json') 64 | req.body = { 'message' => { 'body' => message } }.to_json 65 | 66 | res = http.request(req) 67 | case res 68 | when Net::HTTPSuccess 69 | true 70 | else 71 | raise res.error! 72 | end 73 | end 74 | end 75 | end 76 | 77 | module God 78 | module Contacts 79 | 80 | class Campfire < Contact 81 | class << self 82 | attr_accessor :subdomain, :token, :room, :ssl 83 | attr_accessor :format 84 | end 85 | 86 | self.ssl = false 87 | 88 | self.format = lambda do |message, time, priority, category, host| 89 | "[#{time.strftime('%H:%M:%S')}] #{host} - #{message}" 90 | end 91 | 92 | attr_accessor :subdomain, :token, :room, :ssl 93 | 94 | def valid? 95 | valid = true 96 | valid &= complain("Attribute 'subdomain' must be specified", self) unless arg(:subdomain) 97 | valid &= complain("Attribute 'token' must be specified", self) unless arg(:token) 98 | valid &= complain("Attribute 'room' must be specified", self) unless arg(:room) 99 | valid 100 | end 101 | 102 | def notify(message, time, priority, category, host) 103 | body = Campfire.format.call(message, time, priority, category, host) 104 | 105 | conn = Marshmallow::Connection.new( 106 | :subdomain => arg(:subdomain), 107 | :token => arg(:token), 108 | :ssl => arg(:ssl) 109 | ) 110 | 111 | conn.speak(arg(:room), body) 112 | 113 | self.info = "notified campfire: #{arg(:subdomain)}" 114 | rescue Object => e 115 | applog(nil, :info, "failed to notify campfire: #{e.message}") 116 | applog(nil, :debug, e.backtrace.join("\n")) 117 | end 118 | end 119 | 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/god/contact.rb: -------------------------------------------------------------------------------- 1 | module God 2 | 3 | class Contact 4 | include Configurable 5 | 6 | attr_accessor :name, :group, :info 7 | 8 | def self.generate(kind) 9 | sym = kind.to_s.capitalize.gsub(/_(.)/){$1.upcase}.intern 10 | c = God::Contacts.const_get(sym).new 11 | 12 | unless c.kind_of?(Contact) 13 | abort "Contact '#{c.class.name}' must subclass God::Contact" 14 | end 15 | 16 | c 17 | rescue NameError 18 | raise NoSuchContactError.new("No Contact found with the class name God::Contacts::#{sym}") 19 | end 20 | 21 | def self.valid?(contact) 22 | valid = true 23 | valid &= Configurable.complain("Attribute 'name' must be specified", contact) if contact.name.nil? 24 | valid 25 | end 26 | 27 | def self.defaults 28 | yield self 29 | end 30 | 31 | def arg(name) 32 | self.instance_variable_get("@#{name}") || self.class.instance_variable_get("@#{name}") 33 | end 34 | 35 | # Normalize the given notify specification into canonical form. 36 | # +spec+ is the notify spec as a String, Array of Strings, or Hash 37 | # 38 | # Canonical form looks like: 39 | # {:contacts => ['fred', 'john'], :priority => '1', :category => 'awesome'} 40 | # Where :contacts will be present and point to an Array of Strings. Both 41 | # :priority and :category may not be present but if they are, they will each 42 | # contain a single String. 43 | # 44 | # Returns normalized notify spec 45 | # Raises ArgumentError on invalid spec (message contains details) 46 | def self.normalize(spec) 47 | case spec 48 | when String 49 | {:contacts => Array(spec)} 50 | when Array 51 | unless spec.select { |x| !x.instance_of?(String) }.empty? 52 | raise ArgumentError.new("contains non-String elements") 53 | end 54 | {:contacts => spec} 55 | when Hash 56 | copy = spec.dup 57 | 58 | # check :contacts 59 | if contacts = copy.delete(:contacts) 60 | case contacts 61 | when String 62 | # valid 63 | when Array 64 | unless contacts.select { |x| !x.instance_of?(String) }.empty? 65 | raise ArgumentError.new("has a :contacts key containing non-String elements") 66 | end 67 | # valid 68 | else 69 | raise ArgumentError.new("must have a :contacts key pointing to a String or Array of Strings") 70 | end 71 | else 72 | raise ArgumentError.new("must have a :contacts key") 73 | end 74 | 75 | # remove priority and category 76 | copy.delete(:priority) 77 | copy.delete(:category) 78 | 79 | # check for invalid keys 80 | unless copy.empty? 81 | raise ArgumentError.new("contains extra elements: #{copy.inspect}") 82 | end 83 | 84 | # normalize 85 | spec[:contacts] &&= Array(spec[:contacts]) 86 | spec[:priority] &&= spec[:priority].to_s 87 | spec[:category] &&= spec[:category].to_s 88 | 89 | spec 90 | else 91 | raise ArgumentError.new("must be a String (contact name), Array (of contact names), or Hash (contact specification)") 92 | end 93 | end 94 | 95 | # Abstract 96 | # Send the message to the external source 97 | # +message+ is the message body returned from the condition 98 | # +time+ is the Time at which the notification was made 99 | # +priority+ is the arbitrary priority String 100 | # +category+ is the arbitrary category String 101 | # +host+ is the hostname of the server 102 | def notify(message, time, priority, category, host) 103 | raise AbstractMethodNotOverriddenError.new("Contact#notify must be overridden in subclasses") 104 | end 105 | 106 | # Construct the friendly name of this Contact, looks like: 107 | # 108 | # Contact FooBar 109 | def friendly_name 110 | super + " Contact '#{self.name}'" 111 | end 112 | end 113 | 114 | end 115 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path('../../lib', __FILE__) # For use/testing when no gem is installed 2 | 3 | # Use this flag to actually load all of the god infrastructure 4 | $load_god = true 5 | 6 | require File.join(File.dirname(__FILE__), *%w[.. lib god sys_logger]) 7 | require File.join(File.dirname(__FILE__), *%w[.. lib god]) 8 | God::EventHandler.load 9 | 10 | require 'test/unit' 11 | require 'set' 12 | 13 | include God 14 | 15 | if Process.uid != 0 and RbConfig::CONFIG['host_os'] == "linux" 16 | abort <<-EOF 17 | \n 18 | ********************************************************************* 19 | * * 20 | * You need to run these tests as root * 21 | * chroot and netlink (linux only) require it * 22 | * * 23 | ********************************************************************* 24 | EOF 25 | end 26 | 27 | begin 28 | require 'mocha/setup' 29 | rescue LoadError 30 | unless gems ||= false 31 | require 'rubygems' 32 | gems = true 33 | retry 34 | else 35 | abort "=> You need the Mocha gem to run these tests." 36 | end 37 | end 38 | 39 | module God 40 | module Conditions 41 | class FakeCondition < Condition 42 | def test 43 | true 44 | end 45 | end 46 | 47 | class FakePollCondition < PollCondition 48 | def test 49 | true 50 | end 51 | end 52 | 53 | class FakeEventCondition < EventCondition 54 | def register 55 | end 56 | def deregister 57 | end 58 | end 59 | end 60 | 61 | module Behaviors 62 | class FakeBehavior < Behavior 63 | def before_start 64 | 'foo' 65 | end 66 | def after_start 67 | 'bar' 68 | end 69 | end 70 | end 71 | 72 | module Contacts 73 | class FakeContact < Contact 74 | end 75 | 76 | class InvalidContact 77 | end 78 | end 79 | 80 | def self.reset 81 | self.watches = nil 82 | self.groups = nil 83 | self.server = nil 84 | self.inited = nil 85 | self.host = nil 86 | self.port = nil 87 | self.pid_file_directory = nil 88 | self.registry.reset 89 | end 90 | end 91 | 92 | def silence_warnings 93 | old_verbose, $VERBOSE = $VERBOSE, nil 94 | yield 95 | ensure 96 | $VERBOSE = old_verbose 97 | end 98 | 99 | LOG.instance_variable_set(:@io, StringIO.new()) 100 | 101 | def output_logs 102 | io = LOG.instance_variable_get(:@io) 103 | LOG.instance_variable_set(:@io, $stderr) 104 | yield 105 | ensure 106 | LOG.instance_variable_set(:@io, io) 107 | end 108 | 109 | # module Kernel 110 | # def abort(text) 111 | # raise SystemExit, text 112 | # end 113 | # def exit(code) 114 | # raise SystemExit, "Exit code: #{code}" 115 | # end 116 | # end 117 | 118 | module Test::Unit::Assertions 119 | def assert_abort 120 | assert_raise SystemExit do 121 | yield 122 | end 123 | end 124 | end 125 | 126 | # This allows you to be a good OOP citizen and honor encapsulation, but 127 | # still make calls to private methods (for testing) by doing 128 | # 129 | # obj.bypass.private_thingie(arg1, arg2) 130 | # 131 | # Which is easier on the eye than 132 | # 133 | # obj.send(:private_thingie, arg1, arg2) 134 | # 135 | class Object 136 | class Bypass 137 | instance_methods.each do |m| 138 | undef_method m unless m =~ /^(__|object_id)/ 139 | end 140 | 141 | def initialize(ref) 142 | @ref = ref 143 | end 144 | 145 | def method_missing(sym, *args) 146 | @ref.__send__(sym, *args) 147 | end 148 | end 149 | 150 | def bypass 151 | Bypass.new(self) 152 | end 153 | end 154 | 155 | # Make sure we return valid exit codes 156 | if defined?(RUBY_ENGINE) && RUBY_ENGINE == "ruby" && RUBY_VERSION >= "1.9" 157 | module Kernel 158 | alias :__at_exit :at_exit 159 | def at_exit(&block) 160 | __at_exit do 161 | exit_status = $!.status if $!.is_a?(SystemExit) 162 | block.call 163 | exit exit_status if exit_status 164 | end 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /site/stylesheets/highlight.css: -------------------------------------------------------------------------------- 1 | .highlight { background: #ffffff; } 2 | .highlight .c { color: #999988; font-style: italic } /* Comment */ 3 | .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ 4 | .highlight .k { font-weight: bold } /* Keyword */ 5 | .highlight .o { font-weight: bold } /* Operator */ 6 | .highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */ 7 | .highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ 8 | .highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */ 9 | .highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ 10 | .highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ 11 | .highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */ 12 | .highlight .ge { font-style: italic } /* Generic.Emph */ 13 | .highlight .gr { color: #aa0000 } /* Generic.Error */ 14 | .highlight .gh { color: #999999 } /* Generic.Heading */ 15 | .highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ 16 | .highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */ 17 | .highlight .go { color: #888888 } /* Generic.Output */ 18 | .highlight .gp { color: #555555 } /* Generic.Prompt */ 19 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 20 | .highlight .gu { color: #800080; font-weight: bold; } /* Generic.Subheading */ 21 | .highlight .gt { color: #aa0000 } /* Generic.Traceback */ 22 | .highlight .kc { font-weight: bold } /* Keyword.Constant */ 23 | .highlight .kd { font-weight: bold } /* Keyword.Declaration */ 24 | .highlight .kn { font-weight: bold } /* Keyword.Namespace */ 25 | .highlight .kp { font-weight: bold } /* Keyword.Pseudo */ 26 | .highlight .kr { font-weight: bold } /* Keyword.Reserved */ 27 | .highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */ 28 | .highlight .m { color: #009999 } /* Literal.Number */ 29 | .highlight .s { color: #d14 } /* Literal.String */ 30 | .highlight .na { color: #008080 } /* Name.Attribute */ 31 | .highlight .nb { color: #0086B3 } /* Name.Builtin */ 32 | .highlight .nc { color: #445588; font-weight: bold } /* Name.Class */ 33 | .highlight .no { color: #008080 } /* Name.Constant */ 34 | .highlight .ni { color: #800080 } /* Name.Entity */ 35 | .highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */ 36 | .highlight .nf { color: #990000; font-weight: bold } /* Name.Function */ 37 | .highlight .nn { color: #555555 } /* Name.Namespace */ 38 | .highlight .nt { color: #000080 } /* Name.Tag */ 39 | .highlight .nv { color: #008080 } /* Name.Variable */ 40 | .highlight .ow { font-weight: bold } /* Operator.Word */ 41 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 42 | .highlight .mf { color: #009999 } /* Literal.Number.Float */ 43 | .highlight .mh { color: #009999 } /* Literal.Number.Hex */ 44 | .highlight .mi { color: #009999 } /* Literal.Number.Integer */ 45 | .highlight .mo { color: #009999 } /* Literal.Number.Oct */ 46 | .highlight .sb { color: #d14 } /* Literal.String.Backtick */ 47 | .highlight .sc { color: #d14 } /* Literal.String.Char */ 48 | .highlight .sd { color: #d14 } /* Literal.String.Doc */ 49 | .highlight .s2 { color: #d14 } /* Literal.String.Double */ 50 | .highlight .se { color: #d14 } /* Literal.String.Escape */ 51 | .highlight .sh { color: #d14 } /* Literal.String.Heredoc */ 52 | .highlight .si { color: #d14 } /* Literal.String.Interpol */ 53 | .highlight .sx { color: #d14 } /* Literal.String.Other */ 54 | .highlight .sr { color: #009926 } /* Literal.String.Regex */ 55 | .highlight .s1 { color: #d14 } /* Literal.String.Single */ 56 | .highlight .ss { color: #990073 } /* Literal.String.Symbol */ 57 | .highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */ 58 | .highlight .vc { color: #008080 } /* Name.Variable.Class */ 59 | .highlight .vg { color: #008080 } /* Name.Variable.Global */ 60 | .highlight .vi { color: #008080 } /* Name.Variable.Instance */ 61 | .highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ 62 | 63 | .type-csharp .highlight .k { color: #0000FF } 64 | .type-csharp .highlight .kt { color: #0000FF } 65 | .type-csharp .highlight .nf { color: #000000; font-weight: normal } 66 | .type-csharp .highlight .nc { color: #2B91AF } 67 | .type-csharp .highlight .nn { color: #000000 } 68 | .type-csharp .highlight .s { color: #A31515 } 69 | .type-csharp .highlight .sc { color: #A31515 } 70 | -------------------------------------------------------------------------------- /bin/god: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | STDOUT.sync = true 4 | 5 | $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib]) 6 | 7 | require 'optparse' 8 | require 'drb' 9 | require 'yaml' 10 | 11 | begin 12 | # Save ARGV in case someone wants to use it later 13 | ORIGINAL_ARGV = ARGV.dup 14 | 15 | options = {:daemonize => true, :port => 17165, :syslog => true, :events => true} 16 | 17 | opts = OptionParser.new do |opts| 18 | opts.banner = <<-EOF 19 | Usage: 20 | Starting: 21 | god [-c ] [-p | -b] [-P ] [-l ] [-D] 22 | 23 | Querying: 24 | god [-p ] 25 | god [-p ] 26 | god -v 27 | god -V (must be run as root to be accurate on Linux) 28 | 29 | Commands: 30 | start start task or group 31 | restart restart task or group 32 | stop stop task or group 33 | monitor monitor task or group 34 | unmonitor unmonitor task or group 35 | remove remove task or group from god 36 | load [action] load a config into a running god 37 | log show realtime log for given task 38 | status [task or group name] show status 39 | signal signal all matching tasks 40 | quit stop god 41 | terminate stop god and all tasks 42 | check run self diagnostic 43 | 44 | Options: 45 | EOF 46 | 47 | opts.on("-cCONFIG", "--config-file CONFIG", "Configuration file") do |x| 48 | options[:config] = x 49 | end 50 | 51 | opts.on("-pPORT", "--port PORT", "Communications port (default 17165)") do |x| 52 | options[:port] = x 53 | end 54 | 55 | opts.on("-b", "--auto-bind", "Auto-bind to an unused port number") do 56 | options[:port] = "0" 57 | end 58 | 59 | opts.on("-PFILE", "--pid FILE", "Where to write the PID file") do |x| 60 | options[:pid] = x 61 | end 62 | 63 | opts.on("-lFILE", "--log FILE", "Where to write the log file") do |x| 64 | options[:log] = x 65 | end 66 | 67 | opts.on("-D", "--no-daemonize", "Don't daemonize") do 68 | options[:daemonize] = false 69 | end 70 | 71 | opts.on("-v", "--version", "Print the version number and exit") do 72 | options[:version] = true 73 | end 74 | 75 | opts.on("-V", "Print extended version and build information") do 76 | options[:info] = true 77 | end 78 | 79 | opts.on("--log-level LEVEL", "Log level [debug|info|warn|error|fatal]") do |x| 80 | options[:log_level] = x.to_sym 81 | end 82 | 83 | opts.on("--no-syslog", "Disable output to syslog") do 84 | options[:syslog] = false 85 | end 86 | 87 | opts.on("--attach PID", "Quit god when the attached process dies") do |x| 88 | options[:attach] = x 89 | end 90 | 91 | opts.on("--no-events", "Disable the event system") do 92 | options[:events] = false 93 | end 94 | 95 | opts.on("--bleakhouse", "Enable bleakhouse profiling") do 96 | options[:bleakhouse] = true 97 | end 98 | end 99 | 100 | opts.parse! 101 | 102 | # validate 103 | if options[:log_level] && ![:debug, :info, :warn, :error, :fatal].include?(options[:log_level]) 104 | abort("Invalid log level '#{options[:log_level]}'") 105 | end 106 | 107 | # Use this flag to actually load all of the god infrastructure 108 | $load_god = true 109 | 110 | # dispatch 111 | if !options[:config] && options[:version] 112 | require 'god' 113 | God::CLI::Version.version 114 | elsif !options[:config] && options[:info] 115 | require 'god' 116 | God::EventHandler.load 117 | God::CLI::Version.version_extended 118 | elsif !options[:config] && command = ARGV[0] 119 | require 'god' 120 | God::EventHandler.load 121 | God::CLI::Command.new(command, options, ARGV) 122 | else 123 | require 'god/cli/run' 124 | God::CLI::Run.new(options) 125 | end 126 | rescue Exception => e 127 | if e.instance_of?(SystemExit) 128 | raise 129 | else 130 | puts 'Uncaught exception' 131 | puts e.message 132 | puts e.backtrace.join("\n") 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /lib/god/conditions/flapping.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module Conditions 3 | 4 | # Condition Symbol :flapping 5 | # Type: Trigger 6 | # 7 | # Trigger when a Task transitions to or from a state or states a given number 8 | # of times within a given period. 9 | # 10 | # Paramaters 11 | # Required 12 | # +times+ is the number of times that the Task must transition before 13 | # triggering. 14 | # +within+ is the number of seconds within which the Task must transition 15 | # the specified number of times before triggering. You may use 16 | # the sugar methods #seconds, #minutes, #hours, #days to clarify 17 | # your code (see examples). 18 | # --one or both of-- 19 | # +from_state+ is the state (as a Symbol) from which the transition must occur. 20 | # +to_state is the state (as a Symbol) to which the transition must occur. 21 | # 22 | # Optional: 23 | # +retry_in+ is the number of seconds after which to re-monitor the Task after 24 | # it has been disabled by the condition. 25 | # +retry_times+ is the number of times after which to permanently unmonitor 26 | # the Task. 27 | # +retry_within+ is the number of seconds within which 28 | # 29 | # Examples 30 | # 31 | # Trigger if 32 | class Flapping < TriggerCondition 33 | attr_accessor :times, 34 | :within, 35 | :from_state, 36 | :to_state, 37 | :retry_in, 38 | :retry_times, 39 | :retry_within 40 | 41 | def initialize 42 | self.info = "process is flapping" 43 | end 44 | 45 | def prepare 46 | @timeline = Timeline.new(self.times) 47 | @retry_timeline = Timeline.new(self.retry_times) 48 | end 49 | 50 | def valid? 51 | valid = true 52 | valid &= complain("Attribute 'times' must be specified", self) if self.times.nil? 53 | valid &= complain("Attribute 'within' must be specified", self) if self.within.nil? 54 | valid &= complain("Attributes 'from_state', 'to_state', or both must be specified", self) if self.from_state.nil? && self.to_state.nil? 55 | valid 56 | end 57 | 58 | def process(event, payload) 59 | begin 60 | if event == :state_change 61 | event_from_state, event_to_state = *payload 62 | 63 | from_state_match = !self.from_state || self.from_state && Array(self.from_state).include?(event_from_state) 64 | to_state_match = !self.to_state || self.to_state && Array(self.to_state).include?(event_to_state) 65 | 66 | if from_state_match && to_state_match 67 | @timeline << Time.now 68 | 69 | concensus = (@timeline.size == self.times) 70 | duration = (@timeline.last - @timeline.first) < self.within 71 | 72 | if concensus && duration 73 | @timeline.clear 74 | trigger 75 | retry_mechanism 76 | end 77 | end 78 | end 79 | rescue => e 80 | puts e.message 81 | puts e.backtrace.join("\n") 82 | end 83 | end 84 | 85 | private 86 | 87 | def retry_mechanism 88 | if self.retry_in 89 | @retry_timeline << Time.now 90 | 91 | concensus = (@retry_timeline.size == self.retry_times) 92 | duration = (@retry_timeline.last - @retry_timeline.first) < self.retry_within 93 | 94 | if concensus && duration 95 | # give up 96 | Thread.new do 97 | sleep 1 98 | 99 | # log 100 | msg = "#{self.watch.name} giving up" 101 | applog(self.watch, :info, msg) 102 | end 103 | else 104 | # try again later 105 | Thread.new do 106 | sleep 1 107 | 108 | # log 109 | msg = "#{self.watch.name} auto-reenable monitoring in #{self.retry_in} seconds" 110 | applog(self.watch, :info, msg) 111 | 112 | sleep self.retry_in 113 | 114 | # log 115 | msg = "#{self.watch.name} auto-reenabling monitoring" 116 | applog(self.watch, :info, msg) 117 | 118 | if self.watch.state == :unmonitored 119 | self.watch.monitor 120 | end 121 | end 122 | end 123 | end 124 | end 125 | end 126 | 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/god/conditions/socket_responding.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | include Socket::Constants 3 | 4 | module God 5 | module Conditions 6 | # Condition Symbol :socket_running 7 | # Type: Poll 8 | # 9 | # Trigger when a TCP or UNIX socket is running or not 10 | # 11 | # Parameters 12 | # Required 13 | # +family+ is the family of socket: either 'tcp' or 'unix' 14 | # --one of port or path-- 15 | # +port+ is the port (required if +family+ is 'tcp') 16 | # +path+ is the path (required if +family+ is 'unix') 17 | # 18 | # Optional 19 | # +responding+ is the boolean specifying whether you want to trigger if the socket is responding (true) 20 | # or if it is not responding (false) (default false) 21 | # 22 | # Examples 23 | # 24 | # Trigger if the TCP socket on port 80 is not responding or the connection is refused 25 | # 26 | # on.condition(:socket_responding) do |c| 27 | # c.family = 'tcp' 28 | # c.port = '80' 29 | # end 30 | # 31 | # Trigger if the socket is not responding or the connection is refused (use alternate compact +socket+ interface) 32 | # 33 | # on.condition(:socket_responding) do |c| 34 | # c.socket = 'tcp:80' 35 | # end 36 | # 37 | # Trigger if the socket is responding 38 | # 39 | # on.condition(:socket_responding) do |c| 40 | # c.socket = 'tcp:80' 41 | # c.responding = true 42 | # end 43 | # 44 | # Trigger if the socket is not responding or the connection is refused 5 times in a row 45 | # 46 | # on.condition(:socket_responding) do |c| 47 | # c.socket = 'tcp:80' 48 | # c.times = 5 49 | # end 50 | # 51 | # Trigger if the Unix socket on path '/tmp/sock' is not responding or non-existent 52 | # 53 | # on.condition(:socket_responding) do |c| 54 | # c.family = 'unix' 55 | # c.port = '/tmp/sock' 56 | # end 57 | # 58 | class SocketResponding < PollCondition 59 | attr_accessor :family, :addr, :port, :path, :times, :responding 60 | 61 | def initialize 62 | super 63 | # default to tcp on the localhost 64 | self.family = 'tcp' 65 | self.addr = '127.0.0.1' 66 | # Set these to nil/0 values 67 | self.port = 0 68 | self.path = nil 69 | self.responding = false 70 | 71 | self.times = [1, 1] 72 | end 73 | 74 | def prepare 75 | if self.times.kind_of?(Integer) 76 | self.times = [self.times, self.times] 77 | end 78 | 79 | @timeline = Timeline.new(self.times[1]) 80 | @history = Timeline.new(self.times[1]) 81 | end 82 | 83 | def reset 84 | @timeline.clear 85 | @history.clear 86 | end 87 | 88 | def socket=(s) 89 | components = s.split(':') 90 | if components.size == 3 91 | @family,@addr,@port = components 92 | @port = @port.to_i 93 | elsif components[0] =~ /^tcp$/ 94 | @family = components[0] 95 | @port = components[1].to_i 96 | elsif components[0] =~ /^unix$/ 97 | @family = components[0] 98 | @path = components[1] 99 | end 100 | end 101 | 102 | def valid? 103 | valid = true 104 | if self.family == 'tcp' and @port == 0 105 | valid &= complain("Attribute 'port' must be specified for tcp sockets", self) 106 | end 107 | if self.family == 'unix' and self.path.nil? 108 | valid &= complain("Attribute 'path' must be specified for unix sockets", self) 109 | end 110 | valid = false unless %w{tcp unix}.member?(self.family) 111 | valid 112 | end 113 | 114 | def test 115 | self.info = [] 116 | if self.family == 'tcp' 117 | begin 118 | s = TCPSocket.new(self.addr, self.port) 119 | rescue SystemCallError 120 | end 121 | status = self.responding == !s.nil? 122 | elsif self.family == 'unix' 123 | begin 124 | s = UNIXSocket.new(self.path) 125 | rescue SystemCallError 126 | end 127 | status = self.responding == !s.nil? 128 | else 129 | status = false 130 | end 131 | @timeline.push(status) 132 | history = "[" + @timeline.map {|t| t ? '*' : ''}.join(',') + "]" 133 | if @timeline.select { |x| x }.size >= self.times.first 134 | self.info = "socket out of bounds #{history}" 135 | return true 136 | else 137 | return false 138 | end 139 | end 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /lib/god/cli/run.rb: -------------------------------------------------------------------------------- 1 | module God 2 | module CLI 3 | 4 | class Run 5 | def initialize(options) 6 | @options = options 7 | 8 | dispatch 9 | end 10 | 11 | def dispatch 12 | # have at_exit start god 13 | $run = true 14 | 15 | if @options[:syslog] 16 | require 'god/sys_logger' 17 | end 18 | 19 | # run 20 | if @options[:daemonize] 21 | run_daemonized 22 | else 23 | run_in_front 24 | end 25 | end 26 | 27 | def attach 28 | process = System::Process.new(@options[:attach]) 29 | Thread.new do 30 | loop do 31 | unless process.exists? 32 | applog(nil, :info, "Going down because attached process #{@options[:attach]} exited") 33 | exit! 34 | end 35 | sleep 5 36 | end 37 | end 38 | end 39 | 40 | def default_run 41 | # make sure we have STDIN/STDOUT redirected immediately 42 | setup_logging 43 | 44 | # start attached pid watcher if necessary 45 | if @options[:attach] 46 | self.attach 47 | end 48 | 49 | if @options[:port] 50 | God.port = @options[:port] 51 | end 52 | 53 | if @options[:events] 54 | God::EventHandler.load 55 | end 56 | 57 | # set log level, defaults to WARN 58 | if @options[:log_level] 59 | God.log_level = @options[:log_level] 60 | else 61 | God.log_level = @options[:daemonize] ? :warn : :info 62 | end 63 | 64 | if @options[:config] 65 | if !@options[:config].include?('*') && !File.exist?(@options[:config]) 66 | abort "File not found: #{@options[:config]}" 67 | end 68 | 69 | # start the event handler 70 | God::EventHandler.start if God::EventHandler.loaded? 71 | 72 | load_config @options[:config] 73 | end 74 | setup_logging 75 | end 76 | 77 | def run_in_front 78 | require 'god' 79 | 80 | default_run 81 | end 82 | 83 | def run_daemonized 84 | # trap and ignore SIGHUP 85 | Signal.trap('HUP') {} 86 | # trap and log-reopen SIGUSR1 87 | Signal.trap('USR1') { setup_logging } 88 | 89 | pid = fork do 90 | begin 91 | require 'god' 92 | 93 | # set pid if requested 94 | if @options[:pid] # and as deamon 95 | God.pid = @options[:pid] 96 | end 97 | 98 | default_run 99 | 100 | unless God::EventHandler.loaded? 101 | puts 102 | puts "***********************************************************************" 103 | puts "*" 104 | puts "* Event conditions are not available for your installation of god." 105 | puts "* You may still use and write custom conditions using the poll system" 106 | puts "*" 107 | puts "***********************************************************************" 108 | puts 109 | end 110 | 111 | rescue => e 112 | puts e.message 113 | puts e.backtrace.join("\n") 114 | abort "There was a fatal system error while starting god (see above)" 115 | end 116 | end 117 | 118 | if @options[:pid] 119 | File.open(@options[:pid], 'w') { |f| f.write pid } 120 | end 121 | 122 | ::Process.detach pid 123 | 124 | exit 125 | end 126 | 127 | def setup_logging 128 | log_file = God.log_file 129 | log_file = File.expand_path(@options[:log]) if @options[:log] 130 | log_file = "/dev/null" if !log_file && @options[:daemonize] 131 | if log_file 132 | puts "Sending output to log file: #{log_file}" unless @options[:daemonize] 133 | 134 | # reset file descriptors 135 | STDIN.reopen "/dev/null" 136 | STDOUT.reopen(log_file, "a") 137 | STDERR.reopen STDOUT 138 | STDOUT.sync = true 139 | end 140 | end 141 | 142 | def load_config(config) 143 | files = File.directory?(config) ? Dir['**/*.god'] : Dir[config] 144 | abort "No files could be found" if files.empty? 145 | files.each do |god_file| 146 | unless load_god_file(god_file) 147 | abort "File '#{god_file}' could not be loaded" 148 | end 149 | end 150 | end 151 | 152 | def load_god_file(god_file) 153 | applog(nil, :info, "Loading #{god_file}") 154 | load File.expand_path(god_file) 155 | true 156 | rescue Exception => e 157 | if e.instance_of?(SystemExit) 158 | raise 159 | else 160 | puts "There was an error in #{god_file}" 161 | puts "\t" + e.message 162 | puts "\t" + e.backtrace.join("\n\t") 163 | false 164 | end 165 | end 166 | 167 | end # Run 168 | 169 | end 170 | end 171 | -------------------------------------------------------------------------------- /Announce.txt: -------------------------------------------------------------------------------- 1 | Subject: [ANN] god 0.6.0 released (and mailing list) 2 | 3 | !!!NEW MAILING LIST!!! We now have a mailing list at http://groups.google.com/group/god-rb 4 | 5 | This release is primarily focused on increased stability, robustness, and code cleanliness. 6 | 7 | The last release (0.5.0) switched from TCP sockets to Unix Domain Sockets for the CLI tools. There were some issues regarding file descriptors that would cause god to hang when restarting (the unix socket was being held open by spawned processes). These problems are all fixed in 0.6.0. You may need to kill any processes that were started under god 0.5.0 when you upgrade to 0.6.0 (you will only need to do this once during the upgrade process). 8 | 9 | More attention is now paid to Syslogging behavior. 10 | 11 | God will delete its own PID file when terminated via a user request. 12 | 13 | A new command line option now allows you to check whether your installation properly handles events (sometimes the C extension will compile ok, but still not support events. Gentoo, I'm looking at you). Run `god check` to see an attempt to use the event system. It will tell you if your installation does not support events. 14 | 15 | A watch can now be removed entirely at runtime using `god remove `. 16 | 17 | A new condition DiskUsage allows you to check the available disk space on a given volume. 18 | 19 | Updated documentation is now available on the website: 20 | 21 | http://god.rubyforge.org/ 22 | 23 | 24 | WHAT IS GOD? 25 | 26 | God is an easy to configure, easy to extend monitoring framework written in Ruby. 27 | 28 | Keeping your server processes and tasks running should be a simple part of your deployment process. God aims to be the simplest, most powerful monitoring application available. 29 | 30 | 31 | DISCLAIMER 32 | 33 | God is still beta so I do not yet recommend you use it for mission critical tasks. We are using it at Powerset, Inc. to monitor our public facing applications, but then, we're daring fellows. 34 | 35 | 36 | INSTALL 37 | 38 | sudo gem install god 39 | 40 | 41 | FEATURES 42 | 43 | * Config file is written in Ruby 44 | * Easily write your own custom conditions in Ruby 45 | * Supports both poll and event based conditions 46 | * Different poll conditions can have different intervals 47 | * Easily control non-daemonized processes 48 | 49 | 50 | EXAMPLE 51 | 52 | The easiest way to understand how god will make your life better is by looking at a sample config file. The following configuration file is what I use at gravatar.com to keep the mongrels running: 53 | 54 | # run with: god -c /path/to/gravatar.god 55 | # 56 | # This is the actual config file used to keep the mongrels of 57 | # gravatar.com running. 58 | 59 | RAILS_ROOT = "/Users/tom/dev/gravatar2" 60 | 61 | %w{8200 8201 8202}.each do |port| 62 | God.watch do |w| 63 | w.name = "gravatar2-mongrel-#{port}" 64 | w.interval = 30.seconds # default 65 | w.start = "mongrel_rails start -c #{RAILS_ROOT} -p #{port} \ 66 | -P #{RAILS_ROOT}/log/mongrel.#{port}.pid -d" 67 | w.stop = "mongrel_rails stop -P #{RAILS_ROOT}/log/mongrel.#{port}.pid" 68 | w.restart = "mongrel_rails restart -P #{RAILS_ROOT}/log/mongrel.#{port}.pid" 69 | w.start_grace = 10.seconds 70 | w.restart_grace = 10.seconds 71 | w.pid_file = File.join(RAILS_ROOT, "log/mongrel.#{port}.pid") 72 | 73 | w.behavior(:clean_pid_file) 74 | 75 | w.start_if do |start| 76 | start.condition(:process_running) do |c| 77 | c.interval = 5.seconds 78 | c.running = false 79 | end 80 | end 81 | 82 | w.restart_if do |restart| 83 | restart.condition(:memory_usage) do |c| 84 | c.above = 150.megabytes 85 | c.times = [3, 5] # 3 out of 5 intervals 86 | end 87 | 88 | restart.condition(:cpu_usage) do |c| 89 | c.above = 50.percent 90 | c.times = 5 91 | end 92 | end 93 | 94 | # lifecycle 95 | w.lifecycle do |on| 96 | on.condition(:flapping) do |c| 97 | c.to_state = [:start, :restart] 98 | c.times = 5 99 | c.within = 5.minute 100 | c.transition = :unmonitored 101 | c.retry_in = 10.minutes 102 | c.retry_times = 5 103 | c.retry_within = 2.hours 104 | end 105 | end 106 | end 107 | end 108 | 109 | 110 | DOCS 111 | 112 | Detailed documentation is available at http://god.rubyforge.org/ 113 | 114 | 115 | CHANGES 116 | 117 | == 0.6.0 / 2007-12-4 118 | 119 | * Minor Enhancement 120 | * Move Syslog calls into God::Logger and clean up all calling code 121 | * Remove god's pid file on user requested termination 122 | * Better handling and cleanup of DRb server's unix domain socket 123 | * Allow shorthand for requesting a god log 124 | * Add `god check` to make it easier to diagnose event problems 125 | * Refactor god binary into class/method structure 126 | * Implement `god remove` to remove a Task altogether 127 | * New Conditions 128 | * DiskUsage < PollCondition - trigger if disk usage is above limit on mount [Rudy Desjardins] 129 | 130 | 131 | 132 | AUTHORS 133 | 134 | Tom Preston-Werner 135 | Kevin Clark 136 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'rdoc/task' 4 | require 'date' 5 | 6 | ############################################################################# 7 | # 8 | # Helper functions 9 | # 10 | ############################################################################# 11 | 12 | def name 13 | @name ||= Dir['*.gemspec'].first.split('.').first 14 | end 15 | 16 | def version 17 | line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/] 18 | line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1] 19 | end 20 | 21 | def date 22 | Date.today.to_s 23 | end 24 | 25 | def rubyforge_project 26 | name 27 | end 28 | 29 | def gemspec_file 30 | "#{name}.gemspec" 31 | end 32 | 33 | def gem_file 34 | "#{name}-#{version}.gem" 35 | end 36 | 37 | def replace_header(head, header_name) 38 | head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"} 39 | end 40 | 41 | ############################################################################# 42 | # 43 | # Standard tasks 44 | # 45 | ############################################################################# 46 | 47 | task :default => :test 48 | 49 | require 'rake/testtask' 50 | Rake::TestTask.new(:test) do |test| 51 | test.libs << 'lib' << 'test' 52 | test.pattern = 'test/**/test_*.rb' 53 | test.verbose = true 54 | end 55 | 56 | desc "Generate RCov test coverage and open in your browser" 57 | task :coverage do 58 | require 'rcov' 59 | sh "rm -fr coverage" 60 | sh "rcov test/test_*.rb" 61 | sh "open coverage/index.html" 62 | end 63 | 64 | require 'rdoc/task' 65 | Rake::RDocTask.new do |rdoc| 66 | rdoc.rdoc_dir = 'rdoc' 67 | rdoc.title = "#{name} #{version}" 68 | rdoc.rdoc_files.include('README*') 69 | rdoc.rdoc_files.include('lib/**/*.rb') 70 | end 71 | 72 | desc "Open an irb session preloaded with this library" 73 | task :console do 74 | sh "irb -rubygems -r ./lib/#{name}.rb" 75 | end 76 | 77 | ############################################################################# 78 | # 79 | # Custom tasks (add your own tasks here) 80 | # 81 | ############################################################################# 82 | 83 | desc "Generate and view the site locally" 84 | task :site do 85 | # Generate the dynamic parts of the site. 86 | puts "Generating dynamic..." 87 | require 'gollum' 88 | wiki = Gollum::Wiki.new('.', :base_path => '/doc') 89 | html = wiki.page('god', 'HEAD').formatted_data.gsub("\342\200\231", "'") 90 | template = File.read('./site/index.template.html') 91 | index = template.sub("{{ content }}", html) 92 | File.open('./site/index.html', 'w') do |f| 93 | f.write(index) 94 | end 95 | 96 | puts "Done. Opening in browser..." 97 | sh "open site/index.html" 98 | end 99 | 100 | desc "Commit the local site to the gh-pages branch and deploy" 101 | task :site_release do 102 | # Ensure the gh-pages dir exists so we can generate into it. 103 | puts "Checking for gh-pages dir..." 104 | unless File.exist?("./gh-pages") 105 | puts "No gh-pages directory found. Run the following commands first:" 106 | puts " `git clone git@github.com:mojombo/god gh-pages" 107 | puts " `cd gh-pages" 108 | puts " `git checkout gh-pages`" 109 | exit(1) 110 | end 111 | 112 | # Copy the rest of the site over. 113 | puts "Copying static..." 114 | sh "cp -R site/* gh-pages/" 115 | 116 | # Commit the changes 117 | sha = `git log`.match(/[a-z0-9]{40}/)[0] 118 | sh "cd gh-pages && git add . && git commit -m 'Updating to #{sha}.' && git push" 119 | puts 'Done.' 120 | end 121 | 122 | ############################################################################# 123 | # 124 | # Packaging tasks 125 | # 126 | ############################################################################# 127 | 128 | desc "Create tag v#{version} and build and push #{gem_file} to Rubygems" 129 | task :release => :build do 130 | unless `git branch` =~ /^\* master$/ 131 | puts "You must be on the master branch to release!" 132 | exit! 133 | end 134 | sh "git commit --allow-empty -a -m 'Release #{version}'" 135 | sh "git tag v#{version}" 136 | sh "git push origin master" 137 | sh "git push origin v#{version}" 138 | sh "gem push pkg/#{name}-#{version}.gem" 139 | end 140 | 141 | desc "Build #{gem_file} into the pkg directory" 142 | task :build => :gemspec do 143 | sh "mkdir -p pkg" 144 | sh "gem build #{gemspec_file}" 145 | sh "mv #{gem_file} pkg" 146 | end 147 | 148 | desc "Generate #{gemspec_file}" 149 | task :gemspec do 150 | # read spec file and split out manifest section 151 | spec = File.read(gemspec_file) 152 | head, manifest, tail = spec.split(" # = MANIFEST =\n") 153 | 154 | # replace name version and date 155 | replace_header(head, :name) 156 | replace_header(head, :version) 157 | replace_header(head, :date) 158 | #comment this out if your rubyforge_project has a different name 159 | replace_header(head, :rubyforge_project) 160 | 161 | # determine file list from git ls-files 162 | files = `git ls-files`. 163 | split("\n"). 164 | sort. 165 | reject { |file| file =~ /^\./ }. 166 | reject { |file| file =~ /^(rdoc|pkg|examples|ideas|init|site)/ }. 167 | map { |file| " #{file}" }. 168 | join("\n") 169 | 170 | # piece file back together and write 171 | manifest = " s.files = %w[\n#{files}\n ]\n" 172 | spec = [head, manifest, tail].join(" # = MANIFEST =\n") 173 | File.open(gemspec_file, 'w') { |io| io.write(spec) } 174 | puts "Updated #{gemspec_file}" 175 | end 176 | -------------------------------------------------------------------------------- /test/test_conditions_socket_responding.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestConditionsSocketResponding < Test::Unit::TestCase 4 | # valid? 5 | 6 | def test_valid_should_return_false_if_no_options_set 7 | c = Conditions::SocketResponding.new 8 | c.watch = stub(:name => 'foo') 9 | assert_equal false, c.valid? 10 | 11 | end 12 | 13 | def test_valid_should_return_true_if_required_options_set_for_default 14 | c = Conditions::SocketResponding.new 15 | c.port = 443 16 | assert_equal true, c.valid? 17 | end 18 | 19 | def test_valid_should_return_true_if_required_options_set_for_tcp 20 | c = Conditions::SocketResponding.new 21 | c.family = 'tcp' 22 | c.port = 443 23 | assert_equal true, c.valid? 24 | end 25 | 26 | def test_valid_should_return_true_if_required_options_set_for_unix 27 | c = Conditions::SocketResponding.new 28 | c.path = 'some-path' 29 | c.family = 'unix' 30 | assert_equal true, c.valid? 31 | end 32 | 33 | def test_valid_should_return_true_if_family_is_tcp 34 | c = Conditions::SocketResponding.new 35 | c.port = 443 36 | c.family = 'tcp' 37 | assert_equal true, c.valid? 38 | end 39 | 40 | def test_valid_should_return_true_if_family_is_unix 41 | c = Conditions::SocketResponding.new 42 | c.path = 'some-path' 43 | c.family = 'unix' 44 | c.watch = stub(:name => 'foo') 45 | assert_equal true, c.valid? 46 | end 47 | 48 | # socket method 49 | def test_socket_should_return_127_0_0_1_for_default_addr 50 | c = Conditions::SocketResponding.new 51 | c.socket = 'tcp:443' 52 | assert_equal c.addr, '127.0.0.1' 53 | end 54 | 55 | def test_socket_should_set_properties_for_tcp 56 | c = Conditions::SocketResponding.new 57 | c.socket = 'tcp:127.0.0.1:443' 58 | assert_equal c.family, 'tcp' 59 | assert_equal c.addr, '127.0.0.1' 60 | assert_equal c.port, 443 61 | assert_equal c.responding, false 62 | # path should not be set for tcp sockets 63 | assert_equal c.path, nil 64 | end 65 | 66 | def test_socket_should_set_properties_for_unix 67 | c = Conditions::SocketResponding.new 68 | c.socket = 'unix:/tmp/process.sock' 69 | assert_equal c.family, 'unix' 70 | assert_equal c.path, '/tmp/process.sock' 71 | assert_equal c.responding, false 72 | # path should not be set for unix domain sockets 73 | assert_equal c.port, 0 74 | end 75 | 76 | # test : responding = false 77 | 78 | def test_test_tcp_should_return_false_if_socket_is_listening 79 | c = Conditions::SocketResponding.new 80 | c.prepare 81 | 82 | TCPSocket.expects(:new).returns(0) 83 | assert_equal false, c.test 84 | end 85 | 86 | def test_test_tcp_should_return_true_if_no_socket_is_listening 87 | c = Conditions::SocketResponding.new 88 | c.prepare 89 | 90 | TCPSocket.expects(:new).returns(nil) 91 | assert_equal true, c.test 92 | end 93 | 94 | def test_test_unix_should_return_false_if_socket_is_listening 95 | c = Conditions::SocketResponding.new 96 | c.socket = 'unix:/some/path' 97 | 98 | c.prepare 99 | UNIXSocket.expects(:new).returns(0) 100 | assert_equal false, c.test 101 | end 102 | 103 | def test_test_unix_should_return_true_if_no_socket_is_listening 104 | 105 | c = Conditions::SocketResponding.new 106 | c.socket = 'unix:/some/path' 107 | c.prepare 108 | 109 | UNIXSocket.expects(:new).returns(nil) 110 | assert_equal true, c.test 111 | end 112 | 113 | def test_test_unix_should_return_true_if_socket_is_listening_2_times 114 | 115 | c = Conditions::SocketResponding.new 116 | c.socket = 'unix:/some/path' 117 | c.times = [2, 2] 118 | c.prepare 119 | 120 | UNIXSocket.expects(:new).returns(nil).times(2) 121 | assert_equal false, c.test 122 | assert_equal true, c.test 123 | end 124 | 125 | # test : responding = true 126 | 127 | def test_test_tcp_should_return_true_if_socket_is_listening_with_responding_true 128 | c = Conditions::SocketResponding.new 129 | c.responding = true 130 | c.prepare 131 | 132 | TCPSocket.expects(:new).returns(0) 133 | assert_equal true, c.test 134 | end 135 | 136 | def test_test_tcp_should_return_false_if_no_socket_is_listening_with_responding_true 137 | c = Conditions::SocketResponding.new 138 | c.responding = true 139 | c.prepare 140 | 141 | TCPSocket.expects(:new).returns(nil) 142 | assert_equal false, c.test 143 | end 144 | 145 | def test_test_unix_should_return_true_if_socket_is_listening_with_responding_true 146 | c = Conditions::SocketResponding.new 147 | c.responding = true 148 | c.socket = 'unix:/some/path' 149 | 150 | c.prepare 151 | UNIXSocket.expects(:new).returns(0) 152 | assert_equal true, c.test 153 | end 154 | 155 | def test_test_unix_should_return_false_if_no_socket_is_listening_with_responding_true 156 | c = Conditions::SocketResponding.new 157 | c.socket = 'unix:/some/path' 158 | c.responding = true 159 | c.prepare 160 | 161 | UNIXSocket.expects(:new).returns(nil) 162 | assert_equal false, c.test 163 | end 164 | 165 | def test_test_unix_should_return_false_if_socket_is_listening_2_times_with_responding_true 166 | c = Conditions::SocketResponding.new 167 | c.socket = 'unix:/some/path' 168 | c.responding = true 169 | c.times = [2, 2] 170 | c.prepare 171 | 172 | UNIXSocket.expects(:new).returns(nil).times(2) 173 | assert_equal false, c.test 174 | assert_equal false, c.test 175 | end 176 | end 177 | --------------------------------------------------------------------------------