nines 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +10 -0
- data/.rvmrc +33 -0
- data/MIT-LICENSE.txt +20 -0
- data/README.rdoc +24 -0
- data/bin/nines +61 -0
- data/doc/logrotate.conf +8 -0
- data/install.rb +90 -0
- data/lib/nines/app.rb +194 -0
- data/lib/nines/check.rb +67 -0
- data/lib/nines/check_group.rb +58 -0
- data/lib/nines/email_templates/notification.text.erb +3 -0
- data/lib/nines/http_check.rb +42 -0
- data/lib/nines/logger.rb +26 -0
- data/lib/nines/notifier.rb +32 -0
- data/lib/nines/ping_check.rb +50 -0
- data/lib/nines/version.rb +3 -0
- data/log/.gitkeep +0 -0
- data/nines.gemspec +29 -0
- data/nines.rb.sample +56 -0
- data/nines.yml.sample +83 -0
- data/tmp/.gitkeep +0 -0
- metadata +132 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
ruby_string="ruby-1.9.3-p327"
|
4
|
+
gemset_name="nines"
|
5
|
+
|
6
|
+
alias rails='bundle exec rails'
|
7
|
+
|
8
|
+
if rvm list strings | grep -q "${ruby_string}" ; then
|
9
|
+
|
10
|
+
# Load or create the specified environment
|
11
|
+
if [[ -d "${rvm_path:-$HOME/.rvm}/environments" \
|
12
|
+
&& -s "${rvm_path:-$HOME/.rvm}/environments/${ruby_string}@${gemset_name}" ]] ; then
|
13
|
+
\. "${rvm_path:-$HOME/.rvm}/environments/${ruby_string}@${gemset_name}"
|
14
|
+
else
|
15
|
+
rvm --create "${ruby_string}@${gemset_name}"
|
16
|
+
fi
|
17
|
+
|
18
|
+
# (
|
19
|
+
# # Ensure that Bundler is installed, install it if it is not.
|
20
|
+
# if ! command -v bundle ; then
|
21
|
+
# gem install bundler
|
22
|
+
# fi
|
23
|
+
#
|
24
|
+
# # Bundle while reducing excess noise.
|
25
|
+
# bundle | grep -v 'Using' | grep -v 'complete' | sed '/^$/d'
|
26
|
+
# )&
|
27
|
+
|
28
|
+
else
|
29
|
+
|
30
|
+
# Notify the user to install the desired interpreter before proceeding.
|
31
|
+
echo "${ruby_string} was not found, please run 'rvm install ${ruby_string}' and then cd back into the project directory."
|
32
|
+
|
33
|
+
fi
|
data/MIT-LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Aaron Namba <aaron@biggerbird.com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
=nines
|
2
|
+
|
3
|
+
Nines is a simple server monitoring tool written in Ruby. It reads in hand-coded YAML config files (see config.yml.sample). Rename to config.yml and edit as needed before first run.
|
4
|
+
|
5
|
+
When run, it forks into the background and runs in a continuous loop. If there are bugs in the code (likely) it may die, so keep it running with monit, init, etc.
|
6
|
+
|
7
|
+
=Usage
|
8
|
+
|
9
|
+
git clone git://github.com/anamba/nines.git && cd nines && bundle install && bundle exec ./nines
|
10
|
+
To stop: bundle exec ./nines stop
|
11
|
+
|
12
|
+
=Dependencies
|
13
|
+
|
14
|
+
Developed and tested with MRI ruby 1.9.3.
|
15
|
+
|
16
|
+
Dependencies:
|
17
|
+
* trollop (commandline options)
|
18
|
+
* net-ping (http/ping testing)
|
19
|
+
* dnsruby (dns resolution)
|
20
|
+
* mail (email)
|
21
|
+
|
22
|
+
=License & Copyright
|
23
|
+
|
24
|
+
Distributed under MIT license. Copyright (c) 2012 Aaron Namba <aaron@biggerbird.com>
|
data/bin/nines
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'trollop'
|
4
|
+
require 'nines/version'
|
5
|
+
|
6
|
+
opts = Trollop::options do
|
7
|
+
version "nines #{Nines::VERSION} (c) Aaron Namba"
|
8
|
+
banner "nines #{Nines::VERSION} (c) Aaron Namba\nNote: Command line parameters override config file values."
|
9
|
+
|
10
|
+
opt 'config-file', "Path to YAML configuration file", :short => '-f', :default => 'nines.yml'
|
11
|
+
opt 'verbose', "Enable detailed logging", :type => :boolean
|
12
|
+
opt 'debug', "Run each check once, then exit", :type => :boolean
|
13
|
+
|
14
|
+
stop_on [ 'start', 'stop' ]
|
15
|
+
end
|
16
|
+
|
17
|
+
# absolutize config file path
|
18
|
+
opts['config-file'] = opts['config-file'] =~ /^\// ? opts['config-file'] : File.expand_path(opts['config-file'], Dir.pwd)
|
19
|
+
|
20
|
+
unless File.exists?(opts['config-file'])
|
21
|
+
puts "Config file #{opts['config-file']} not found (or not accessible)"
|
22
|
+
exit 1
|
23
|
+
end
|
24
|
+
|
25
|
+
# args seem okay, load up the app
|
26
|
+
require 'nines'
|
27
|
+
|
28
|
+
# instantiate Nines::App using config file
|
29
|
+
app = Nines::App.new(File.expand_path(opts['config-file']))
|
30
|
+
Nines::App.debug ||= opts['debug']
|
31
|
+
Nines::App.verbose ||= opts['verbose']
|
32
|
+
|
33
|
+
unless Nines::App.debug
|
34
|
+
unless app.logfile_writable
|
35
|
+
puts "Couldn't open #{app.logfile} for logging"
|
36
|
+
exit 1
|
37
|
+
end
|
38
|
+
unless app.pidfile_writable
|
39
|
+
puts "Couldn't write pid to #{app.pidfile}"
|
40
|
+
exit 1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# process subcommands
|
45
|
+
cmd = ARGV.shift
|
46
|
+
case cmd
|
47
|
+
when 'start'
|
48
|
+
cmd_opts = Trollop.options do
|
49
|
+
end
|
50
|
+
|
51
|
+
app.start(cmd_opts)
|
52
|
+
|
53
|
+
when 'stop'
|
54
|
+
cmd_opts = Trollop.options do
|
55
|
+
end
|
56
|
+
|
57
|
+
app.stop(cmd_opts)
|
58
|
+
|
59
|
+
else
|
60
|
+
app.start
|
61
|
+
end
|
data/doc/logrotate.conf
ADDED
data/install.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'rbconfig'
|
2
|
+
require 'find'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
include RbConfig
|
6
|
+
|
7
|
+
$ruby = CONFIG['ruby_install_name']
|
8
|
+
|
9
|
+
##
|
10
|
+
# Install a binary file. We patch in on the way through to
|
11
|
+
# insert a #! line. If this is a Unix install, we name
|
12
|
+
# the command (for example) 'rake' and let the shebang line
|
13
|
+
# handle running it. Under windows, we add a '.rb' extension
|
14
|
+
# and let file associations to their stuff
|
15
|
+
#
|
16
|
+
|
17
|
+
def installBIN(from, opfile)
|
18
|
+
|
19
|
+
tmp_dir = nil
|
20
|
+
for t in [".", "/tmp", "c:/temp", $bindir]
|
21
|
+
stat = File.stat(t) rescue next
|
22
|
+
if stat.directory? and stat.writable?
|
23
|
+
tmp_dir = t
|
24
|
+
break
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
fail "Cannot find a temporary directory" unless tmp_dir
|
29
|
+
tmp_file = File.join(tmp_dir, "_tmp")
|
30
|
+
|
31
|
+
File.open(from) do |ip|
|
32
|
+
File.open(tmp_file, "w") do |op|
|
33
|
+
ruby = File.join($realbindir, $ruby)
|
34
|
+
op.puts "#!#{ruby} -w"
|
35
|
+
op.write ip.read
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
opfile += ".rb" if CONFIG["target_os"] =~ /mswin/i
|
40
|
+
FileUtils.install(tmp_file, File.join($bindir, opfile),
|
41
|
+
{:mode => 0755, :verbose => true})
|
42
|
+
File.unlink(tmp_file)
|
43
|
+
end
|
44
|
+
|
45
|
+
$sitedir = CONFIG["sitelibdir"]
|
46
|
+
unless $sitedir
|
47
|
+
version = CONFIG["MAJOR"]+"."+CONFIG["MINOR"]
|
48
|
+
$libdir = File.join(CONFIG["libdir"], "ruby", version)
|
49
|
+
$sitedir = $:.find {|x| x =~ /site_ruby/}
|
50
|
+
if !$sitedir
|
51
|
+
$sitedir = File.join($libdir, "site_ruby")
|
52
|
+
elsif $sitedir !~ Regexp.quote(version)
|
53
|
+
$sitedir = File.join($sitedir, version)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
$bindir = CONFIG["bindir"]
|
58
|
+
|
59
|
+
$realbindir = $bindir
|
60
|
+
|
61
|
+
bindir = CONFIG["bindir"]
|
62
|
+
if (destdir = ENV['DESTDIR'])
|
63
|
+
$bindir = destdir + $bindir
|
64
|
+
$sitedir = destdir + $sitedir
|
65
|
+
|
66
|
+
FileUtils.mkdir_p($bindir)
|
67
|
+
FileUtils.mkdir_p($sitedir)
|
68
|
+
end
|
69
|
+
|
70
|
+
rake_dest = File.join($sitedir, "rake")
|
71
|
+
FileUtils.mkdir_p(rake_dest, {:verbose => true})
|
72
|
+
File.chmod(0755, rake_dest)
|
73
|
+
|
74
|
+
# The library files
|
75
|
+
|
76
|
+
files = Dir.chdir('lib') { Dir['**/*.rb'].sort }
|
77
|
+
|
78
|
+
for fn in files
|
79
|
+
fn_dir = File.dirname(fn)
|
80
|
+
target_dir = File.join($sitedir, fn_dir)
|
81
|
+
if ! File.exist?(target_dir)
|
82
|
+
FileUtils.mkdir_p(target_dir)
|
83
|
+
end
|
84
|
+
FileUtils.install(File.join('lib', fn), File.join($sitedir, fn),
|
85
|
+
{:mode => 0644, :verbose => true})
|
86
|
+
end
|
87
|
+
|
88
|
+
# and the executable
|
89
|
+
|
90
|
+
installBIN("bin/nines", "nines")
|
data/lib/nines/app.rb
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'erb'
|
3
|
+
require 'dnsruby'
|
4
|
+
require 'mail'
|
5
|
+
|
6
|
+
module Nines
|
7
|
+
class App
|
8
|
+
class << self
|
9
|
+
attr_accessor :root, :config, :continue,
|
10
|
+
:debug, :verbose, :logfile, :pidfile, :logger, :notifier,
|
11
|
+
:email_from, :email_subject_prefix
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(config_file)
|
15
|
+
self.class.root = File.expand_path('../../../', __FILE__)
|
16
|
+
|
17
|
+
# load config files
|
18
|
+
case File.extname(config_file)
|
19
|
+
when '.yml' then self.class.config = YAML.load(ERB.new(File.read(config_file)).result)
|
20
|
+
when '.rb' then require config_file
|
21
|
+
end
|
22
|
+
self.class.config = stringify_keys_and_symbols(self.class.config)
|
23
|
+
|
24
|
+
# set main parameters
|
25
|
+
self.class.debug = config['debug']
|
26
|
+
self.class.verbose = config['verbose']
|
27
|
+
|
28
|
+
self.class.logfile = config['logfile'] || 'nines.log'
|
29
|
+
self.class.pidfile = config['pidfile'] || 'nines.pid'
|
30
|
+
self.class.email_from = config['email_from'] || 'Nines Notifier <no-reply@example.com>'
|
31
|
+
self.class.email_subject_prefix = config['email_subject_prefix'] || ''
|
32
|
+
end
|
33
|
+
|
34
|
+
# shortcuts
|
35
|
+
def config ; self.class.config ; end
|
36
|
+
def logfile ; self.class.logfile ; end
|
37
|
+
def pidfile ; self.class.pidfile ; end
|
38
|
+
def debug ; self.class.debug ; end
|
39
|
+
def logger ; self.class.logger ; end
|
40
|
+
|
41
|
+
def logfile_writable
|
42
|
+
begin
|
43
|
+
File.open(logfile, 'a') { }
|
44
|
+
true
|
45
|
+
rescue Exception => e
|
46
|
+
puts "Exception: #{e}"
|
47
|
+
false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def pidfile_writable
|
52
|
+
begin
|
53
|
+
File.open(pidfile, 'a') { }
|
54
|
+
true
|
55
|
+
rescue Exception => e
|
56
|
+
puts "Exception: #{e}"
|
57
|
+
false
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# make sure you're not using OpenDNS or something else that resolves invalid names
|
62
|
+
def check_hostnames
|
63
|
+
all_good = true
|
64
|
+
|
65
|
+
@check_groups.each do |group|
|
66
|
+
group.checks.each do |check|
|
67
|
+
unless check.hostname && Dnsruby::Resolv.getaddress(check.hostname)
|
68
|
+
puts "Error: check #{check.name} has invalid hostname '#{check.hostname}'"
|
69
|
+
all_good = false
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
all_good
|
75
|
+
end
|
76
|
+
|
77
|
+
def configure_smtp
|
78
|
+
if config['smtp'].is_a?(Hash)
|
79
|
+
Mail.defaults do
|
80
|
+
delivery_method :smtp, {
|
81
|
+
:address => Nines::App.config['smtp']['address'] || 'localhost',
|
82
|
+
:port => Nines::App.config['smtp']['port'] || 25,
|
83
|
+
:domain => Nines::App.config['smtp']['domain'],
|
84
|
+
:user_name => Nines::App.config['smtp']['user_name'],
|
85
|
+
:password => Nines::App.config['smtp']['password'],
|
86
|
+
:authentication => (Nines::App.config['smtp']['authentication'] || 'plain').to_sym,
|
87
|
+
:enable_starttls_auto => Nines::App.config['smtp']['enable_starttls_auto'],
|
88
|
+
:tls => Nines::App.config['smtp']['tls'],
|
89
|
+
}
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def stringify_keys_and_symbols(obj)
|
95
|
+
case obj.class.to_s
|
96
|
+
when 'Array'
|
97
|
+
obj.map! { |el| stringify_keys_and_symbols(el) }
|
98
|
+
when 'Hash'
|
99
|
+
obj.stringify_keys!
|
100
|
+
obj.each { |k,v| obj[k] = stringify_keys_and_symbols(v) }
|
101
|
+
when 'Symbol'
|
102
|
+
obj = obj.to_s
|
103
|
+
end
|
104
|
+
|
105
|
+
obj
|
106
|
+
end
|
107
|
+
|
108
|
+
def start(options = {})
|
109
|
+
# set up logger
|
110
|
+
self.class.logger = Logger.new(debug ? STDOUT : File.open(logfile, 'a'))
|
111
|
+
logger.sync = 1 # makes it possible to tail the logfile
|
112
|
+
|
113
|
+
# use it
|
114
|
+
logger.puts "[#{Time.now}] - nines starting"
|
115
|
+
|
116
|
+
# set up notifier
|
117
|
+
configure_smtp
|
118
|
+
self.class.notifier = Notifier.new(config['contacts'])
|
119
|
+
|
120
|
+
# set up check_groups (uses logger and notifier)
|
121
|
+
if !config['check_groups'].is_a?(Array) || config['check_groups'].empty?
|
122
|
+
raise Exception.new("No check groups configured, nothing to do.")
|
123
|
+
end
|
124
|
+
|
125
|
+
@check_groups = []
|
126
|
+
config['check_groups'].each do |options|
|
127
|
+
@check_groups << CheckGroup.new(options)
|
128
|
+
end
|
129
|
+
|
130
|
+
# TODO: this is a little awkwardly placed, but can fix later
|
131
|
+
unless check_hostnames
|
132
|
+
puts "Invalid hostnames found in config file"
|
133
|
+
exit 1
|
134
|
+
end
|
135
|
+
|
136
|
+
# fork and detach
|
137
|
+
if pid = fork
|
138
|
+
File.open(pidfile, 'w') { |f| f.print pid }
|
139
|
+
puts "Background process started with pid #{pid} (end it using `#{$0} stop`)"
|
140
|
+
puts "Debug mode enabled, background process will log to STDOUT and exit after running each check once." if debug
|
141
|
+
exit 0
|
142
|
+
end
|
143
|
+
|
144
|
+
#
|
145
|
+
# rest of this method runs as background process
|
146
|
+
#
|
147
|
+
|
148
|
+
# trap signals before spawning threads
|
149
|
+
self.class.continue = true
|
150
|
+
trap("INT") { Nines::App.continue = false ; puts "Caught SIGINT, will exit after current checks complete or time out." }
|
151
|
+
trap("TERM") { Nines::App.continue = false ; puts "Caught SIGTERM, will exit after current checks complete or time out." }
|
152
|
+
|
153
|
+
# iterate through config, spawning check threads as we go
|
154
|
+
@threads = []
|
155
|
+
|
156
|
+
@check_groups.each do |group|
|
157
|
+
group.checks.each do |check|
|
158
|
+
@threads << Thread.new(Thread.current) { |parent|
|
159
|
+
begin
|
160
|
+
check.run
|
161
|
+
rescue Exception => e
|
162
|
+
parent.raise e
|
163
|
+
end
|
164
|
+
}
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
@threads.each { |t| t.join if t.alive? }
|
169
|
+
|
170
|
+
logger.puts "[#{Time.now}] - nines finished"
|
171
|
+
logger.close
|
172
|
+
|
173
|
+
puts "Background process finished"
|
174
|
+
end
|
175
|
+
|
176
|
+
def stop(options = {})
|
177
|
+
begin
|
178
|
+
pid = File.read(self.class.pidfile).to_i
|
179
|
+
rescue Errno::ENOENT => e
|
180
|
+
STDERR.puts "Couldn't open pid file #{self.class.pidfile}, please check your config."
|
181
|
+
exit 1
|
182
|
+
end
|
183
|
+
|
184
|
+
begin
|
185
|
+
Process.kill "INT", pid
|
186
|
+
exit 0
|
187
|
+
rescue Errno::ESRCH => e
|
188
|
+
STDERR.puts "Couldn't kill process with pid #{pid}. Are you sure it's running?"
|
189
|
+
exit 1
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
end
|
data/lib/nines/check.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
module Nines
|
2
|
+
class Check
|
3
|
+
attr_accessor :group, :name, :hostname, :address, :timeout, :port, :interval, :logger, :notifier, :up, :since, :cycles
|
4
|
+
|
5
|
+
def initialize(group, options)
|
6
|
+
@group = group
|
7
|
+
@hostname = options['hostname']
|
8
|
+
@name = options['name'] || @hostname
|
9
|
+
@timeout = options['timeout_sec'] || 10
|
10
|
+
@port = options['port']
|
11
|
+
@interval = options['interval_sec'] || 60
|
12
|
+
|
13
|
+
@logger = Nines::App.logger || STDOUT
|
14
|
+
@notifier = Nines::App.notifier
|
15
|
+
|
16
|
+
@times_notified = {}
|
17
|
+
@up = true
|
18
|
+
@since = Time.now.utc
|
19
|
+
@cycles = 0
|
20
|
+
end
|
21
|
+
|
22
|
+
def log_status(up, description)
|
23
|
+
if up
|
24
|
+
logger.puts "[#{Time.now}] - #{name} - Check passed: #{description}"
|
25
|
+
|
26
|
+
case @up
|
27
|
+
when true
|
28
|
+
@cycles += 1
|
29
|
+
when false
|
30
|
+
@up = true
|
31
|
+
@since = Time.now.utc
|
32
|
+
@cycles = 0
|
33
|
+
|
34
|
+
# back up notification
|
35
|
+
if notifier
|
36
|
+
@times_notified.keys.each do |contact_name|
|
37
|
+
logger.puts "[#{Time.now}] - #{name} - UP again, notifying contact '#{contact_name}'"
|
38
|
+
notifier.notify!(contact_name, self)
|
39
|
+
end
|
40
|
+
@times_notified = {}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
else
|
44
|
+
logger.puts "[#{Time.now}] - #{name} - Check FAILED: #{description}"
|
45
|
+
|
46
|
+
case @up
|
47
|
+
when false
|
48
|
+
@cycles += 1
|
49
|
+
when true
|
50
|
+
@up = false
|
51
|
+
@since = Time.now.utc
|
52
|
+
@cycles = 0
|
53
|
+
end
|
54
|
+
|
55
|
+
if notifier && to_notify = group.contacts_to_notify(@cycles, @times_notified)
|
56
|
+
to_notify.each do |contact_name|
|
57
|
+
logger.puts "[#{Time.now}] - #{name} - Notifying contact '#{contact_name}'"
|
58
|
+
notifier.notify!(contact_name, self)
|
59
|
+
@times_notified[contact_name] ||= 0
|
60
|
+
@times_notified[contact_name] += 1
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Nines
|
2
|
+
class CheckGroup
|
3
|
+
attr_accessor :contacts, :checks
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
options.stringify_keys!
|
7
|
+
@name = options['name']
|
8
|
+
@description = options['description']
|
9
|
+
|
10
|
+
parameters = options['parameters'] || {}
|
11
|
+
parameters.stringify_keys!
|
12
|
+
@check_type = parameters.delete('type')
|
13
|
+
|
14
|
+
@contacts = []
|
15
|
+
notify = options['notify'] || []
|
16
|
+
notify.each do |contact|
|
17
|
+
contact.stringify_keys!
|
18
|
+
@contacts << {
|
19
|
+
'name' => contact['contact'],
|
20
|
+
'after' => contact['after'] || 2,
|
21
|
+
'every' => contact['every'] || 5,
|
22
|
+
'upto' => contact['upto'] || 5
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
@checks = []
|
27
|
+
checks = options['checks'] || []
|
28
|
+
checks.each do |check|
|
29
|
+
check = { hostname: check } unless check.is_a?(Hash)
|
30
|
+
check.stringify_keys!
|
31
|
+
case @check_type
|
32
|
+
when 'http'
|
33
|
+
@checks << HttpCheck.new(self, parameters.merge(check))
|
34
|
+
when 'ping'
|
35
|
+
@checks << PingCheck.new(self, parameters.merge(check))
|
36
|
+
else
|
37
|
+
raise Exception.new("Unknown check type: #{@check_type} (supported values: http, ping)")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# times_notified must be a hash with contact names as keys
|
43
|
+
def contacts_to_notify(cycles, times_notified)
|
44
|
+
# cycles starts at 0, but user generally expects first down event to be cycle 1
|
45
|
+
|
46
|
+
to_notify = []
|
47
|
+
@contacts.each do |contact|
|
48
|
+
next if times_notified[contact['name']].to_i >= contact['upto']
|
49
|
+
next if (cycles+1) < contact['after']
|
50
|
+
next if (cycles+1 - contact['after']) % contact['every'] != 0
|
51
|
+
to_notify << contact['name']
|
52
|
+
end
|
53
|
+
|
54
|
+
to_notify
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'net/ping'
|
2
|
+
require 'dnsruby'
|
3
|
+
|
4
|
+
module Nines
|
5
|
+
class HttpCheck < Check
|
6
|
+
attr_accessor :uri, :up_statuses, :user_agent
|
7
|
+
|
8
|
+
def initialize(group, options)
|
9
|
+
super(group, options)
|
10
|
+
|
11
|
+
@uri = options['uri'] || "http://#{hostname}:#{port}/"
|
12
|
+
@up_statuses = options['up_statuses'] || [ 200 ]
|
13
|
+
@user_agent = options['user_agent'] || "nines/1.0"
|
14
|
+
end
|
15
|
+
|
16
|
+
# shortcuts
|
17
|
+
def debug ; Nines::App.debug ; end
|
18
|
+
|
19
|
+
def run
|
20
|
+
while Nines::App.continue do
|
21
|
+
check_started = Time.now
|
22
|
+
@address = Dnsruby::Resolv.getaddress(hostname)
|
23
|
+
|
24
|
+
@pinger = Net::Ping::HTTP.new(uri, port, timeout)
|
25
|
+
@pinger.user_agent = user_agent
|
26
|
+
|
27
|
+
# the check
|
28
|
+
log_status(@pinger.ping?, "#{uri} (#{address})#{@pinger.warning ? " [warning: #{@pinger.warning}]" : ''}")
|
29
|
+
|
30
|
+
break if debug
|
31
|
+
|
32
|
+
wait = interval.to_f - (Time.now - check_started)
|
33
|
+
while wait > 0 do
|
34
|
+
break unless Nines::App.continue
|
35
|
+
sleep [1, wait].min
|
36
|
+
wait -= 1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
data/lib/nines/logger.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Nines
|
2
|
+
class Logger
|
3
|
+
|
4
|
+
def initialize(io)
|
5
|
+
@mutex = Mutex.new
|
6
|
+
@io = io
|
7
|
+
end
|
8
|
+
|
9
|
+
def sync ; @io.sync ; end
|
10
|
+
def sync=(val) ; @io.sync = val ; end
|
11
|
+
|
12
|
+
def puts(*args)
|
13
|
+
@mutex.synchronize { @io.puts args }
|
14
|
+
end
|
15
|
+
alias_method :error, :puts
|
16
|
+
|
17
|
+
def debug(*args)
|
18
|
+
@mutex.synchronize { @io.puts args } if Nines::App.verbose
|
19
|
+
end
|
20
|
+
|
21
|
+
def close
|
22
|
+
@io.close unless @io == STDOUT || @io == STDERR
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'mail'
|
2
|
+
|
3
|
+
module Nines
|
4
|
+
class Notifier
|
5
|
+
|
6
|
+
def initialize(contacts)
|
7
|
+
@contacts = contacts
|
8
|
+
end
|
9
|
+
|
10
|
+
def notify!(contact_name, check, details = '')
|
11
|
+
contact = @contacts[contact_name]
|
12
|
+
email_body = ERB.new(File.open(Nines::App.root + '/lib/nines/email_templates/notification.text.erb').read).result(binding)
|
13
|
+
|
14
|
+
Mail.deliver do
|
15
|
+
from Nines::App.email_from
|
16
|
+
to contact['email']
|
17
|
+
subject "#{Nines::App.email_subject_prefix}#{check.name} is #{check.up ? 'UP' : 'DOWN'}"
|
18
|
+
body email_body
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def human_duration(seconds)
|
23
|
+
case
|
24
|
+
when seconds < 60 then "#{seconds} sec"
|
25
|
+
when seconds < 3600 then "#{seconds/60} min #{seconds%60} sec"
|
26
|
+
when seconds < 86400 then "#{seconds/3600} hr #{seconds%3600/60} min #{seconds%60} sec"
|
27
|
+
else "#{seconds/86400} day #{seconds%86400/3600} hr #{seconds%3600/60} min #{seconds%60} sec"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'net/ping'
|
2
|
+
require 'dnsruby'
|
3
|
+
|
4
|
+
module Nines
|
5
|
+
class PingCheck < Check
|
6
|
+
attr_accessor :protocol
|
7
|
+
|
8
|
+
def initialize(group, options)
|
9
|
+
super(group, options)
|
10
|
+
|
11
|
+
@protocol = (options['protocol'] || 'icmp').downcase
|
12
|
+
end
|
13
|
+
|
14
|
+
# shortcuts
|
15
|
+
def debug ; Nines::App.debug ; end
|
16
|
+
|
17
|
+
def run
|
18
|
+
while Nines::App.continue do
|
19
|
+
check_started = Time.now
|
20
|
+
@address = Dnsruby::Resolv.getaddress(hostname)
|
21
|
+
|
22
|
+
@pinger = case protocol
|
23
|
+
when 'tcp' then Net::Ping::TCP.new(hostname, nil, timeout)
|
24
|
+
when 'udp' then Net::Ping::UDP.new(hostname, nil, timeout)
|
25
|
+
when 'icmp'
|
26
|
+
if Process::UID == 0
|
27
|
+
Net::Ping::ICMP.new(hostname, nil, timeout)
|
28
|
+
else
|
29
|
+
Net::Ping::External.new(hostname, nil, timeout)
|
30
|
+
end
|
31
|
+
else "invalid ping protocol #{protocol}"
|
32
|
+
end
|
33
|
+
|
34
|
+
# the check
|
35
|
+
log_status(@pinger.ping?, "#{protocol == 'icmp' ? 'icmp' : "#{protocol}/#{port}"} ping on #{hostname} (#{address})")
|
36
|
+
|
37
|
+
break if debug
|
38
|
+
|
39
|
+
wait = interval.to_f - (Time.now - check_started)
|
40
|
+
while wait > 0 do
|
41
|
+
break unless Nines::App.continue
|
42
|
+
sleep [1, wait].min
|
43
|
+
wait -= 1
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
data/log/.gitkeep
ADDED
File without changes
|
data/nines.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
require "nines/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.platform = Gem::Platform::RUBY
|
6
|
+
s.name = 'nines'
|
7
|
+
s.version = Nines::VERSION
|
8
|
+
s.summary = 'Simple server monitoring tool written in pure Ruby.'
|
9
|
+
s.description = 'Nines is a simple server monitoring tool written in Ruby.'
|
10
|
+
|
11
|
+
s.required_ruby_version = '>= 1.9.3'
|
12
|
+
s.required_rubygems_version = '>= 1.8.11'
|
13
|
+
|
14
|
+
s.author = "Aaron Namba"
|
15
|
+
s.email = "aaron@biggerbird.com"
|
16
|
+
s.homepage = "https://github.com/anamba/nines"
|
17
|
+
s.license = 'MIT'
|
18
|
+
|
19
|
+
s.bindir = 'bin'
|
20
|
+
s.files = `git ls-files`.split("\n")
|
21
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
22
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
23
|
+
s.require_paths = ["lib"]
|
24
|
+
|
25
|
+
s.add_dependency 'net-ping', '~> 1.5.3'
|
26
|
+
s.add_dependency 'dnsruby', '~> 1.53'
|
27
|
+
s.add_dependency 'mail', '~> 2.4'
|
28
|
+
s.add_dependency 'trollop', '~> 2.0'
|
29
|
+
end
|
data/nines.rb.sample
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
Nines::App.config = {
|
2
|
+
debug: false,
|
3
|
+
verbose: false,
|
4
|
+
|
5
|
+
logfile: 'log/nines.log',
|
6
|
+
pidfile: 'tmp/nines.pid',
|
7
|
+
email_from: 'Nines Notifier <no-reply@example.com>',
|
8
|
+
email_subject_prefix: '[NINES] ',
|
9
|
+
|
10
|
+
contacts: {
|
11
|
+
'admin-standard' => { email: 'admin@example.com' },
|
12
|
+
'admin-urgent' => { email: 'admin@example.com', twitter: 'admin_oncall', sms: '19875551234', phone: '19875554321' }
|
13
|
+
},
|
14
|
+
|
15
|
+
check_groups: [
|
16
|
+
{ name: 'Web Sites - High Priority',
|
17
|
+
description: 'Check often, notify immediately',
|
18
|
+
parameters: { type: :http, port: 80, timeout_sec: 5, interval_sec: 30 },
|
19
|
+
notify: [ { contact: 'admin-urgent', after: 2, every: 2, upto: 100 } ],
|
20
|
+
checks: [ 'www.corporation.com', 'www.clientabc.com' ]
|
21
|
+
},
|
22
|
+
{ name: 'Web Sites - Low Priority',
|
23
|
+
description: 'Check infrequently, use urgent contact only after extended downtime',
|
24
|
+
parameters: { type: :http, port: 80, timeout_sec: 15, interval_sec: 300 },
|
25
|
+
notify: [
|
26
|
+
{ contact: 'admin-standard', after: 2, every: 2, upto: 10 },
|
27
|
+
{ contact: 'admin-urgent', after: 25, every: 10, upto: 10 }
|
28
|
+
],
|
29
|
+
checks: [
|
30
|
+
'blog.johndoe.name',
|
31
|
+
{ name: 'Blog Redirect', hostname: 'www.oldblogsite.com', up_statuses: [ 301 ] }
|
32
|
+
]
|
33
|
+
},
|
34
|
+
{ name: 'Servers',
|
35
|
+
description: 'Simple ping tests',
|
36
|
+
parameters: { type: :ping, protocol: :icmp, interval_sec: 60 },
|
37
|
+
notify: [
|
38
|
+
{ contact: 'admin-standard', after: 2, every: 2, upto: 10 },
|
39
|
+
{ contact: 'admin-urgent', after: 25, every: 10, upto: 10 }
|
40
|
+
],
|
41
|
+
checks: [
|
42
|
+
'web1.hostingco.com',
|
43
|
+
{ name: 'web1 admin IP', hostname: 'web1.hostingco.net' }
|
44
|
+
]
|
45
|
+
},
|
46
|
+
],
|
47
|
+
|
48
|
+
smtp: {
|
49
|
+
address: 'smtp.sendgrid.net',
|
50
|
+
port: 587,
|
51
|
+
user_name: 'myusername',
|
52
|
+
password: 'mypassword',
|
53
|
+
authentication: 'plain',
|
54
|
+
enable_starttls_auto: true
|
55
|
+
}
|
56
|
+
}
|
data/nines.yml.sample
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
---
|
2
|
+
debug: false
|
3
|
+
verbose: false
|
4
|
+
|
5
|
+
logfile: log/nines.log
|
6
|
+
pidfile: tmp/nines.pid
|
7
|
+
email_from: 'Nines Notifier <no-reply@calchost.com>'
|
8
|
+
email_subject_prefix: '[NINES] '
|
9
|
+
|
10
|
+
contacts:
|
11
|
+
admin-standard:
|
12
|
+
email: admin@example.com
|
13
|
+
admin-urgent:
|
14
|
+
email: admin@example.com
|
15
|
+
twitter: admin_oncall
|
16
|
+
sms: '19875551234'
|
17
|
+
phone: '19875554321'
|
18
|
+
|
19
|
+
check_groups:
|
20
|
+
- name: Web Sites - High Priority
|
21
|
+
description: Check often, notify immediately
|
22
|
+
parameters:
|
23
|
+
type: :http
|
24
|
+
port: 80
|
25
|
+
timeout_sec: 5
|
26
|
+
interval_sec: 30
|
27
|
+
notify:
|
28
|
+
- contact: admin-urgent
|
29
|
+
after: 2
|
30
|
+
every: 2
|
31
|
+
upto: 100
|
32
|
+
checks:
|
33
|
+
- www.corporation.com
|
34
|
+
- www.clientabc.com
|
35
|
+
- name: Web Sites - Low Priority
|
36
|
+
description: Check infrequently, use urgent contact only after extended downtime
|
37
|
+
parameters:
|
38
|
+
type: http
|
39
|
+
port: 80
|
40
|
+
timeout_sec: 15
|
41
|
+
interval_sec: 300
|
42
|
+
notify:
|
43
|
+
- contact: admin-standard
|
44
|
+
after: 2
|
45
|
+
every: 2
|
46
|
+
upto: 10
|
47
|
+
- contact: admin-urgent
|
48
|
+
after: 25
|
49
|
+
every: 10
|
50
|
+
upto: 10
|
51
|
+
checks:
|
52
|
+
- blog.johndoe.name
|
53
|
+
- name: Blog Redirect
|
54
|
+
hostname: www.oldblogsite.com
|
55
|
+
up_statuses:
|
56
|
+
- 301
|
57
|
+
- name: Servers
|
58
|
+
description: Simple ping tests
|
59
|
+
parameters:
|
60
|
+
type: ping
|
61
|
+
protocol: icmp
|
62
|
+
interval_sec: 60
|
63
|
+
notify:
|
64
|
+
- contact: admin-standard
|
65
|
+
after: 2
|
66
|
+
every: 2
|
67
|
+
upto: 10
|
68
|
+
- contact: admin-urgent
|
69
|
+
after: 25
|
70
|
+
every: 10
|
71
|
+
upto: 10
|
72
|
+
checks:
|
73
|
+
- web1.hostingco.com
|
74
|
+
- name: web1 admin IP
|
75
|
+
hostname: web1.hostingco.net
|
76
|
+
|
77
|
+
smtp:
|
78
|
+
address: smtp.sendgrid.net
|
79
|
+
port: 587
|
80
|
+
user_name: myusername
|
81
|
+
password: mypassword
|
82
|
+
authentication: plain
|
83
|
+
enable_starttls_auto: true
|
data/tmp/.gitkeep
ADDED
File without changes
|
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nines
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Aaron Namba
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-12-03 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: net-ping
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.5.3
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.5.3
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: dnsruby
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '1.53'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '1.53'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: mail
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.4'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.4'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: trollop
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '2.0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '2.0'
|
78
|
+
description: Nines is a simple server monitoring tool written in Ruby.
|
79
|
+
email: aaron@biggerbird.com
|
80
|
+
executables:
|
81
|
+
- nines
|
82
|
+
extensions: []
|
83
|
+
extra_rdoc_files: []
|
84
|
+
files:
|
85
|
+
- .gitignore
|
86
|
+
- .rvmrc
|
87
|
+
- MIT-LICENSE.txt
|
88
|
+
- README.rdoc
|
89
|
+
- bin/nines
|
90
|
+
- doc/logrotate.conf
|
91
|
+
- install.rb
|
92
|
+
- lib/nines.rb
|
93
|
+
- lib/nines/app.rb
|
94
|
+
- lib/nines/check.rb
|
95
|
+
- lib/nines/check_group.rb
|
96
|
+
- lib/nines/email_templates/notification.text.erb
|
97
|
+
- lib/nines/http_check.rb
|
98
|
+
- lib/nines/logger.rb
|
99
|
+
- lib/nines/notifier.rb
|
100
|
+
- lib/nines/ping_check.rb
|
101
|
+
- lib/nines/version.rb
|
102
|
+
- log/.gitkeep
|
103
|
+
- nines.gemspec
|
104
|
+
- nines.rb.sample
|
105
|
+
- nines.yml.sample
|
106
|
+
- tmp/.gitkeep
|
107
|
+
homepage: https://github.com/anamba/nines
|
108
|
+
licenses:
|
109
|
+
- MIT
|
110
|
+
post_install_message:
|
111
|
+
rdoc_options: []
|
112
|
+
require_paths:
|
113
|
+
- lib
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
115
|
+
none: false
|
116
|
+
requirements:
|
117
|
+
- - ! '>='
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: 1.9.3
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: 1.8.11
|
126
|
+
requirements: []
|
127
|
+
rubyforge_project:
|
128
|
+
rubygems_version: 1.8.24
|
129
|
+
signing_key:
|
130
|
+
specification_version: 3
|
131
|
+
summary: Simple server monitoring tool written in pure Ruby.
|
132
|
+
test_files: []
|