solanum 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -1,116 +1,23 @@
1
- require 'solanum/matcher'
1
+ class Solanum
2
+ class Source
3
+ attr_reader :type, :period, :attributes
2
4
 
3
- # This class represents a source of data, whether read from command output,
4
- # a file on the system, or just calculated from other values. Each source
5
- # may have multiple matchers, which will be run against the input data to
6
- # produce metrics. Each matcher sees the _whole_ input.
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
- # Collect input data from the given source.
38
- def read_input!
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
- if options[:record]
53
- block = lambda do |matches|
54
- value = matches[1]
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