solanum 0.2.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +51 -45
- data/bin/solanum +11 -53
- data/lib/solanum.rb +109 -54
- data/lib/solanum/config.rb +67 -74
- data/lib/solanum/output/print.rb +18 -0
- data/lib/solanum/output/riemann.rb +20 -0
- data/lib/solanum/schedule.rb +55 -0
- data/lib/solanum/source.rb +13 -106
- data/lib/solanum/source/certificate.rb +88 -0
- data/lib/solanum/source/cpu.rb +119 -0
- data/lib/solanum/source/diskstats.rb +118 -0
- data/lib/solanum/source/load.rb +44 -0
- data/lib/solanum/source/memory.rb +70 -0
- data/lib/solanum/source/network.rb +65 -0
- data/lib/solanum/source/uptime.rb +28 -0
- data/lib/solanum/util.rb +40 -0
- metadata +31 -16
- checksums.yaml +0 -7
- data/lib/solanum/matcher.rb +0 -70
@@ -0,0 +1,18 @@
|
|
1
|
+
class Solanum
|
2
|
+
class Output
|
3
|
+
class Print
|
4
|
+
def initialize(args)
|
5
|
+
end
|
6
|
+
|
7
|
+
def write_events(events)
|
8
|
+
events.each do |event|
|
9
|
+
puts "%-40s %5s (%s) %s" % [
|
10
|
+
event['service'], event['metric'],
|
11
|
+
event['state'].nil? ? "--" : event['state'],
|
12
|
+
event.inspect
|
13
|
+
]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'riemann/client'
|
2
|
+
|
3
|
+
class Solanum
|
4
|
+
class Output
|
5
|
+
class Riemann
|
6
|
+
|
7
|
+
def initialize(args)
|
8
|
+
@client = Riemann::Client.new(host: args[:host], port: args[:port])
|
9
|
+
end
|
10
|
+
|
11
|
+
def write_events(events)
|
12
|
+
# OPTIMIZE: batch events?
|
13
|
+
events.each do |event|
|
14
|
+
@client << event
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# Schedule management class.
|
2
|
+
class Solanum
|
3
|
+
class Schedule
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@lock = Mutex.new
|
7
|
+
@timetable = []
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
# Peek at the next scheduled entry.
|
12
|
+
def peek_next
|
13
|
+
@lock.synchronize do
|
14
|
+
@timetable.first
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
# Time to spend waiting until the next scheduled entry. Returns a number of
|
20
|
+
# seconds, or nil if the schedule is empty.
|
21
|
+
def next_wait
|
22
|
+
entry = peek_next
|
23
|
+
if entry
|
24
|
+
next_time, next_id = *entry
|
25
|
+
duration = next_time - Time.now
|
26
|
+
#puts "Next scheduled run for #{next_id} at #{next_time} in #{duration} seconds" # DEBUG
|
27
|
+
duration
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
# Try to get the next ready entry. Returns the id if it is ready and removes
|
33
|
+
# it from the scheudle, otherwise nil if no entries are ready to run.
|
34
|
+
def pop_ready!
|
35
|
+
@lock.synchronize do
|
36
|
+
if @timetable.first && Time.now >= @timetable.first[0]
|
37
|
+
entry = @timetable.shift
|
38
|
+
entry[1]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# Schedule the given id for later running. Returns the scheduled entry.
|
45
|
+
def insert!(time, id)
|
46
|
+
entry = [time, id]
|
47
|
+
@lock.synchronize do
|
48
|
+
@timetable << entry
|
49
|
+
@timetable.sort_by! {|e| e[0] }
|
50
|
+
end
|
51
|
+
entry
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
data/lib/solanum/source.rb
CHANGED
@@ -1,116 +1,23 @@
|
|
1
|
-
|
1
|
+
class Solanum
|
2
|
+
class Source
|
3
|
+
attr_reader :type, :period, :attributes
|
2
4
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
#
|
8
|
-
# Author:: Greg Look
|
9
|
-
class Solanum::Source
|
10
|
-
attr_reader :config, :matchers
|
11
|
-
|
12
|
-
# Creates a new Source
|
13
|
-
def initialize(config)
|
14
|
-
@config = config
|
15
|
-
@matchers = []
|
16
|
-
end
|
17
|
-
|
18
|
-
# Collect input and process it with the defined matchers to produce some
|
19
|
-
# output measurements. The current set of metrics collected in this cycle
|
20
|
-
# will be passed to the measure function.
|
21
|
-
def collect(current_metrics)
|
22
|
-
input = read_input!
|
23
|
-
metrics = {}
|
24
|
-
|
25
|
-
unless input.nil?
|
26
|
-
@matchers.each do |matcher|
|
27
|
-
measurements = matcher.call(input)
|
28
|
-
metrics.merge!(measurements) if measurements
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
metrics
|
5
|
+
def initialize(opts)
|
6
|
+
@type = opts['type']
|
7
|
+
@period = (opts['period'] || 10).to_i
|
8
|
+
@attributes = opts['attributes']
|
33
9
|
end
|
34
10
|
|
35
|
-
private
|
36
11
|
|
37
|
-
|
38
|
-
|
39
|
-
nil
|
12
|
+
def collect!
|
13
|
+
raise "Not Yet Implemented"
|
40
14
|
end
|
41
15
|
|
42
|
-
# Declares a matcher for a single line of input.
|
43
|
-
def match(pattern, options={}, &block)
|
44
|
-
raise "pattern must be provided" if pattern.nil?
|
45
|
-
|
46
|
-
commands = 0
|
47
|
-
commands += 1 if options[:record]
|
48
|
-
commands += 1 if block_given?
|
49
|
-
raise "Must specify :record or provide a block to execute" if commands == 0
|
50
|
-
raise "Only one of :record or a block should be provided" if commands > 1
|
51
16
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
value = value.send(options[:cast]) if options[:cast]
|
56
|
-
value *= options[:scale] if options[:scale]
|
57
|
-
{options[:record] => value}
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
@matchers << Solanum::Matcher::LinePattern.new(block, pattern)
|
62
|
-
end
|
63
|
-
|
64
|
-
# Declares a matcher for JSON input.
|
65
|
-
def json(&block)
|
66
|
-
@matchers << Solanum::Matcher::JSONReader.new(block)
|
17
|
+
def next_run(from=Time.now)
|
18
|
+
jitter = 0.95 + 0.10*rand
|
19
|
+
from + jitter*@period
|
67
20
|
end
|
68
21
|
|
69
|
-
|
70
|
-
|
71
|
-
### SOURCE TYPES ###
|
72
|
-
|
73
|
-
public
|
74
|
-
|
75
|
-
class File < Solanum::Source
|
76
|
-
def read_input!
|
77
|
-
# Check that file exists and is readable.
|
78
|
-
raise "File does not exist: #{@config}" unless ::File.exists? @config
|
79
|
-
raise "File is not readable: #{@config}" unless ::File.readable? @config
|
80
|
-
|
81
|
-
# Read lines from the file.
|
82
|
-
::File.read(@config)
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
class Command < Solanum::Source
|
87
|
-
def read_input!
|
88
|
-
# Locate absolute command path.
|
89
|
-
command, args = @config.split(/\s/, 2)
|
90
|
-
abs_command =
|
91
|
-
if ::File.executable? command
|
92
|
-
command
|
93
|
-
else
|
94
|
-
%x{which #{command} 2> /dev/null}.chomp
|
95
|
-
end
|
96
|
-
|
97
|
-
# Check that command exists and is executable.
|
98
|
-
raise "Command #{command} not found" unless ::File.exist? abs_command
|
99
|
-
raise "Command #{abs_command} not executable" unless ::File.executable? abs_command
|
100
|
-
|
101
|
-
# Run command for output.
|
102
|
-
input = %x{#{abs_command} #{args}}
|
103
|
-
raise "Error executing command: #{abs_command} #{args}" unless $?.success?
|
104
|
-
|
105
|
-
input
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
class Compute < Solanum::Source
|
110
|
-
def collect(current_metrics)
|
111
|
-
# Compute metrics directly, but don't let the block change the existing
|
112
|
-
# metrics in-place.
|
113
|
-
@config.call(current_metrics.dup.freeze)
|
114
|
-
end
|
115
|
-
end
|
22
|
+
end
|
116
23
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'socket'
|
3
|
+
require 'solanum/source'
|
4
|
+
require 'solanum/util'
|
5
|
+
|
6
|
+
class Solanum::Source::Certificate < Solanum::Source
|
7
|
+
attr_reader :host, :port, :ca_cert, :expiry_states
|
8
|
+
|
9
|
+
|
10
|
+
def initialize(opts)
|
11
|
+
super(opts)
|
12
|
+
@host = opts['host'] or raise "No host provided"
|
13
|
+
@port = opts['port'] || 443
|
14
|
+
@hostname = opts['hostname'] || @host
|
15
|
+
@ca_cert = opts['ca_cert']
|
16
|
+
@expiry_states = opts['expiry_states'] || {}
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
def connect
|
21
|
+
# Configure context.
|
22
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
23
|
+
#ctx.verify_hostname = true # Only in ruby 2.x?
|
24
|
+
|
25
|
+
if @ca_cert
|
26
|
+
ctx.ca_file = @ca_cert
|
27
|
+
ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
28
|
+
end
|
29
|
+
|
30
|
+
# Open socket connection.
|
31
|
+
sock = TCPSocket.new(@host, @port)
|
32
|
+
ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx)
|
33
|
+
ssl.sync_close = true
|
34
|
+
ssl.hostname = @hostname
|
35
|
+
ssl.connect
|
36
|
+
|
37
|
+
yield ssl if block_given?
|
38
|
+
ensure
|
39
|
+
if ssl
|
40
|
+
ssl.close
|
41
|
+
elsif sock
|
42
|
+
sock.close
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
# Collect metric events.
|
48
|
+
def collect!
|
49
|
+
events = []
|
50
|
+
prefix = "certificate #{@hostname}"
|
51
|
+
|
52
|
+
begin
|
53
|
+
connect do |ssl|
|
54
|
+
cert = ssl.peer_cert
|
55
|
+
|
56
|
+
# Connect okay.
|
57
|
+
events << {
|
58
|
+
service: "#{prefix} connect",
|
59
|
+
metric: 1,
|
60
|
+
state: 'ok',
|
61
|
+
description: "Certificate for #{@hostname} verified successfully",
|
62
|
+
}
|
63
|
+
|
64
|
+
# Certificate expiration time.
|
65
|
+
#puts cert.inspect
|
66
|
+
expiry = cert.not_after - Time.now
|
67
|
+
expiry_days = expiry/86400
|
68
|
+
events << {
|
69
|
+
service: "#{prefix} expiry",
|
70
|
+
metric: expiry_days,
|
71
|
+
state: state_under(@expiry_states, expiry_days),
|
72
|
+
description: "Certificate for #{@hostname} expires in #{duration_str(expiry)}",
|
73
|
+
}
|
74
|
+
end
|
75
|
+
rescue => e
|
76
|
+
# Connect error.
|
77
|
+
events << {
|
78
|
+
service: "#{prefix} connect",
|
79
|
+
metric: 1,
|
80
|
+
state: 'critical',
|
81
|
+
description: "Error connecting to #{@hostname}: #{e}",
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
events
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'solanum/source'
|
2
|
+
require 'solanum/util'
|
3
|
+
|
4
|
+
class Solanum::Source::Cpu < Solanum::Source
|
5
|
+
attr_reader :detailed, :per_core, :usage_states
|
6
|
+
|
7
|
+
STAT_FILE = '/proc/stat'
|
8
|
+
STATES = %w{user nice system idle iowait irqhard irqsoft}
|
9
|
+
|
10
|
+
|
11
|
+
def initialize(opts)
|
12
|
+
super(opts)
|
13
|
+
@last = nil
|
14
|
+
@detailed = opts['detailed'] || false
|
15
|
+
@per_core = opts['per_core'] || false
|
16
|
+
@usage_states = opts['usage_states'] || {}
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
# Calculate cpu utilization from the cumulative time spent in
|
21
|
+
# 'jiffies' (1/100 sec) since system boot.
|
22
|
+
def parse_info(line)
|
23
|
+
fields = line.chomp.split(' ')
|
24
|
+
name = fields.shift
|
25
|
+
data = Hash.new(0)
|
26
|
+
STATES.each_with_index do |state, i|
|
27
|
+
data[state] = fields[i].to_i
|
28
|
+
end
|
29
|
+
return name, data
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# Collect metric events.
|
34
|
+
def collect!
|
35
|
+
# Parse lines from usage stats.
|
36
|
+
lines = File.readlines(STAT_FILE).take_while {|l| l.start_with? 'cpu' }
|
37
|
+
totals = {}
|
38
|
+
lines.each do |line|
|
39
|
+
name, data = parse_info(line)
|
40
|
+
totals[name] = data
|
41
|
+
end
|
42
|
+
|
43
|
+
# Need at least one run to start reporting accurate metrics.
|
44
|
+
unless @last
|
45
|
+
@last = totals
|
46
|
+
return []
|
47
|
+
end
|
48
|
+
|
49
|
+
# Calculate diffs from previous measurements.
|
50
|
+
diffs = {}
|
51
|
+
totals.each do |name, data|
|
52
|
+
diffs[name] = Hash.new(0)
|
53
|
+
data.each do |state, jiffies|
|
54
|
+
last = @last[name] && @last[name][state]
|
55
|
+
diffs[name][state] = jiffies - last if last
|
56
|
+
end
|
57
|
+
end
|
58
|
+
@last = totals
|
59
|
+
|
60
|
+
# Convert diffs to relative percentages.
|
61
|
+
diffs.each do |name, diff|
|
62
|
+
elapsed = diff.values.reduce(&:+)
|
63
|
+
diff.keys.each do |state|
|
64
|
+
diff[state] = diff[state].to_f/elapsed
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Generate metric events.
|
69
|
+
events = []
|
70
|
+
|
71
|
+
# aggregate cpu usage
|
72
|
+
usage = 1.0 - diffs['cpu']['idle']
|
73
|
+
events << {
|
74
|
+
service: 'cpu usage',
|
75
|
+
metric: usage,
|
76
|
+
state: state_over(@usage_states, usage),
|
77
|
+
}
|
78
|
+
|
79
|
+
# detailed aggregate cpu state metrics
|
80
|
+
if @detailed
|
81
|
+
diffs['cpu'].each do |state, usage|
|
82
|
+
events << {
|
83
|
+
service: 'cpu state',
|
84
|
+
metric: usage,
|
85
|
+
cpu_state: state,
|
86
|
+
}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# even more detailed per-core usage
|
91
|
+
if @per_core
|
92
|
+
diffs.each do |name, diff|
|
93
|
+
next if name == 'cpu'
|
94
|
+
|
95
|
+
usage = 1.0 - diff['idle']
|
96
|
+
events << {
|
97
|
+
service: 'cpu core usage',
|
98
|
+
metric: usage,
|
99
|
+
state: state_over(@usage_states, usage),
|
100
|
+
cpu_core: name,
|
101
|
+
}
|
102
|
+
|
103
|
+
if @detailed
|
104
|
+
diff.each do |state, usage|
|
105
|
+
events << {
|
106
|
+
service: 'cpu core state',
|
107
|
+
metric: usage,
|
108
|
+
cpu_core: name,
|
109
|
+
cpu_state: state,
|
110
|
+
}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
events
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'solanum/source'
|
2
|
+
|
3
|
+
class Solanum::Source::Diskstats < Solanum::Source
|
4
|
+
attr_reader :devices
|
5
|
+
|
6
|
+
STAT_FILE = '/proc/diskstats'
|
7
|
+
|
8
|
+
FIELDS = %w{
|
9
|
+
major minor name
|
10
|
+
reads_completed reads_merged read_sectors read_time
|
11
|
+
writes_completed writes_merged write_sectors write_time
|
12
|
+
io_active io_time io_weighted_time
|
13
|
+
}
|
14
|
+
|
15
|
+
|
16
|
+
def initialize(opts)
|
17
|
+
super(opts)
|
18
|
+
@devices = opts['devices'] || []
|
19
|
+
@detailed = opts['detailed'] || false
|
20
|
+
@last = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def parse_stats(line)
|
25
|
+
columns = line.strip.split(/\s+/)
|
26
|
+
columns = columns.take(3) + columns.drop(3).map(&:to_i)
|
27
|
+
Hash[FIELDS.zip(columns)]
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def collect!
|
32
|
+
events = []
|
33
|
+
|
34
|
+
File.readlines(STAT_FILE).each do |line|
|
35
|
+
stats = parse_stats(line)
|
36
|
+
device = stats['name']
|
37
|
+
|
38
|
+
if @devices.include?(device) || (@devices.empty? && device =~ /^sd[a-z]$/)
|
39
|
+
if @last[device]
|
40
|
+
diff = Hash.new(0)
|
41
|
+
FIELDS.drop(3).each do |field|
|
42
|
+
diff[field] = stats[field] - @last[device][field]
|
43
|
+
end
|
44
|
+
prefix = "diskstats #{device}"
|
45
|
+
|
46
|
+
# Reads
|
47
|
+
|
48
|
+
events << {
|
49
|
+
service: "#{prefix} read sectors",
|
50
|
+
metric: diff['read_sectors'],
|
51
|
+
}
|
52
|
+
|
53
|
+
events << {
|
54
|
+
service: "#{prefix} read time",
|
55
|
+
metric: diff['read_time'],
|
56
|
+
}
|
57
|
+
|
58
|
+
if @detailed
|
59
|
+
events << {
|
60
|
+
service: "#{prefix} read completed",
|
61
|
+
metric: diff['reads_completed'],
|
62
|
+
}
|
63
|
+
|
64
|
+
events << {
|
65
|
+
service: "#{prefix} read merged",
|
66
|
+
metric: diff['reads_merged'],
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
# Writes
|
71
|
+
|
72
|
+
events << {
|
73
|
+
service: "#{prefix} write sectors",
|
74
|
+
metric: diff['write_sectors'],
|
75
|
+
}
|
76
|
+
|
77
|
+
events << {
|
78
|
+
service: "#{prefix} write time",
|
79
|
+
metric: diff['write_time'],
|
80
|
+
}
|
81
|
+
|
82
|
+
if @detailed
|
83
|
+
events << {
|
84
|
+
service: "#{prefix} write completed",
|
85
|
+
metric: diff['writes_completed'],
|
86
|
+
}
|
87
|
+
|
88
|
+
events << {
|
89
|
+
service: "#{prefix} write merged",
|
90
|
+
metric: diff['writes_merged'],
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
# IO
|
95
|
+
|
96
|
+
events << {
|
97
|
+
service: "#{prefix} io active",
|
98
|
+
metric: diff['io_active'],
|
99
|
+
}
|
100
|
+
|
101
|
+
events << {
|
102
|
+
service: "#{prefix} io time",
|
103
|
+
metric: diff['io_time'],
|
104
|
+
}
|
105
|
+
|
106
|
+
events << {
|
107
|
+
service: "#{prefix} io weighted-time",
|
108
|
+
metric: diff['io_weighted_time'],
|
109
|
+
}
|
110
|
+
end
|
111
|
+
@last[device] = stats
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
events
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|