nines 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|