panoptimon 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +24 -0
- data/Gemfile +4 -0
- data/LICENSE +29 -0
- data/README.md +78 -0
- data/Rakefile +14 -0
- data/bin/panoptimon +118 -0
- data/collectors/cpu/cpu +39 -0
- data/collectors/cpu/cpu.json +1 -0
- data/collectors/disk/disk +37 -0
- data/collectors/disk/disk.json +1 -0
- data/collectors/disk/requires +1 -0
- data/collectors/disk_free/disk_free +15 -0
- data/collectors/disk_free/disk_free.json +1 -0
- data/collectors/dns/README.md +33 -0
- data/collectors/dns/dns +9 -0
- data/collectors/dns/dns.json +7 -0
- data/collectors/dns/lib/panoptimon-collector-dns/dns.rb +43 -0
- data/collectors/dns/lib/panoptimon-collector-dns.rb +2 -0
- data/collectors/dns/panoptimon-collector-dns.gemspec +4 -0
- data/collectors/files/README.md +32 -0
- data/collectors/files/files +129 -0
- data/collectors/files/files.json +14 -0
- data/collectors/files/spec/files_spec.rb +57 -0
- data/collectors/haproxy/README.md +40 -0
- data/collectors/haproxy/haproxy +16 -0
- data/collectors/haproxy/haproxy.json +3 -0
- data/collectors/haproxy/lib/panoptimon-collector-haproxy/haproxy.rb +149 -0
- data/collectors/haproxy/lib/panoptimon-collector-haproxy.rb +1 -0
- data/collectors/haproxy/notes.txt +13 -0
- data/collectors/haproxy/spec/haproxy_spec.rb +98 -0
- data/collectors/haproxy/spec/haproxy_spec.rb-get.html +22 -0
- data/collectors/haproxy/spec/haproxy_spec.rb-show_info.txt +21 -0
- data/collectors/haproxy/spec/haproxy_spec.rb-show_stat.csv +25 -0
- data/collectors/haproxy/spec/haproxy_spec.rb-show_stat2.csv +11 -0
- data/collectors/http/README.md +49 -0
- data/collectors/http/http +27 -0
- data/collectors/http/http.json +3 -0
- data/collectors/http/lib/panoptimon-collector-http/http.rb +74 -0
- data/collectors/http/lib/panoptimon-collector-http/version.rb +7 -0
- data/collectors/http/lib/panoptimon-collector-http.rb +2 -0
- data/collectors/interfaces/interfaces +33 -0
- data/collectors/interfaces/interfaces.json +1 -0
- data/collectors/iostat/iostat +53 -0
- data/collectors/iostat/iostat.json +1 -0
- data/collectors/json/README.md +27 -0
- data/collectors/json/json +37 -0
- data/collectors/json/json.json +1 -0
- data/collectors/load/load +15 -0
- data/collectors/load/load.json +1 -0
- data/collectors/memcached/memcached +55 -0
- data/collectors/memcached/memcached.json +7 -0
- data/collectors/memcached/test-notes.txt +3 -0
- data/collectors/memory/memory +33 -0
- data/collectors/memory/memory.json +1 -0
- data/collectors/mysql_status/mysql_status +52 -0
- data/collectors/mysql_status/mysql_status.json +4 -0
- data/collectors/network/network +67 -0
- data/collectors/network/network.json +18 -0
- data/collectors/nginx/README.md +32 -0
- data/collectors/nginx/lib/panoptimon-collector-nginx/nginx.rb +45 -0
- data/collectors/nginx/lib/panoptimon-collector-nginx.rb +2 -0
- data/collectors/nginx/nginx +11 -0
- data/collectors/nginx/nginx.json +3 -0
- data/collectors/nginx/panoptimon-collector-nginx.gemspec +4 -0
- data/collectors/ping/README.md +54 -0
- data/collectors/ping/ping +57 -0
- data/collectors/ping/ping.json +7 -0
- data/collectors/process/README.md +36 -0
- data/collectors/process/process +61 -0
- data/collectors/process/process.json +7 -0
- data/collectors/service/README.md +51 -0
- data/collectors/service/samples/.gitignore +1 -0
- data/collectors/service/samples/data/disconnect +11 -0
- data/collectors/service/samples/data/flappy +7 -0
- data/collectors/service/samples/data/solid +18 -0
- data/collectors/service/samples/replay +27 -0
- data/collectors/service/service +86 -0
- data/collectors/service/service.json +7 -0
- data/collectors/smtp/lib/panoptimon-collector-smtp/smtp.rb +30 -0
- data/collectors/smtp/lib/panoptimon-collector-smtp.rb +1 -0
- data/collectors/smtp/smtp +27 -0
- data/collectors/smtp/smtp.json +10 -0
- data/collectors/socket/README.md +36 -0
- data/collectors/socket/lib/panoptimon-collector-socket/socket.rb +38 -0
- data/collectors/socket/lib/panoptimon-collector-socket/tcp.rb +34 -0
- data/collectors/socket/lib/panoptimon-collector-socket/unix.rb +28 -0
- data/collectors/socket/lib/panoptimon-collector-socket.rb +3 -0
- data/collectors/socket/socket +13 -0
- data/collectors/socket/socket.json +16 -0
- data/collectors/socket/tests/tcp_spec.rb +21 -0
- data/collectors/socket/tests/unix_spec.rb +35 -0
- data/collectors/ssh/README.md +27 -0
- data/collectors/ssh/ssh +41 -0
- data/collectors/ssh/ssh.json +3 -0
- data/lib/panoptimon/collector.rb +135 -0
- data/lib/panoptimon/eventmonkeypatch/popen3.rb +40 -0
- data/lib/panoptimon/http.rb +63 -0
- data/lib/panoptimon/logger.rb +19 -0
- data/lib/panoptimon/monitor.rb +154 -0
- data/lib/panoptimon/util/string-with-as_number.rb +5 -0
- data/lib/panoptimon/util.rb +23 -0
- data/lib/panoptimon/version.rb +5 -0
- data/lib/panoptimon.rb +144 -0
- data/misc/collector_setup.rb +23 -0
- data/misc/monitor_setup.rb +25 -0
- data/misc/plugins_setup.rb +25 -0
- data/misc/riemann-cli.rb +33 -0
- data/panoptimon.gemspec +33 -0
- data/plugins/daemon_health/README.md +31 -0
- data/plugins/daemon_health/daemon_health.json +4 -0
- data/plugins/daemon_health/daemon_health.rb +34 -0
- data/plugins/daemon_health/lib/panoptimon-plugin-daemon_health/rollup.rb +64 -0
- data/plugins/daemon_health/panoptimon-plugin-daemon_health.gemspec +10 -0
- data/plugins/daemon_health/spec/moving_avg_spec.rb +24 -0
- data/plugins/email/README.md +30 -0
- data/plugins/email/email.json +3 -0
- data/plugins/email/email.rb +52 -0
- data/plugins/log_to_file/log_to_file.json +1 -0
- data/plugins/log_to_file/log_to_file.rb +8 -0
- data/plugins/log_to_logger/log_to_logger.json +3 -0
- data/plugins/log_to_logger/log_to_logger.rb +7 -0
- data/plugins/metrics_http/README.md +23 -0
- data/plugins/metrics_http/metrics_http.json +1 -0
- data/plugins/metrics_http/metrics_http.rb +17 -0
- data/plugins/riemann_stream/requires +1 -0
- data/plugins/riemann_stream/riemann_stream.json +3 -0
- data/plugins/riemann_stream/riemann_stream.rb +23 -0
- data/plugins/status_http/requires +1 -0
- data/plugins/status_http/status_http.json +1 -0
- data/plugins/status_http/status_http.rb +60 -0
- data/sample_configs/1/collectors/alls_well.json +6 -0
- data/sample_configs/1/collectors/clock/clock +12 -0
- data/sample_configs/1/collectors/clock.json +1 -0
- data/sample_configs/1/collectors/df/df.json +6 -0
- data/sample_configs/1/collectors/df/wrap_df +21 -0
- data/sample_configs/1/collectors/load.json +1 -0
- data/sample_configs/1/panoptimon.json +4 -0
- data/sample_configs/1/plugins/isup/isup.rb +3 -0
- data/sample_configs/1/plugins/isup.json +1 -0
- data/sample_configs/1/plugins/log_to_file.json +1 -0
- data/sample_configs/err_handler/collectors/fail.json +4 -0
- data/sample_configs/err_handler/collectors/noisy.json +5 -0
- data/sample_configs/err_handler/collectors/noisy_failure.json +5 -0
- data/sample_configs/err_handler/collectors/notfound.json +4 -0
- data/sample_configs/err_handler/panoptimon.json +3 -0
- data/sample_configs/err_handler/plugins/.exists +0 -0
- data/sample_configs/passthru/collectors/beep.json +5 -0
- data/sample_configs/passthru/collectors/cat/collect_this.json +1 -0
- data/sample_configs/passthru/collectors/cat.json +5 -0
- data/sample_configs/passthru/panoptimon.json +3 -0
- data/sample_configs/passthru/plugins/okcat/okcat.rb +17 -0
- data/sample_configs/passthru/plugins/okcat.json +1 -0
- data/sample_configs/plugin_error/collectors/beep.json +5 -0
- data/sample_configs/plugin_error/panoptimon.json +1 -0
- data/sample_configs/plugin_error/plugins/error_always/error_always.rb +1 -0
- data/sample_configs/plugin_error/plugins/error_always.json +1 -0
- data/sample_configs/slow/collectors/slowbeep.json +6 -0
- data/sample_configs/slow/panoptimon.json +1 -0
- data/sample_configs/slow/plugins/.exists +0 -0
- data/sample_configs/timeout_newline/collectors/slow_lf.json +8 -0
- data/sample_configs/timeout_newline/panoptimon.json +1 -0
- data/sample_configs/timeout_newline/plugins/.exists +0 -0
- data/sample_configs/timeout_not/collectors/slowly.json +7 -0
- data/sample_configs/timeout_not/panoptimon.json +1 -0
- data/sample_configs/timeout_not/plugins/.exists +0 -0
- data/spec/collector/config_spec.rb +30 -0
- data/spec/collector/initialize_spec.rb +24 -0
- data/spec/collector/metric_spec.rb +22 -0
- data/spec/passthru_spec.rb +13 -0
- data/spec/util_spec.rb +37 -0
- data/tools/link_and_enable +37 -0
- data/tools/metricify +8 -0
- metadata +319 -0
@@ -0,0 +1,135 @@
|
|
1
|
+
# Copyright (C) 2012 Sourcefire, Inc.
|
2
|
+
|
3
|
+
module Panoptimon
|
4
|
+
|
5
|
+
require 'json';
|
6
|
+
|
7
|
+
class Collector
|
8
|
+
|
9
|
+
include Panoptimon::Logger
|
10
|
+
|
11
|
+
attr_reader :name, :cmd, :config, :bus, :last_run_time, :interval
|
12
|
+
def initialize (args)
|
13
|
+
cmd = args.delete(:command) or raise "must have 'command' argument"
|
14
|
+
@cmd = cmd.class == Array ? cmd : Shellwords.shellsplit(cmd)
|
15
|
+
->(exe) {
|
16
|
+
raise "no such file '#{exe}'" unless File.exist?(exe)
|
17
|
+
raise "command '#{exe}' is not executable" unless File.executable?(exe)
|
18
|
+
}.call(@cmd[0]) # TODO or maybe args[:interpreter]
|
19
|
+
@bus = args.delete(:bus) or raise "must have 'bus' argument"
|
20
|
+
args.each { |k,v| instance_variable_set("@#{k}", v) }
|
21
|
+
|
22
|
+
@name ||= 'unnamed'
|
23
|
+
@config ||= {}
|
24
|
+
|
25
|
+
@interval = config[:interval] ||= 60
|
26
|
+
@last_run_time = Time.at(-@interval)
|
27
|
+
end
|
28
|
+
|
29
|
+
def run
|
30
|
+
cmdc = @cmd + [JSON.generate(config)]
|
31
|
+
|
32
|
+
@last_run_time = Time.now # TODO .to_i ?
|
33
|
+
|
34
|
+
logger.info {"run command: #{cmdc}"}
|
35
|
+
@child = EM.popen3b(cmdc, CollectorSink, self)
|
36
|
+
@child.on_unbind { |status, errmess|
|
37
|
+
logger.error {"collector #{name} failed: #{status}" +
|
38
|
+
(errmess.nil? ? '' :
|
39
|
+
"\n #{errmess.chomp.split(/\n/).join("\n ")}")
|
40
|
+
} if(not(status.nil?) and status != 0)
|
41
|
+
@child = nil
|
42
|
+
}
|
43
|
+
logger.debug "timeout is: #{config[:timeout]}"
|
44
|
+
# XXX afaict, eventmachine just did not implement this:
|
45
|
+
# @child.set_comm_inactivity_timeout(config[:timeout])
|
46
|
+
end
|
47
|
+
|
48
|
+
def noise(mess)
|
49
|
+
logger.warn "collector/#{name} noise: #{mess.chomp}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def running?
|
53
|
+
@child.nil? ? false : true
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
class Metric < Hash
|
59
|
+
|
60
|
+
def initialize (name, data)
|
61
|
+
name = data.delete('_name') if data['_name']
|
62
|
+
self.merge!(_flatten_hash({}, name, data))
|
63
|
+
end
|
64
|
+
|
65
|
+
def _flatten_hash (i,p,h)
|
66
|
+
h.each {|k,v|
|
67
|
+
ok = "#{p}|#{k}"
|
68
|
+
# TODO reject non-numeric data?
|
69
|
+
if k != '_info' and v.is_a?(Hash)
|
70
|
+
_flatten_hash(i, ok, v)
|
71
|
+
else
|
72
|
+
i[ok] = v
|
73
|
+
end
|
74
|
+
}
|
75
|
+
return i
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
module CollectorSink
|
80
|
+
|
81
|
+
def initialize (handler)
|
82
|
+
@handler = handler
|
83
|
+
@timeout = @handler.config[:timeout]
|
84
|
+
@interval = @handler.config[:interval]
|
85
|
+
timer_on
|
86
|
+
end
|
87
|
+
|
88
|
+
# reset / start timeout timer
|
89
|
+
def timer_on (opts={})
|
90
|
+
@timer.cancel unless @timer.nil?
|
91
|
+
length = @timeout + (opts[:with_interval] ? @interval : 0)
|
92
|
+
@timer = EventMachine::Timer.new(length) {
|
93
|
+
scrap = @buf ? " - #{@buf.flush}" : ''
|
94
|
+
@handler.logger.error "timeout on #{@handler.name}" + scrap
|
95
|
+
@handler.logger.debug {"pid #{get_pid}"}
|
96
|
+
close_connection()
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
def timer_off
|
101
|
+
@timer.cancel
|
102
|
+
end
|
103
|
+
|
104
|
+
def receive_data (data)
|
105
|
+
timer_on
|
106
|
+
@handler.logger.debug "incoming"
|
107
|
+
@buf ||= BufferedTokenizer.new("\n")
|
108
|
+
@buf.extract(data).each do |line|
|
109
|
+
timer_on(with_interval: true)
|
110
|
+
begin
|
111
|
+
data = JSON.parse(line)
|
112
|
+
rescue
|
113
|
+
# TODO feed errors up to the monitor
|
114
|
+
$stderr.puts "error parsing #{line.dump} - #{$!}"
|
115
|
+
end
|
116
|
+
@handler.logger.debug "line: #{line}"
|
117
|
+
@handler.bus.notify(Metric.new(@handler.name, data))
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def receive_stderr (mess)
|
122
|
+
@handler.noise(mess)
|
123
|
+
(@err_mess ||= '') << mess
|
124
|
+
end
|
125
|
+
|
126
|
+
def on_unbind (&block); @on_unbind = block; end
|
127
|
+
def unbind
|
128
|
+
timer_off
|
129
|
+
@on_unbind.call(get_status.exitstatus, @err_mess)
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# Copyright (C) 2012 Sourcefire, Inc.
|
2
|
+
|
3
|
+
# adapted from http://pastebin.com/C4hvAyKM
|
4
|
+
# and https://gist.github.com/1333428
|
5
|
+
|
6
|
+
# feed stderr into connection's receive_stderr()
|
7
|
+
|
8
|
+
module EventMachine
|
9
|
+
class StderrHandler < EventMachine::Connection
|
10
|
+
def initialize(connection); @connection = connection; end
|
11
|
+
def receive_data(data); @connection.receive_stderr(data); end
|
12
|
+
def unbind; detach; end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.popen3b(cmd, handler=nil, *args)
|
16
|
+
klass = klass_from_handler(Connection, handler, *args)
|
17
|
+
raise "no command?" unless cmd.first
|
18
|
+
cmd.unshift(cmd.first) # -> execvp
|
19
|
+
|
20
|
+
original_stderr = $stderr.dup
|
21
|
+
|
22
|
+
begin
|
23
|
+
rd, wr = IO.pipe
|
24
|
+
|
25
|
+
$stderr.reopen wr
|
26
|
+
s = invoke_popen(cmd)
|
27
|
+
$stderr.reopen original_stderr
|
28
|
+
|
29
|
+
connection = klass.new(s, *args)
|
30
|
+
EM.attach(rd, StderrHandler, connection)
|
31
|
+
@conns[s] = connection
|
32
|
+
yield(connection) if block_given?
|
33
|
+
connection
|
34
|
+
rescue
|
35
|
+
$stderr.reopen(original_stderr)
|
36
|
+
raise $!
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# Copyright (C) 2012 Sourcefire, Inc.
|
2
|
+
|
3
|
+
require 'thin'
|
4
|
+
|
5
|
+
module Panoptimon
|
6
|
+
class HTTP
|
7
|
+
|
8
|
+
include Logger
|
9
|
+
|
10
|
+
def initialize (args={})
|
11
|
+
@match = []
|
12
|
+
@mount = []
|
13
|
+
# TODO args[:config].http_port, ssl, etc
|
14
|
+
@http = Thin::Server.new('0.0.0.0', 8080, self);
|
15
|
+
end
|
16
|
+
|
17
|
+
def start
|
18
|
+
@http.backend.start
|
19
|
+
end
|
20
|
+
|
21
|
+
def call (env)
|
22
|
+
path = env['PATH_INFO']
|
23
|
+
return favicon(env) if path == '/favicon.ico'
|
24
|
+
# logger.debug { "#{path} => " + env.inspect }
|
25
|
+
if go = @match.find {|x| path =~ x[0]}
|
26
|
+
elsif go = @mount.find {|x| path =~ %r{^#{x[0]}(/|$)}}
|
27
|
+
env['SCRIPT_NAME'] = go[0]
|
28
|
+
env['PATH_INFO'] = path.sub(%r{^#{go[0]}}, '')
|
29
|
+
else
|
30
|
+
return [404, {'Content-Type' => 'text/html'},
|
31
|
+
'<html><head><title>Not Found</title></head>' +
|
32
|
+
'<body><p>nope</p></body></html>']
|
33
|
+
end
|
34
|
+
env['rack.logger'] = logger
|
35
|
+
begin
|
36
|
+
return go[1].call(env)
|
37
|
+
rescue => ex
|
38
|
+
logger.error { "error: #{ex.message} #{ex.backtrace.join("\n ")}" }
|
39
|
+
return [500, {'Content-Type' => 'text/html'}, ['bah']]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def favicon(env)
|
44
|
+
# TODO bundle / configure favicon?
|
45
|
+
# NOTE why doesn't rack/thin support .to_path per spec?
|
46
|
+
return [200, {'Content-Type' => 'image/x-icon'},
|
47
|
+
Pathname.new('/tmp/favicon.ico').open]
|
48
|
+
end
|
49
|
+
|
50
|
+
# regexp-match
|
51
|
+
def match (regexp, app)
|
52
|
+
regexp = %r{^#{regexp}} if regexp.class == String
|
53
|
+
@match.push([regexp, app])
|
54
|
+
end
|
55
|
+
|
56
|
+
# path prefix
|
57
|
+
def mount (point, app)
|
58
|
+
point.sub(%r{/*$}, '')
|
59
|
+
@mount.push([point, app])
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# Copyright (C) 2012 Sourcefire, Inc.
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
module Panoptimon::Logger
|
6
|
+
|
7
|
+
def logger
|
8
|
+
Panoptimon::Logger.logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.logger
|
12
|
+
@logger ||= Logger.new($stderr).tap {|l|
|
13
|
+
env_l = ENV.delete('LOG_LEVEL')
|
14
|
+
l.level = env_l.nil? ?
|
15
|
+
Logger::WARN : Logger.const_get(env_l.upcase)
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# Copyright (C) 2012 Sourcefire, Inc.
|
2
|
+
|
3
|
+
module Panoptimon
|
4
|
+
class Monitor
|
5
|
+
|
6
|
+
include Panoptimon::Logger
|
7
|
+
|
8
|
+
attr_reader :config, :collectors, :plugins, :cached, :owd,
|
9
|
+
:loaded_plugins, :bus
|
10
|
+
|
11
|
+
def initialize (args={})
|
12
|
+
@collectors = []
|
13
|
+
@plugins = {}
|
14
|
+
@loaded_plugins = {}
|
15
|
+
args.each { |k,v| instance_variable_set("@#{k}", v) }
|
16
|
+
|
17
|
+
@owd = Dir.pwd
|
18
|
+
|
19
|
+
me = self
|
20
|
+
@bus = EM.spawn { |metric| me.bus_driver(metric) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def _dirjson (x)
|
24
|
+
x = Pathname.new(x)
|
25
|
+
x.entries.find_all {|f| f.to_s =~ /\.json$/i}.
|
26
|
+
map {|f| x + f}
|
27
|
+
end
|
28
|
+
|
29
|
+
def find_collectors; _dirjson(config.collectors_dir); end
|
30
|
+
def find_plugins; _dirjson(config.plugins_dir); end
|
31
|
+
|
32
|
+
def load_collectors
|
33
|
+
find_collectors.each {|f|
|
34
|
+
begin
|
35
|
+
_init_collector(_load_collector_config(f))
|
36
|
+
rescue => ex
|
37
|
+
logger.error "collector #{f} failed to load: \n" +
|
38
|
+
" #{ex.message} \n #{ex.backtrace[0]}"
|
39
|
+
end
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def _load_collector_config (file)
|
44
|
+
conf = JSON.parse(file.read, {:symbolize_names => true})
|
45
|
+
base = file.basename.sub(/\.json$/, '').to_s
|
46
|
+
command = conf[:exec] ||= base
|
47
|
+
command = file.dirname + base + command unless command =~ /^\//
|
48
|
+
return conf.
|
49
|
+
merge({
|
50
|
+
name: base,
|
51
|
+
interval: (self.config.collector_interval || 99).to_i,
|
52
|
+
timeout: (self.config.collector_timeout || 99).to_i
|
53
|
+
}) {|k,a,b| a}.
|
54
|
+
merge({command: command})
|
55
|
+
end
|
56
|
+
|
57
|
+
def _init_collector (conf)
|
58
|
+
name = conf.delete(:name)
|
59
|
+
command = conf.delete(:command)
|
60
|
+
collector = Collector.new(
|
61
|
+
name: name,
|
62
|
+
bus: @bus,
|
63
|
+
command: [command.to_s] + conf[:args].to_a,
|
64
|
+
config: conf,
|
65
|
+
)
|
66
|
+
collectors << collector
|
67
|
+
end
|
68
|
+
|
69
|
+
def http
|
70
|
+
return @http unless @http.nil?
|
71
|
+
# TODO rescue LoadError => nicer error message
|
72
|
+
require 'panoptimon/http'
|
73
|
+
@http = HTTP.new
|
74
|
+
end
|
75
|
+
|
76
|
+
def empty_binding; binding; end
|
77
|
+
def load_plugins
|
78
|
+
find_plugins.each {|f| _init_plugin(_load_plugin_config(f)) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def _load_plugin_config (file)
|
82
|
+
conf = JSON.parse(file.read, {:symbolize_names => true})
|
83
|
+
base = file.basename.sub(/\.json$/, '').to_s
|
84
|
+
|
85
|
+
# TODO support conf[:require] -> class.setup(conf) scheme?
|
86
|
+
rb = conf[:require] || "#{base}.rb"
|
87
|
+
rb = file.dirname + base + rb
|
88
|
+
return conf.
|
89
|
+
merge({
|
90
|
+
name: base,
|
91
|
+
}) {|k,a,b| a}.
|
92
|
+
merge({
|
93
|
+
base: base,
|
94
|
+
rb: rb
|
95
|
+
})
|
96
|
+
end
|
97
|
+
|
98
|
+
def _init_plugin (conf)
|
99
|
+
name = conf.delete(:name)
|
100
|
+
rb = conf.delete(:rb)
|
101
|
+
setup = eval("->(name, config, monitor) {#{rb.open.read}\n}",
|
102
|
+
empty_binding, rb.to_s, 1)
|
103
|
+
callback = begin; setup.call(name, conf, self)
|
104
|
+
rescue; raise "error loading plugin '#{name}' - #{$!}"; end
|
105
|
+
logger.debug "plugin #{callback} - #{plugins[name]}"
|
106
|
+
plugins[name] = callback unless callback.nil?
|
107
|
+
loaded_plugins[name] = conf.clone # XXX need a plugin object?
|
108
|
+
end
|
109
|
+
|
110
|
+
def run
|
111
|
+
|
112
|
+
runall = ->() {
|
113
|
+
logger.debug "beep"
|
114
|
+
collectors.each {|c|
|
115
|
+
logger.info "#{c.cmd} (#{c.running? ? 'on ' : 'off'
|
116
|
+
}) last run time: #{c.last_run_time}"
|
117
|
+
next if c.last_run_time + c.interval > Time.now or c.running?
|
118
|
+
c.run
|
119
|
+
}
|
120
|
+
}
|
121
|
+
EM.next_tick(&runall)
|
122
|
+
logger.warn 'no collectors' if collectors.length == 0
|
123
|
+
minterval = collectors.map{|c| c.interval}.min
|
124
|
+
minterval = 67 if minterval.nil? # XXX should never happen
|
125
|
+
logger.debug "minimum: #{minterval}"
|
126
|
+
EM.add_periodic_timer(minterval, &runall);
|
127
|
+
|
128
|
+
@http.start if @http
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
def stop
|
133
|
+
EM.stop
|
134
|
+
end
|
135
|
+
|
136
|
+
def enable_cache(arg=true);
|
137
|
+
if arg; @cached ||= {}; else; @cached = nil; end
|
138
|
+
end
|
139
|
+
|
140
|
+
def bus_driver(metric)
|
141
|
+
logger.debug {"metric: #{metric.inspect}"}
|
142
|
+
metric.each {|k,v| @cached[k] = v} if @cached
|
143
|
+
plugins.each {|n,p|
|
144
|
+
begin p.call(metric)
|
145
|
+
rescue => e
|
146
|
+
logger.warn "plugin '#{n}' error: " +
|
147
|
+
"#{e}\n #{e.backtrace[0].sub(%r{^.*/?(plugins/)}, '\1')}"
|
148
|
+
plugins.delete(n)
|
149
|
+
end
|
150
|
+
}
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Copyright (C) 2012 Sourcefire, Inc.
|
2
|
+
|
3
|
+
module Panoptimon
|
4
|
+
module Util
|
5
|
+
VERSION = '0.0.1'
|
6
|
+
|
7
|
+
def self._os; @os ||= Gem::Platform.local.os.to_sym; end
|
8
|
+
|
9
|
+
# return osname
|
10
|
+
# or, given a hash, return the corresponding hash element and raise
|
11
|
+
# error if os not in hash keys
|
12
|
+
def self.os (dispatch={})
|
13
|
+
# TODO or mess with rbconfig + Config::CONFIG['host_os']
|
14
|
+
os = _os
|
15
|
+
return os unless dispatch.length > 0
|
16
|
+
|
17
|
+
it = dispatch[os] || dispatch[:default] or
|
18
|
+
raise "unsupported OS: #{os}"
|
19
|
+
return it.is_a?(Proc) ? it.call() : it
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
data/lib/panoptimon.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
# Copyright (C) 2012 Sourcefire, Inc.
|
2
|
+
|
3
|
+
require 'panoptimon/version'
|
4
|
+
require 'panoptimon/logger'
|
5
|
+
require 'panoptimon/monitor'
|
6
|
+
require 'panoptimon/collector'
|
7
|
+
|
8
|
+
require 'optparse'
|
9
|
+
require 'ostruct'
|
10
|
+
|
11
|
+
require 'rubygems'
|
12
|
+
require 'eventmachine'
|
13
|
+
require 'panoptimon/eventmonkeypatch/popen3.rb'
|
14
|
+
|
15
|
+
module Panoptimon
|
16
|
+
|
17
|
+
class Panoptimon
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.load_options (args)
|
21
|
+
defaults = {
|
22
|
+
:daemonize => true,
|
23
|
+
:config_dir => '/etc/panoptimon/',
|
24
|
+
:config_file => '%/panoptimon.json',
|
25
|
+
:collectors_dir => '%/collectors',
|
26
|
+
:plugins_dir => '%/plugins',
|
27
|
+
:collector_interval => 60,
|
28
|
+
:collector_timeout => 120,
|
29
|
+
}
|
30
|
+
|
31
|
+
options = ->() {
|
32
|
+
o = {}
|
33
|
+
OptionParser.new do |opts|
|
34
|
+
|
35
|
+
opts.on('-C', '--config-dir DIR',
|
36
|
+
"Config directory (#{defaults[:config_dir]})"
|
37
|
+
) { |v| o[:config_dir] = v }
|
38
|
+
|
39
|
+
opts.on('-c', '--config-file FILENAME',
|
40
|
+
"Alternative configuration file ",
|
41
|
+
"(#{defaults[:config_file]})"
|
42
|
+
) { |v| o[:config_file] = v.nil? ? '' : v }
|
43
|
+
|
44
|
+
opts.on('-D', '--[no-]foreground',
|
45
|
+
"Don't daemonize (#{not defaults[:daemonize]})"
|
46
|
+
) { |v| o[:daemonize] = ! v }
|
47
|
+
|
48
|
+
['collectors', 'plugins'].each { |x|
|
49
|
+
k = "#{x}_dir".to_sym
|
50
|
+
opts.on("--#{x}-dir DIR",
|
51
|
+
"#{x.capitalize} directory (#{defaults[k]})"
|
52
|
+
) { |v| o[k] = v }
|
53
|
+
}
|
54
|
+
|
55
|
+
[:collectors, :plugins].each { |x|
|
56
|
+
opts.on('--list-'+x.to_s, "list all #{x} found"
|
57
|
+
) { (o[:lists] ||= []).push(x) }
|
58
|
+
}
|
59
|
+
|
60
|
+
opts.on('-o', '--configure X=Y',
|
61
|
+
'Set configuration values'
|
62
|
+
) { |x|
|
63
|
+
(k,v) = x.split(/=/, 2)
|
64
|
+
(o[:configure] ||= {})[k.to_sym] = v
|
65
|
+
}
|
66
|
+
|
67
|
+
opts.on('--show WHAT',
|
68
|
+
%q{Show/validate settings for:}, %q{ 'config' / collector:foo / plugin:foo}
|
69
|
+
) { |x| (k,v) = x.split(/:/, 2)
|
70
|
+
o[:show] = {k.to_sym => v||true}
|
71
|
+
}
|
72
|
+
|
73
|
+
opts.on('--plugin-test FILE',
|
74
|
+
'Load and test plugin(s).'
|
75
|
+
) { |x| (o[:plugin_test] ||= []).push(x) }
|
76
|
+
|
77
|
+
opts.on('-d', '--debug', "Enable debugging."
|
78
|
+
) { |v| o[:debug] = v }
|
79
|
+
|
80
|
+
opts.on('--verbose', "Enable verbose output"
|
81
|
+
) { |v| o[:verbose] = v }
|
82
|
+
|
83
|
+
opts.on('-v', '--version', "Print version"
|
84
|
+
) {
|
85
|
+
puts "panoptimon version #{Panoptimon::VERSION}"
|
86
|
+
o[:quit] = true
|
87
|
+
opts.terminate
|
88
|
+
}
|
89
|
+
|
90
|
+
opts.on('--help-defaults', 'Show default config values'
|
91
|
+
) {
|
92
|
+
puts JSON.pretty_generate(defaults)
|
93
|
+
o[:quit] = true
|
94
|
+
opts.terminate
|
95
|
+
}
|
96
|
+
|
97
|
+
opts.on("-h", "--help", "Show this message"
|
98
|
+
) {
|
99
|
+
puts opts
|
100
|
+
o[:quit] = true
|
101
|
+
opts.terminate
|
102
|
+
}
|
103
|
+
|
104
|
+
end.parse!(args)
|
105
|
+
|
106
|
+
return o
|
107
|
+
}.call
|
108
|
+
|
109
|
+
return false if options[:quit]
|
110
|
+
|
111
|
+
render = ->(d, x) { # x with '%/' can be relative to dir d
|
112
|
+
f = "#{x}"; f.sub!(/^%\//, '').nil? ? f : File.join(d, f)
|
113
|
+
}
|
114
|
+
|
115
|
+
# default config file is relative to config dir
|
116
|
+
cfile = render.call(
|
117
|
+
options[:config_dir] || defaults[:config_dir],
|
118
|
+
options[:config_file] || defaults[:config_file])
|
119
|
+
|
120
|
+
config = defaults.merge(
|
121
|
+
options[:config_file] == '' ? {} :
|
122
|
+
JSON.parse(File.read(cfile), {:symbolize_names => true})
|
123
|
+
).merge(options);
|
124
|
+
|
125
|
+
config[:config_file] = cfile # for diagnostics
|
126
|
+
|
127
|
+
(config.delete(:configure) || {}).each { | k,v| config[k] = v }
|
128
|
+
|
129
|
+
[:collectors_dir, :plugins_dir].each { |d|
|
130
|
+
config[d] = render.call(config[:config_dir], config[d])
|
131
|
+
}
|
132
|
+
|
133
|
+
# make all paths absolute
|
134
|
+
[:config_file, :config_dir, :collectors_dir, :plugins_dir].each { |d|
|
135
|
+
config[d] = File.expand_path(config[d]) unless config[d] == ''
|
136
|
+
}
|
137
|
+
|
138
|
+
return OpenStruct.new(config).freeze
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
# vim:ts=2:sw=2:et:sta
|
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'panoptimon'
|
4
|
+
|
5
|
+
count = 0
|
6
|
+
duck = EM.spawn { |metric|
|
7
|
+
count += 1
|
8
|
+
puts "metric: #{metric.inspect} (#{count})"
|
9
|
+
EM.stop if count >= 500
|
10
|
+
}
|
11
|
+
|
12
|
+
c = Panoptimon::Collector.new(bus: duck,
|
13
|
+
command: 'sample_configs/1/collectors/clock/clock',
|
14
|
+
# command: %q{echo -e '{"everythings_ok" : 1}\n\c' },
|
15
|
+
config: {:interval => 0.5})
|
16
|
+
|
17
|
+
puts "collector: #{c.inspect}"
|
18
|
+
#c.logger.level = ::Logger::DEBUG
|
19
|
+
|
20
|
+
EM.run {
|
21
|
+
c.run
|
22
|
+
EM.add_periodic_timer(1) { puts "running: #{c.running?}" }
|
23
|
+
}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'panoptimon'
|
4
|
+
|
5
|
+
count = 0
|
6
|
+
bus = EM.spawn { |metric|
|
7
|
+
count += 1
|
8
|
+
puts "metric: #{metric.inspect} (#{count})"
|
9
|
+
EM.stop if count >= 2000
|
10
|
+
}
|
11
|
+
|
12
|
+
m = Panoptimon::Monitor.new(
|
13
|
+
:collectors => [
|
14
|
+
Panoptimon::Collector.new(bus: bus,
|
15
|
+
command: 'sample_configs/1/collectors/clock/clock',
|
16
|
+
config: {:interval => 0.5}),
|
17
|
+
Panoptimon::Collector.new(bus: bus,
|
18
|
+
command: %q{echo -e '{"everythings_ok" : 1}\n\c' },
|
19
|
+
config: {:interval => 0.03})
|
20
|
+
]
|
21
|
+
)
|
22
|
+
|
23
|
+
EM.run {
|
24
|
+
m.run
|
25
|
+
}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'panoptimon'
|
4
|
+
|
5
|
+
m = Panoptimon::Monitor.new
|
6
|
+
|
7
|
+
count = 0
|
8
|
+
bus = EM.spawn { |metric|
|
9
|
+
count += 1
|
10
|
+
puts "metric: #{metric.inspect} (#{count})"
|
11
|
+
m.bus_driver(metric)
|
12
|
+
EM.stop if count >= 10
|
13
|
+
}
|
14
|
+
|
15
|
+
m.collectors << Panoptimon::Collector.new(
|
16
|
+
bus: bus,
|
17
|
+
command: 'sample_configs/1/collectors/clock/clock',
|
18
|
+
config: {:interval => 0.5},
|
19
|
+
)
|
20
|
+
|
21
|
+
m.plugins[:hello] = ->(metric){
|
22
|
+
puts "#{Time.now} - should do something with #{metric.inspect}"
|
23
|
+
}
|
24
|
+
|
25
|
+
EM.run { m.run }
|
data/misc/riemann-cli.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
#!/opt/ruby/bin/ruby
|
2
|
+
#
|
3
|
+
# Riemann data feeder
|
4
|
+
# Feed json directly to a Riemann server
|
5
|
+
#
|
6
|
+
# Copyright Sourcefire, 2012
|
7
|
+
# Benjamin Krueger <bkrueger@sourcefire.com>
|
8
|
+
|
9
|
+
require 'rubygems'
|
10
|
+
require 'json'
|
11
|
+
require 'riemann/client'
|
12
|
+
|
13
|
+
input_json = JSON[STDIN.read]
|
14
|
+
|
15
|
+
puts "Service: #{input_json['service']}"
|
16
|
+
puts "State: #{input_json['state']}"
|
17
|
+
puts "Metric: #{input_json['metric']}"
|
18
|
+
puts "Description: #{input_json['description']}"
|
19
|
+
|
20
|
+
def submitEvent(event)
|
21
|
+
# New Riemann client
|
22
|
+
c = Riemann::Client.new host: 'riemann.example.com', port: 5555
|
23
|
+
|
24
|
+
# Send event to Riemann server
|
25
|
+
c << {
|
26
|
+
service: event['service'],
|
27
|
+
state: event['state'],
|
28
|
+
metric: event['metric'].to_i,
|
29
|
+
description: event['description']
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
submitEvent(input_json)
|