solanum 0.2.0 → 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.
@@ -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