sensu 0.6.0-x86-mingw32
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 +5 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +58 -0
- data/MIT-LICENSE.txt +20 -0
- data/README.org +61 -0
- data/Rakefile +8 -0
- data/bin/sensu-api +9 -0
- data/bin/sensu-client +7 -0
- data/bin/sensu-server +9 -0
- data/lib/sensu.rb +4 -0
- data/lib/sensu/api.rb +138 -0
- data/lib/sensu/client.rb +104 -0
- data/lib/sensu/config.rb +62 -0
- data/lib/sensu/helpers.rb +18 -0
- data/lib/sensu/server.rb +186 -0
- data/sensu.gemspec +42 -0
- metadata +194 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
sensu (0.5.14)
|
5
|
+
amqp (= 0.7.4)
|
6
|
+
async_sinatra
|
7
|
+
em-hiredis
|
8
|
+
em-syslog
|
9
|
+
json
|
10
|
+
thin
|
11
|
+
uuidtools
|
12
|
+
|
13
|
+
GEM
|
14
|
+
remote: http://rubygems.org/
|
15
|
+
specs:
|
16
|
+
amqp (0.7.4)
|
17
|
+
eventmachine
|
18
|
+
async_sinatra (0.5.0)
|
19
|
+
rack (>= 1.2.1)
|
20
|
+
sinatra (>= 1.0)
|
21
|
+
callsite (0.0.6)
|
22
|
+
bundler (~> 1.0.0)
|
23
|
+
daemons (1.1.4)
|
24
|
+
em-hiredis (0.1.0)
|
25
|
+
hiredis (~> 0.3.0)
|
26
|
+
em-syslog (0.0.2)
|
27
|
+
eventmachine
|
28
|
+
em-ventually (0.1.2)
|
29
|
+
callsite (~> 0.0.5)
|
30
|
+
eventmachine
|
31
|
+
eventmachine (0.12.10)
|
32
|
+
hiredis (0.3.2)
|
33
|
+
json (1.6.1)
|
34
|
+
mime-types (1.16)
|
35
|
+
minitest (2.6.0)
|
36
|
+
rack (1.3.3)
|
37
|
+
rake (0.9.2)
|
38
|
+
rest-client (1.6.7)
|
39
|
+
mime-types (>= 1.16)
|
40
|
+
sinatra (1.2.6)
|
41
|
+
rack (~> 1.1)
|
42
|
+
tilt (< 2.0, >= 1.2.2)
|
43
|
+
thin (1.2.11)
|
44
|
+
daemons (>= 1.0.9)
|
45
|
+
eventmachine (>= 0.12.6)
|
46
|
+
rack (>= 1.0.0)
|
47
|
+
tilt (1.3.3)
|
48
|
+
uuidtools (2.1.2)
|
49
|
+
|
50
|
+
PLATFORMS
|
51
|
+
ruby
|
52
|
+
|
53
|
+
DEPENDENCIES
|
54
|
+
em-ventually
|
55
|
+
minitest
|
56
|
+
rake
|
57
|
+
rest-client
|
58
|
+
sensu!
|
data/MIT-LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Sonian Inc.
|
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.org
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
* Welcome to Sensu
|
2
|
+
Sensu is a monitoring system framework, handling the remote execution of checks.
|
3
|
+
|
4
|
+
Checks can utilize user created plugins, returning an exit status code and outputting to STDOUT.
|
5
|
+
|
6
|
+
Check results are handled by user created handlers.
|
7
|
+
|
8
|
+
Documentation can be found [[https://github.com/sonian/sensu/wiki][here]].
|
9
|
+
|
10
|
+
* License
|
11
|
+
Sensu is released under the [[https://github.com/sonian/sensu/blob/master/MIT-LICENSE.txt][MIT license]].
|
12
|
+
|
13
|
+
* Contributing
|
14
|
+
- [[http://help.github.com/fork-a-repo/][Fork]] [[https://github.com/sonian/sensu][Sensu]]
|
15
|
+
- Use a [[https://github.com/dchelimsky/rspec/wiki/Topic-Branches][topic branch]]
|
16
|
+
- Create a [[http://help.github.com/send-pull-requests/][pull request]]
|
17
|
+
|
18
|
+
Keep it simple.
|
19
|
+
|
20
|
+
** Readme Driven Development
|
21
|
+
*** A Client Will
|
22
|
+
- Have a set of attributes to describe it, including its responsibilities
|
23
|
+
- Send keep-alives to a server
|
24
|
+
- Subscribe to a queue bound to a set of fanout exchanges, determined by its responsibilities
|
25
|
+
- Substitute tokens in check commands with their matching client attribute
|
26
|
+
- Report when it does not have a client attribute for token substitution
|
27
|
+
- Receive checks from subscriptions, execute them, and publish the results to a queue with its client name
|
28
|
+
- Not allow overlapping check executions of the same name
|
29
|
+
- Report when it is unaware of a check it received from a subscription
|
30
|
+
|
31
|
+
*** A Server Will
|
32
|
+
- Subscribe to a queue for check results, another for keep-alives
|
33
|
+
- Pull keep-alives, storing client details
|
34
|
+
- Publish checks on defined intervals to their associated fanout exchanges
|
35
|
+
- Pull check results, storing the latest events for clients, a good result will flush a previous event for that client
|
36
|
+
- Create an event when it stops receiving keep-alives from a client, a new keep-alive for the client will clear the event
|
37
|
+
- Trigger a event handler, either the default handler or one specified for the check, providing it with a JSON event file
|
38
|
+
|
39
|
+
*** An API Will
|
40
|
+
- List all current events
|
41
|
+
- List all clients and their attributes
|
42
|
+
- Show a client and its attributes
|
43
|
+
- Remove a client and resolve associated events
|
44
|
+
|
45
|
+
*** A Plugin Will
|
46
|
+
- Output to STDOUT
|
47
|
+
- Return a valid exit status code
|
48
|
+
|
49
|
+
*** A Handler Will
|
50
|
+
- Accept a command line argument "-f", for an event file path
|
51
|
+
- Parse the JSON event file
|
52
|
+
- Handle the event as it wishes
|
53
|
+
|
54
|
+
** Testing
|
55
|
+
*** Requirements
|
56
|
+
- RabbitMQ (default configuration)
|
57
|
+
- Redis (default configuration)
|
58
|
+
|
59
|
+
*** MiniTest
|
60
|
+
|
61
|
+
: rake test
|
data/Rakefile
ADDED
data/bin/sensu-api
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
raise "The Sensu API does not run on Windows" if RUBY_PLATFORM.downcase =~ /mswin|mingw32|windows/
|
4
|
+
|
5
|
+
$: << File.dirname(__FILE__) + '/../lib' unless $:.include?(File.dirname(__FILE__) + '/../lib/')
|
6
|
+
require 'sensu/api'
|
7
|
+
|
8
|
+
options = Sensu::Config.read_arguments(ARGV)
|
9
|
+
Sensu::API.run(options)
|
data/bin/sensu-client
ADDED
data/bin/sensu-server
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
raise "The Sensu Server does not run on Windows" if RUBY_PLATFORM.downcase =~ /mswin|mingw32|windows/
|
4
|
+
|
5
|
+
$: << File.dirname(__FILE__) + '/../lib' unless $:.include?(File.dirname(__FILE__) + '/../lib/')
|
6
|
+
require 'sensu/server'
|
7
|
+
|
8
|
+
options = Sensu::Config.read_arguments(ARGV)
|
9
|
+
Sensu::Server.run(options)
|
data/lib/sensu.rb
ADDED
data/lib/sensu/api.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'config')
|
2
|
+
require 'sinatra/async'
|
3
|
+
require 'em-hiredis'
|
4
|
+
|
5
|
+
module Sensu
|
6
|
+
class API < Sinatra::Base
|
7
|
+
register Sinatra::Async
|
8
|
+
|
9
|
+
def self.run(options={})
|
10
|
+
EM.run do
|
11
|
+
self.setup(options)
|
12
|
+
self.run!(:port => @settings['api']['port'])
|
13
|
+
|
14
|
+
Signal.trap('INT') do
|
15
|
+
EM.stop
|
16
|
+
end
|
17
|
+
|
18
|
+
Signal.trap('TERM') do
|
19
|
+
EM.stop
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.setup(options={})
|
25
|
+
config = Sensu::Config.new(options)
|
26
|
+
@settings = config.settings
|
27
|
+
set :redis, EM::Hiredis.connect('redis://' + @settings['redis']['host'] + ':' + @settings['redis']['port'].to_s)
|
28
|
+
connection = AMQP.connect(symbolize_keys(@settings['rabbitmq']))
|
29
|
+
set :amq, MQ.new(connection)
|
30
|
+
end
|
31
|
+
|
32
|
+
helpers do
|
33
|
+
include Rack::Utils
|
34
|
+
alias_method :conn, :settings
|
35
|
+
end
|
36
|
+
|
37
|
+
before do
|
38
|
+
content_type 'application/json'
|
39
|
+
end
|
40
|
+
|
41
|
+
aget '/clients' do
|
42
|
+
current_clients = Array.new
|
43
|
+
conn.redis.smembers('clients').callback do |clients|
|
44
|
+
unless clients.empty?
|
45
|
+
clients.each_with_index do |client, index|
|
46
|
+
conn.redis.get('client:' + client).callback do |client_json|
|
47
|
+
current_clients.push(JSON.parse(client_json))
|
48
|
+
body current_clients.to_json if index == clients.size-1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
else
|
52
|
+
body current_clients.to_json
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
aget '/client/:id' do |client|
|
58
|
+
conn.redis.get('client:' + client).callback do |client_json|
|
59
|
+
status 404 if client_json.nil?
|
60
|
+
body client_json
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
aget '/events' do
|
65
|
+
current_events = Hash.new
|
66
|
+
conn.redis.smembers('clients').callback do |clients|
|
67
|
+
unless clients.empty?
|
68
|
+
clients.each_with_index do |client, index|
|
69
|
+
conn.redis.hgetall('events:' + client).callback do |events|
|
70
|
+
client_events = Hash[*events]
|
71
|
+
client_events.each do |key, value|
|
72
|
+
client_events[key] = JSON.parse(value)
|
73
|
+
end
|
74
|
+
current_events[client] = client_events unless client_events.empty?
|
75
|
+
body current_events.to_json if index == clients.size-1
|
76
|
+
end
|
77
|
+
end
|
78
|
+
else
|
79
|
+
body current_events.to_json
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
adelete '/client/:id' do |client|
|
85
|
+
conn.redis.sismember('clients', client).callback do |client_exists|
|
86
|
+
unless client_exists == 0
|
87
|
+
conn.redis.exists('events:' + client).callback do |events_exist|
|
88
|
+
unless events_exist == 0
|
89
|
+
conn.redis.hgetall('events:' + client).callback do |events|
|
90
|
+
Hash[*events].keys.each do |check_name|
|
91
|
+
check = {'name' => check_name, 'issued' => Time.now.to_i, 'status' => 0, 'output' => 'client is being removed...'}
|
92
|
+
conn.amq.queue('results').publish({'client' => client, 'check' => check}.to_json)
|
93
|
+
end
|
94
|
+
EM.add_timer(8) do
|
95
|
+
conn.redis.srem('clients', client)
|
96
|
+
conn.redis.del('events:' + client)
|
97
|
+
conn.redis.del('client:' + client)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
else
|
101
|
+
conn.redis.srem('clients', client)
|
102
|
+
conn.redis.del('events:' + client)
|
103
|
+
conn.redis.del('client:' + client)
|
104
|
+
end
|
105
|
+
status 204
|
106
|
+
body ''
|
107
|
+
end
|
108
|
+
else
|
109
|
+
status 404
|
110
|
+
body ''
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
apost '/test/client' do
|
116
|
+
attributes = '{
|
117
|
+
"name": "test",
|
118
|
+
"address": "localhost",
|
119
|
+
"subscriptions": [
|
120
|
+
"foo",
|
121
|
+
"bar"
|
122
|
+
]
|
123
|
+
}'
|
124
|
+
conn.redis.set('client:test', attributes).callback do
|
125
|
+
conn.redis.sadd('clients', 'test')
|
126
|
+
status 201
|
127
|
+
body ''
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
apost '/test/event' do
|
132
|
+
conn.redis.hset('events:test', 'test', {'status' => 2, 'output' => 'CRITICAL :: test', 'occurrences' => 1}.to_json).callback do
|
133
|
+
status 201
|
134
|
+
body ''
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
data/lib/sensu/client.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'config')
|
2
|
+
|
3
|
+
module Sensu
|
4
|
+
class Client
|
5
|
+
def self.run(options={})
|
6
|
+
EM.run do
|
7
|
+
client = self.new(options)
|
8
|
+
client.setup_amqp
|
9
|
+
client.setup_keepalives
|
10
|
+
client.setup_subscriptions
|
11
|
+
client.monitor_queues
|
12
|
+
|
13
|
+
Signal.trap('INT') do
|
14
|
+
EM.stop
|
15
|
+
end
|
16
|
+
|
17
|
+
Signal.trap('TERM') do
|
18
|
+
EM.stop
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(options={})
|
24
|
+
config = Sensu::Config.new(:config_file => options[:config_file])
|
25
|
+
@settings = config.settings
|
26
|
+
@checks_in_progress = Array.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def setup_amqp
|
30
|
+
connection = AMQP.connect(symbolize_keys(@settings['rabbitmq']))
|
31
|
+
@amq = MQ.new(connection)
|
32
|
+
@keepalive_queue = @amq.queue('keepalives')
|
33
|
+
@result_queue = @amq.queue('results')
|
34
|
+
end
|
35
|
+
|
36
|
+
def setup_keepalives
|
37
|
+
@keepalive_queue.publish(@settings['client'].merge({'timestamp' => Time.now.to_i}).to_json)
|
38
|
+
EM.add_periodic_timer(30) do
|
39
|
+
@keepalive_queue.publish(@settings['client'].merge({'timestamp' => Time.now.to_i}).to_json)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def execute_check(check)
|
44
|
+
result = {'client' => @settings['client']['name'], 'check' => check}
|
45
|
+
if @settings['checks'].has_key?(check['name'])
|
46
|
+
unless @checks_in_progress.include?(check['name'])
|
47
|
+
@checks_in_progress.push(check['name'])
|
48
|
+
unmatched_tokens = Array.new
|
49
|
+
command = @settings['checks'][check['name']]['command'].gsub(/:::(.*?):::/) do
|
50
|
+
key = $1.to_s
|
51
|
+
unmatched_tokens.push(key) unless @settings['client'].has_key?(key)
|
52
|
+
@settings['client'][key].to_s
|
53
|
+
end
|
54
|
+
if unmatched_tokens.empty?
|
55
|
+
execute = proc do
|
56
|
+
IO.popen(command + ' 2>&1') do |io|
|
57
|
+
result['check']['output'] = io.read
|
58
|
+
end
|
59
|
+
result['check']['status'] = $?.exitstatus
|
60
|
+
result
|
61
|
+
end
|
62
|
+
publish = proc do |result|
|
63
|
+
@result_queue.publish(result.to_json)
|
64
|
+
@checks_in_progress.delete(result['check']['name'])
|
65
|
+
end
|
66
|
+
EM.defer(execute, publish)
|
67
|
+
else
|
68
|
+
result['check']['status'] = 3
|
69
|
+
result['check']['output'] = 'Missing client attributes: ' + unmatched_tokens.join(', ')
|
70
|
+
@result_queue.publish(result.to_json)
|
71
|
+
@checks_in_progress.delete(check['name'])
|
72
|
+
end
|
73
|
+
end
|
74
|
+
else
|
75
|
+
result['check']['status'] = 3
|
76
|
+
result['check']['output'] = 'Unknown check'
|
77
|
+
@result_queue.publish(result.to_json)
|
78
|
+
@checks_in_progress.delete(check['name'])
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def setup_subscriptions
|
83
|
+
@check_queue = @amq.queue(UUIDTools::UUID.random_create.to_s, :exclusive => true)
|
84
|
+
@settings['client']['subscriptions'].each do |exchange|
|
85
|
+
@check_queue.bind(@amq.fanout(exchange))
|
86
|
+
end
|
87
|
+
@check_queue.subscribe do |check_json|
|
88
|
+
check = JSON.parse(check_json)
|
89
|
+
execute_check(check)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def monitor_queues
|
94
|
+
EM.add_periodic_timer(5) do
|
95
|
+
unless @check_queue.subscribed?
|
96
|
+
@check_queue.delete
|
97
|
+
EM.add_timer(1) do
|
98
|
+
setup_subscriptions
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
data/lib/sensu/config.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'rubygems' if RUBY_VERSION < '1.9.0'
|
2
|
+
require 'optparse'
|
3
|
+
require 'json'
|
4
|
+
require 'uuidtools'
|
5
|
+
require 'amqp'
|
6
|
+
require 'em/syslog'
|
7
|
+
require File.join(File.dirname(__FILE__), 'helpers')
|
8
|
+
|
9
|
+
module Sensu
|
10
|
+
class Config
|
11
|
+
attr_accessor :settings
|
12
|
+
|
13
|
+
def initialize(options={})
|
14
|
+
config_file = options[:config_file] || '/etc/sensu/config.json'
|
15
|
+
@settings = JSON.parse(File.open(config_file, 'r').read)
|
16
|
+
validate_config
|
17
|
+
end
|
18
|
+
|
19
|
+
def validate_config
|
20
|
+
@settings['checks'].each do |name, info|
|
21
|
+
unless info['interval'].is_a?(Integer) && info['interval'] > 0
|
22
|
+
raise 'configuration invalid, missing interval for check ' + name
|
23
|
+
end
|
24
|
+
unless info['command'].is_a?(String)
|
25
|
+
raise 'configuration invalid, missing command for check ' + name
|
26
|
+
end
|
27
|
+
unless info['subscribers'].is_a?(Array) && info['subscribers'].count > 0
|
28
|
+
raise 'configuration invalid, missing subscribers for check ' + name
|
29
|
+
end
|
30
|
+
end
|
31
|
+
unless @settings['client']['name'].is_a?(String)
|
32
|
+
raise 'configuration invalid, client must have a name'
|
33
|
+
end
|
34
|
+
unless @settings['client']['address'].is_a?(String)
|
35
|
+
raise 'configuration invalid, client must have an address (ip or hostname)'
|
36
|
+
end
|
37
|
+
unless @settings['client']['subscriptions'].is_a?(Array) && @settings['client']['subscriptions'].count > 0
|
38
|
+
raise 'configuration invalid, client must have subscriptions'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.read_arguments(arguments)
|
43
|
+
options = Hash.new
|
44
|
+
optparse = OptionParser.new do |opts|
|
45
|
+
opts.on('-h', '--help', 'Display this screen') do
|
46
|
+
puts opts
|
47
|
+
exit
|
48
|
+
end
|
49
|
+
options[:worker] = false
|
50
|
+
opts.on('-w', '--worker', 'Only consume jobs, no check publishing (default: false)') do
|
51
|
+
options[:worker] = true
|
52
|
+
end
|
53
|
+
options[:config_file] = nil
|
54
|
+
opts.on('-c', '--config FILE', 'Sensu JSON config FILE (default: /etc/sensu/config.json)') do |file|
|
55
|
+
options[:config_file] = file
|
56
|
+
end
|
57
|
+
end
|
58
|
+
optparse.parse!(arguments)
|
59
|
+
options
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
def symbolize_keys(item)
|
2
|
+
case item
|
3
|
+
when Array
|
4
|
+
item.map do |i|
|
5
|
+
symbolize_keys(i)
|
6
|
+
end
|
7
|
+
when Hash
|
8
|
+
Hash[
|
9
|
+
item.map do |key, value|
|
10
|
+
k = key.is_a?(String) ? key.to_sym : key
|
11
|
+
v = symbolize_keys(value)
|
12
|
+
[k,v]
|
13
|
+
end
|
14
|
+
]
|
15
|
+
else
|
16
|
+
item
|
17
|
+
end
|
18
|
+
end
|
data/lib/sensu/server.rb
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'config')
|
2
|
+
require 'em-hiredis'
|
3
|
+
|
4
|
+
module Sensu
|
5
|
+
class Server
|
6
|
+
attr_accessor :redis, :is_worker
|
7
|
+
alias :redis_connection :redis
|
8
|
+
|
9
|
+
def self.run(options={})
|
10
|
+
EM.threadpool_size = 15
|
11
|
+
EM.run do
|
12
|
+
server = self.new(options)
|
13
|
+
server.setup_logging
|
14
|
+
server.setup_redis
|
15
|
+
server.setup_amqp
|
16
|
+
server.setup_keepalives
|
17
|
+
server.setup_results
|
18
|
+
unless server.is_worker
|
19
|
+
server.setup_publisher
|
20
|
+
server.setup_keepalive_monitor
|
21
|
+
end
|
22
|
+
server.monitor_queues
|
23
|
+
|
24
|
+
Signal.trap('INT') do
|
25
|
+
EM.stop
|
26
|
+
end
|
27
|
+
|
28
|
+
Signal.trap('TERM') do
|
29
|
+
EM.stop
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(options={})
|
35
|
+
config = Sensu::Config.new(:config_file => options[:config_file])
|
36
|
+
@settings = config.settings
|
37
|
+
@is_worker = options[:worker]
|
38
|
+
end
|
39
|
+
|
40
|
+
def setup_logging
|
41
|
+
EM.syslog_setup(@settings['syslog']['host'], @settings['syslog']['port'])
|
42
|
+
end
|
43
|
+
|
44
|
+
def setup_redis
|
45
|
+
@redis = EM::Hiredis.connect('redis://' + @settings['redis']['host'] + ':' + @settings['redis']['port'].to_s)
|
46
|
+
end
|
47
|
+
|
48
|
+
def setup_amqp
|
49
|
+
connection = AMQP.connect(symbolize_keys(@settings['rabbitmq']))
|
50
|
+
@amq = MQ.new(connection)
|
51
|
+
end
|
52
|
+
|
53
|
+
def setup_keepalives
|
54
|
+
@keepalive_queue = @amq.queue('keepalives')
|
55
|
+
@keepalive_queue.subscribe do |keepalive_json|
|
56
|
+
client = JSON.parse(keepalive_json)['name']
|
57
|
+
@redis.set('client:' + client, keepalive_json).callback do
|
58
|
+
@redis.sadd('clients', client)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def handle_event(event)
|
64
|
+
handler = proc do
|
65
|
+
result = Hash.new
|
66
|
+
IO.popen(@settings['handlers'][event['check']['handler']] + ' 2>&1', 'r+') do |io|
|
67
|
+
io.write(JSON.pretty_generate(event))
|
68
|
+
io.close_write
|
69
|
+
result['output'] = io.read
|
70
|
+
end
|
71
|
+
result['status'] = $?.exitstatus
|
72
|
+
result
|
73
|
+
end
|
74
|
+
report = proc do |result|
|
75
|
+
EM.debug('handled :: ' + event['check']['handler'] + ' :: ' + result['status'].to_s + ' :: ' + result['output'])
|
76
|
+
end
|
77
|
+
EM.defer(handler, report)
|
78
|
+
end
|
79
|
+
|
80
|
+
def process_result(result)
|
81
|
+
@redis.get('client:' + result['client']).callback do |client_json|
|
82
|
+
unless client_json.nil?
|
83
|
+
client = JSON.parse(client_json)
|
84
|
+
check = result['check']
|
85
|
+
check.merge!(@settings['checks'][check['name']]) if @settings['checks'].has_key?(check['name'])
|
86
|
+
check['handler'] ||= 'default'
|
87
|
+
event = {'client' => client, 'check' => check, 'occurrences' => 1}
|
88
|
+
if check['type'] == 'metric'
|
89
|
+
handle_event(event)
|
90
|
+
else
|
91
|
+
@redis.hget('events:' + client['name'], check['name']).callback do |event_json|
|
92
|
+
previous_event = event_json ? JSON.parse(event_json) : nil
|
93
|
+
if previous_event && check['status'] == 0
|
94
|
+
@redis.hdel('events:' + client['name'], check['name'])
|
95
|
+
event['action'] = 'resolve'
|
96
|
+
handle_event(event)
|
97
|
+
elsif check['status'] > 0
|
98
|
+
if previous_event && check['status'] == previous_event['status']
|
99
|
+
event['occurrences'] = previous_event['occurrences'] += 1
|
100
|
+
end
|
101
|
+
@redis.hset('events:' + client['name'], check['name'], {
|
102
|
+
'status' => check['status'],
|
103
|
+
'output' => check['output'],
|
104
|
+
'occurrences' => event['occurrences']
|
105
|
+
}.to_json).callback do
|
106
|
+
event['action'] = 'create'
|
107
|
+
handle_event(event)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def setup_results
|
117
|
+
@result_queue = @amq.queue('results')
|
118
|
+
@result_queue.subscribe do |result_json|
|
119
|
+
result = JSON.parse(result_json)
|
120
|
+
process_result(result)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def setup_publisher(options={})
|
125
|
+
exchanges = Hash.new
|
126
|
+
stagger = options[:test] ? 0 : 7
|
127
|
+
@settings['checks'].each_with_index do |(name, details), index|
|
128
|
+
EM.add_timer(stagger*index) do
|
129
|
+
details['subscribers'].each do |exchange|
|
130
|
+
if exchanges[exchange].nil?
|
131
|
+
exchanges[exchange] = @amq.fanout(exchange)
|
132
|
+
end
|
133
|
+
interval = options[:test] ? 0.5 : details['interval']
|
134
|
+
EM.add_periodic_timer(interval) do
|
135
|
+
exchanges[exchange].publish({'name' => name, 'issued' => Time.now.to_i}.to_json)
|
136
|
+
EM.debug('published :: ' + exchange + ' :: ' + name)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def setup_keepalive_monitor
|
144
|
+
EM.add_periodic_timer(30) do
|
145
|
+
@redis.smembers('clients').callback do |clients|
|
146
|
+
clients.each do |client_id|
|
147
|
+
@redis.get('client:' + client_id).callback do |client_json|
|
148
|
+
client = JSON.parse(client_json)
|
149
|
+
time_since_last_check = Time.now.to_i - client['timestamp']
|
150
|
+
result = {'client' => client['name'], 'check' => {'name' => 'keepalive', 'issued' => Time.now.to_i}}
|
151
|
+
case
|
152
|
+
when time_since_last_check >= 180
|
153
|
+
result['check']['status'] = 2
|
154
|
+
result['check']['output'] = 'No keep-alive sent from host in over 180 seconds'
|
155
|
+
@result_queue.publish(result.to_json)
|
156
|
+
when time_since_last_check >= 120
|
157
|
+
result['check']['status'] = 1
|
158
|
+
result['check']['output'] = 'No keep-alive sent from host in over 120 seconds'
|
159
|
+
@result_queue.publish(result.to_json)
|
160
|
+
else
|
161
|
+
@redis.hexists('events:' + client_id, 'keepalive').callback do |exists|
|
162
|
+
if exists == 1
|
163
|
+
result['check']['status'] = 0
|
164
|
+
result['check']['output'] = 'Keep-alive sent from host'
|
165
|
+
@result_queue.publish(result.to_json)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def monitor_queues
|
176
|
+
EM.add_periodic_timer(5) do
|
177
|
+
unless @keepalive_queue.subscribed?
|
178
|
+
setup_keepalives
|
179
|
+
end
|
180
|
+
unless @result_queue.subscribed?
|
181
|
+
setup_results
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
data/sensu.gemspec
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "sensu"
|
3
|
+
s.version = "0.6.0"
|
4
|
+
s.authors = ["Sean Porter", "Justin Kolberg"]
|
5
|
+
s.email = ["sean.porter@sonian.net", "justin.kolberg@sonian.net"]
|
6
|
+
s.homepage = "https://github.com/sonian/sensu"
|
7
|
+
s.summary = %q{A server monitoring framework}
|
8
|
+
s.description = %q{A server monitoring framework using the publish-subscribe model}
|
9
|
+
s.license = "MIT"
|
10
|
+
s.has_rdoc = false
|
11
|
+
|
12
|
+
case ENV['BUILD']
|
13
|
+
when "mingw"
|
14
|
+
s.platform = "x86-mingw32"
|
15
|
+
when "mswin"
|
16
|
+
s.platform = "x86-mswin32"
|
17
|
+
else
|
18
|
+
s.platform = Gem::Platform::RUBY
|
19
|
+
end
|
20
|
+
|
21
|
+
s.add_dependency("eventmachine", "1.0.0.beta.4.1") if s.platform =~ /mswin|mingw32|windows/
|
22
|
+
|
23
|
+
s.add_dependency("amqp", "0.7.4")
|
24
|
+
s.add_dependency("json")
|
25
|
+
s.add_dependency("uuidtools")
|
26
|
+
s.add_dependency("em-syslog")
|
27
|
+
|
28
|
+
unless s.platform =~ /mswin|mingw32|windows/
|
29
|
+
s.add_dependency("em-hiredis")
|
30
|
+
s.add_dependency("async_sinatra")
|
31
|
+
s.add_dependency("thin")
|
32
|
+
end
|
33
|
+
|
34
|
+
s.add_development_dependency('rake')
|
35
|
+
s.add_development_dependency('minitest')
|
36
|
+
s.add_development_dependency('em-ventually')
|
37
|
+
s.add_development_dependency('rest-client')
|
38
|
+
|
39
|
+
s.files = `git ls-files`.split("\n").reject {|f| f =~ /(dist|test)/}
|
40
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
41
|
+
s.require_paths = ["lib"]
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sensu
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.6.0
|
6
|
+
platform: x86-mingw32
|
7
|
+
authors:
|
8
|
+
- Sean Porter
|
9
|
+
- Justin Kolberg
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
|
14
|
+
date: 2011-09-27 00:00:00 Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: amqp
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - "="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 0.7.4
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: json
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: "0"
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id002
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: uuidtools
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: "0"
|
47
|
+
type: :runtime
|
48
|
+
version_requirements: *id003
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: em-syslog
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
type: :runtime
|
59
|
+
version_requirements: *id004
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: em-hiredis
|
62
|
+
prerelease: false
|
63
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: "0"
|
69
|
+
type: :runtime
|
70
|
+
version_requirements: *id005
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: async_sinatra
|
73
|
+
prerelease: false
|
74
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: "0"
|
80
|
+
type: :runtime
|
81
|
+
version_requirements: *id006
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: thin
|
84
|
+
prerelease: false
|
85
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: "0"
|
91
|
+
type: :runtime
|
92
|
+
version_requirements: *id007
|
93
|
+
- !ruby/object:Gem::Dependency
|
94
|
+
name: rake
|
95
|
+
prerelease: false
|
96
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: "0"
|
102
|
+
type: :development
|
103
|
+
version_requirements: *id008
|
104
|
+
- !ruby/object:Gem::Dependency
|
105
|
+
name: minitest
|
106
|
+
prerelease: false
|
107
|
+
requirement: &id009 !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: "0"
|
113
|
+
type: :development
|
114
|
+
version_requirements: *id009
|
115
|
+
- !ruby/object:Gem::Dependency
|
116
|
+
name: em-ventually
|
117
|
+
prerelease: false
|
118
|
+
requirement: &id010 !ruby/object:Gem::Requirement
|
119
|
+
none: false
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: "0"
|
124
|
+
type: :development
|
125
|
+
version_requirements: *id010
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: rest-client
|
128
|
+
prerelease: false
|
129
|
+
requirement: &id011 !ruby/object:Gem::Requirement
|
130
|
+
none: false
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: "0"
|
135
|
+
type: :development
|
136
|
+
version_requirements: *id011
|
137
|
+
description: A server monitoring framework using the publish-subscribe model
|
138
|
+
email:
|
139
|
+
- sean.porter@sonian.net
|
140
|
+
- justin.kolberg@sonian.net
|
141
|
+
executables:
|
142
|
+
- sensu-api
|
143
|
+
- sensu-client
|
144
|
+
- sensu-server
|
145
|
+
extensions: []
|
146
|
+
|
147
|
+
extra_rdoc_files: []
|
148
|
+
|
149
|
+
files:
|
150
|
+
- .gitignore
|
151
|
+
- Gemfile
|
152
|
+
- Gemfile.lock
|
153
|
+
- MIT-LICENSE.txt
|
154
|
+
- README.org
|
155
|
+
- Rakefile
|
156
|
+
- bin/sensu-api
|
157
|
+
- bin/sensu-client
|
158
|
+
- bin/sensu-server
|
159
|
+
- lib/sensu.rb
|
160
|
+
- lib/sensu/api.rb
|
161
|
+
- lib/sensu/client.rb
|
162
|
+
- lib/sensu/config.rb
|
163
|
+
- lib/sensu/helpers.rb
|
164
|
+
- lib/sensu/server.rb
|
165
|
+
- sensu.gemspec
|
166
|
+
homepage: https://github.com/sonian/sensu
|
167
|
+
licenses:
|
168
|
+
- MIT
|
169
|
+
post_install_message:
|
170
|
+
rdoc_options: []
|
171
|
+
|
172
|
+
require_paths:
|
173
|
+
- lib
|
174
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
175
|
+
none: false
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: "0"
|
180
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
181
|
+
none: false
|
182
|
+
requirements:
|
183
|
+
- - ">="
|
184
|
+
- !ruby/object:Gem::Version
|
185
|
+
version: "0"
|
186
|
+
requirements: []
|
187
|
+
|
188
|
+
rubyforge_project:
|
189
|
+
rubygems_version: 1.8.8
|
190
|
+
signing_key:
|
191
|
+
specification_version: 3
|
192
|
+
summary: A server monitoring framework
|
193
|
+
test_files: []
|
194
|
+
|