solanum 0.2.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.
- checksums.yaml +7 -0
- data/README.md +53 -0
- data/bin/solanum +94 -0
- data/lib/solanum/config.rb +97 -0
- data/lib/solanum/matcher.rb +70 -0
- data/lib/solanum/source.rb +116 -0
- data/lib/solanum.rb +81 -0
- metadata +64 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e9ebaaa5f2e140d9a0b2d821bc685a363f103ea1
|
4
|
+
data.tar.gz: 4fc6fbb8e60b919d8a9d1d8c8d36d557a80269ca
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 30e99d6aa3fba2cf78f4ba69bde44e799212ca457a7de325ee40585e111c237188b3b1d0770c642a86641da5c253eab09a64310ae4a39fc2f7be6cee19a7d4f6
|
7
|
+
data.tar.gz: 254e37a291a4e3925e3a0e38cb7fde12f4a177508ba0354ed6ba035d2844fd086411ed040eedb63d421f51f2841f4d9277d9b8b32a26f232497f547a2a7e0160
|
data/README.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
Solanum
|
2
|
+
=======
|
3
|
+
|
4
|
+
This gem provides a domain-specific language (DSL) for collecting metrics
|
5
|
+
data in Ruby. The `solanum` script takes a number of monitoring configuration
|
6
|
+
scripts as arguments and periodically collects the metrics defined. The results
|
7
|
+
can be printed to the console or sent to a [Riemann](http://riemann.io/) server.
|
8
|
+
This requires the `riemann-client` gem to work.
|
9
|
+
|
10
|
+
## Structure
|
11
|
+
|
12
|
+
Solanum scripts define _sources_, which provide some string input when they are
|
13
|
+
read. This input is processed by a set of _matchers_ for each source, which can
|
14
|
+
generate named measurements from that data. A simple example would be a file
|
15
|
+
source, which is read and matched line-by-line against a set of regular
|
16
|
+
expressions.
|
17
|
+
|
18
|
+
The emitted measurements can undergo a bit more processing before being
|
19
|
+
reported. For example, some metrics are monotonically-increasing counters, and
|
20
|
+
what we actually want is the _difference_ between each reading. For others, we
|
21
|
+
may want to apply threshold-based states to the events. These are set by
|
22
|
+
_service prototypes_, which are also defined in the scripts.
|
23
|
+
|
24
|
+
## Examples
|
25
|
+
|
26
|
+
Here's an example of reading some information about the current system memory:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
# Read memory usage.
|
30
|
+
read "/proc/meminfo" do
|
31
|
+
match /^MemTotal:\s+(\d+) kB$/, cast: :to_i, scale: 1024, record: 'memory total bytes'
|
32
|
+
match /^MemFree:\s+(\d+) kB$/, cast: :to_i, scale: 1024, record: 'memory free bytes'
|
33
|
+
end
|
34
|
+
|
35
|
+
# Calculate percentages from total space.
|
36
|
+
compute do |metrics|
|
37
|
+
total = metrics['memory total bytes']
|
38
|
+
free = metrics['memory free bytes']
|
39
|
+
if total && free
|
40
|
+
metrics['memory free pct'] = free.to_f/total
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Define a service prototype with a threshold-based state.
|
45
|
+
service 'memory free pct', state: thresholds(0.00, :critical, 0.10, :warning, 0.25, :ok)
|
46
|
+
```
|
47
|
+
|
48
|
+
See the files in the `examples` directory for more monitor configuration samples.
|
49
|
+
|
50
|
+
## License
|
51
|
+
|
52
|
+
This is free and unencumbered software released into the public domain.
|
53
|
+
See the UNLICENSE file for more information.
|
data/bin/solanum
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$: << File.expand_path("../../lib", __FILE__)
|
4
|
+
|
5
|
+
require 'optparse'
|
6
|
+
require 'solanum'
|
7
|
+
|
8
|
+
$options = {
|
9
|
+
riemann_host: nil,
|
10
|
+
riemann_port: 5555,
|
11
|
+
interval: 5,
|
12
|
+
verbose: false,
|
13
|
+
}
|
14
|
+
|
15
|
+
$defaults = {
|
16
|
+
host: %x{hostname}.chomp,
|
17
|
+
tags: [],
|
18
|
+
ttl: 10,
|
19
|
+
}
|
20
|
+
|
21
|
+
def fail(msg, code=1)
|
22
|
+
STDERR.puts(msg)
|
23
|
+
exit code
|
24
|
+
end
|
25
|
+
|
26
|
+
def log(msg)
|
27
|
+
puts "%s %s" % [Time.now.strftime("%H:%M:%S"), msg] if $options[:verbose]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Parse command-line options.
|
31
|
+
options = OptionParser.new do |opts|
|
32
|
+
opts.banner = "Usage: #{File.basename($0)} [options] <monitor config> [monitor config] ..."
|
33
|
+
opts.separator ""
|
34
|
+
opts.separator "Event Attributes:"
|
35
|
+
opts.on( '--host HOST', "Event hostname (default: #{$defaults[:host]})") {|v| $defaults[:host] = v }
|
36
|
+
opts.on('-a', '--attribute KEY=VAL', "Attribute to add to the event (may be given multiple times)") {|attr| k,v = attr.split(/=/); if k and v then $defaults[k.intern] = v end }
|
37
|
+
opts.on('-t', '--tag TAG', "Tag to add to events (may be given multiple times)") {|v| $defaults[:tags] << v }
|
38
|
+
opts.on( '--ttl SECONDS', "Default TTL for events (default: #{$options[:ttl]})") {|v| $defaults[:ttl] = v.to_i }
|
39
|
+
opts.separator ""
|
40
|
+
opts.separator "General Options:"
|
41
|
+
opts.on( '--riemann-host HOST', "Riemann host to report events to") {|v| $options[:riemann_host] = v }
|
42
|
+
opts.on( '--riemann-port PORT', "Riemann port (default: #{$options[:riemann_port]})") {|v| $options[:riemann_port] = v.to_i }
|
43
|
+
opts.on('-i', '--interval SECONDS', "Seconds between updates (default: #{$options[:interval]})") {|v| $options[:interval] = v.to_i }
|
44
|
+
opts.on('-v', '--verbose', "Print additional information to stdout") { $options[:verbose] = true }
|
45
|
+
opts.on('-h', '--help', "Displays usage information") { print opts; exit }
|
46
|
+
end
|
47
|
+
options.parse!
|
48
|
+
|
49
|
+
# Check usage.
|
50
|
+
fail options if ARGV.empty?
|
51
|
+
|
52
|
+
|
53
|
+
|
54
|
+
##### MONITORING CONFIGS #####
|
55
|
+
|
56
|
+
$solanum = Solanum.new(ARGV)
|
57
|
+
fail "No sources loaded!" if $solanum.sources.empty?
|
58
|
+
|
59
|
+
if $options[:riemann_host]
|
60
|
+
begin
|
61
|
+
require 'riemann/client'
|
62
|
+
rescue LoadError
|
63
|
+
fail "ERROR: could not load Riemann client library! `gem install riemann-client` to enable reporting"
|
64
|
+
end
|
65
|
+
|
66
|
+
$riemann = Riemann::Client.new(host: $options[:riemann_host], port: $options[:riemann_port])
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
|
71
|
+
##### REPORT LOOP #####
|
72
|
+
|
73
|
+
trap "SIGINT" do
|
74
|
+
exit
|
75
|
+
end
|
76
|
+
|
77
|
+
loop do
|
78
|
+
$solanum.collect!
|
79
|
+
events = $solanum.build_events($defaults)
|
80
|
+
|
81
|
+
events.each do |event|
|
82
|
+
if $options[:verbose] || $riemann.nil?
|
83
|
+
puts "%-40s %5s (%s) %s" % [
|
84
|
+
event[:service], event[:metric],
|
85
|
+
event[:state].nil? ? "--" : event[:state],
|
86
|
+
event.inspect
|
87
|
+
]
|
88
|
+
end
|
89
|
+
|
90
|
+
$riemann << event if $riemann
|
91
|
+
end
|
92
|
+
|
93
|
+
sleep $options[:interval]
|
94
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'solanum/source'
|
2
|
+
|
3
|
+
class Solanum::Config
|
4
|
+
attr_reader :sources, :services
|
5
|
+
|
6
|
+
def initialize(path)
|
7
|
+
@sources = []
|
8
|
+
@services = []
|
9
|
+
|
10
|
+
instance_eval ::File.readlines(path).join, path, 1
|
11
|
+
|
12
|
+
raise "No sources loaded from monitor script: #{path}" if @sources.empty?
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# Registers a new source object. If a block is given, it is used to configure
|
19
|
+
# the source with instance_exec.
|
20
|
+
def register_source(source, config=nil)
|
21
|
+
source.instance_exec &config if config
|
22
|
+
@sources << source
|
23
|
+
source
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
# Registers a source which runs a command and matches against output lines.
|
28
|
+
def run(command, &config)
|
29
|
+
register_source Solanum::Source::Command.new(command), config
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# Registers a source which matches against the lines in a file.
|
34
|
+
def read(path, &config)
|
35
|
+
register_source Solanum::Source::File.new(path), config
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
# Registers a source which computes metrics directly.
|
40
|
+
def compute(&block)
|
41
|
+
register_source Solanum::Source::Compute.new(block)
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# Registers a pair of [matcher, prototype] where matcher is generally a string
|
46
|
+
# or regex to match a service name, and prototype is a map of :ttl, :state,
|
47
|
+
# :tags, etc.
|
48
|
+
def service(service, prototype={})
|
49
|
+
@services << [service, prototype]
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
##### HELPER METHODS #####
|
54
|
+
|
55
|
+
# Creates a state function based on thresholds. If the first argument is a
|
56
|
+
# symbol, it is taken as the default service state. Otherwise, arguments should
|
57
|
+
# be alternating numeric thresholds and state values to assign if the metric
|
58
|
+
# value exceeds the threshold.
|
59
|
+
#
|
60
|
+
# For example, for an 'availability' metric you often want to warn on low
|
61
|
+
# values. To assign a 'critical' state to values between 0% and 10%,
|
62
|
+
# 'warning' between 10% and 25%, and 'ok' above, use the following:
|
63
|
+
#
|
64
|
+
# thresholds(0.00, :critical, 0.10, :warning, 0.25, :ok)
|
65
|
+
#
|
66
|
+
# For 'usage' metrics it's the inverse, giving low values ok states and
|
67
|
+
# warning about high values:
|
68
|
+
#
|
69
|
+
# thresholds(:ok, 55, :warning, 65, :critical)
|
70
|
+
#
|
71
|
+
def thresholds(*args)
|
72
|
+
default_state = nil
|
73
|
+
default_state = args.shift unless args.first.kind_of? Numeric
|
74
|
+
|
75
|
+
# Check arguments.
|
76
|
+
raise "Thresholds must be paired with state values" unless args.count.even?
|
77
|
+
args.each_slice(2) do |threshold|
|
78
|
+
limit, state = *threshold
|
79
|
+
raise "Limits must be numeric: #{limit}" unless limit.kind_of? Numeric
|
80
|
+
raise "State values must be strings or symbols: #{state}" unless state.instance_of?(String) || state.instance_of?(Symbol)
|
81
|
+
end
|
82
|
+
|
83
|
+
# State block.
|
84
|
+
lambda do |v|
|
85
|
+
state = default_state
|
86
|
+
args.each_slice(2) do |threshold|
|
87
|
+
if threshold[0] < v
|
88
|
+
state = threshold[1]
|
89
|
+
else
|
90
|
+
break
|
91
|
+
end
|
92
|
+
end
|
93
|
+
state
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
# A matcher takes in an input string and returns a hash of measurement names to
|
4
|
+
# numeric values.
|
5
|
+
#
|
6
|
+
# Author:: Greg Look
|
7
|
+
class Solanum::Matcher
|
8
|
+
attr_reader :fn
|
9
|
+
|
10
|
+
# Creates a new Matcher which will run the given function on input.
|
11
|
+
def initialize(fn)
|
12
|
+
raise "function must be provided" if fn.nil?
|
13
|
+
@fn = fn
|
14
|
+
end
|
15
|
+
|
16
|
+
# Attempts to match the given input, returning a hash of metrics.
|
17
|
+
def call(input)
|
18
|
+
{}
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
### MATCHER TYPES ###
|
23
|
+
|
24
|
+
public
|
25
|
+
|
26
|
+
# LinePattern matchers define a regular expression which is tested against
|
27
|
+
# each line of input. The given function is called for **each** matched line,
|
28
|
+
# and the resulting measurements are merged together.
|
29
|
+
class LinePattern < Solanum::Matcher
|
30
|
+
def initialize(fn, pattern)
|
31
|
+
super fn
|
32
|
+
raise "pattern must be provided" if pattern.nil?
|
33
|
+
@pattern = pattern
|
34
|
+
end
|
35
|
+
|
36
|
+
def call(input)
|
37
|
+
raise "No input provided!" if input.nil?
|
38
|
+
lines = input.split("\n")
|
39
|
+
metrics = {}
|
40
|
+
|
41
|
+
lines.each do |line|
|
42
|
+
begin
|
43
|
+
if @pattern === line
|
44
|
+
measurements = @fn.call($~)
|
45
|
+
metrics.merge!(measurements) if measurements
|
46
|
+
end
|
47
|
+
rescue => e
|
48
|
+
STDERR.puts("Error calculating metrics from line match: #{e.inspect}")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
metrics
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
# JsonReader matches a JSON-formatted input string and passes the parsed
|
58
|
+
# object to the block body.
|
59
|
+
class JSONReader < Solanum::Matcher
|
60
|
+
def call(input)
|
61
|
+
begin
|
62
|
+
json = JSON.parse(input)
|
63
|
+
@fn.call(json)
|
64
|
+
rescue => e
|
65
|
+
STDERR.puts("Error matching JSON input: #{e.inspect}")
|
66
|
+
{}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'solanum/matcher'
|
2
|
+
|
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
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Collect input data from the given source.
|
38
|
+
def read_input!
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
|
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
|
+
|
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)
|
67
|
+
end
|
68
|
+
|
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
|
116
|
+
end
|
data/lib/solanum.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
# Class which wraps up an active Solanum monitoring system into an object.
|
2
|
+
#
|
3
|
+
# Author:: Greg Look
|
4
|
+
class Solanum
|
5
|
+
attr_reader :sources, :services, :metrics
|
6
|
+
|
7
|
+
require 'solanum/config'
|
8
|
+
require 'solanum/source'
|
9
|
+
|
10
|
+
|
11
|
+
# Loads the given monitoring scripts and initializes the sources and service
|
12
|
+
# definitions.
|
13
|
+
def initialize(scripts)
|
14
|
+
@sources = []
|
15
|
+
@services = []
|
16
|
+
@metrics = {}
|
17
|
+
|
18
|
+
scripts.each do |path|
|
19
|
+
begin
|
20
|
+
config = Solanum::Config.new(path)
|
21
|
+
@sources.concat(config.sources)
|
22
|
+
@services.concat(config.services)
|
23
|
+
rescue => e
|
24
|
+
STDERR.puts "Error loading monitor script #{path}: #{e}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
@sources.freeze
|
29
|
+
@services.freeze
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# Collects metrics from the given sources, in order. Updates the internal
|
34
|
+
# merged map of metric data.
|
35
|
+
def collect!
|
36
|
+
@old_metrics = @metrics
|
37
|
+
@metrics = @sources.reduce({}) do |metrics, source|
|
38
|
+
begin
|
39
|
+
new_metrics = source.collect(metrics) || {}
|
40
|
+
metrics.merge(new_metrics)
|
41
|
+
rescue => e
|
42
|
+
STDERR.puts "Error collecting metrics from #{source}: #{e}"
|
43
|
+
metrics
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
# Builds full events from a set of service prototypes, old metrics, and new
|
50
|
+
# metrics.
|
51
|
+
def build_events(defaults={})
|
52
|
+
@metrics.keys.sort.map do |service|
|
53
|
+
value = @metrics[service]
|
54
|
+
prototype = @services.select{|m| m[0] === service }.map{|m| m[1] }.reduce({}, &:merge)
|
55
|
+
|
56
|
+
state = prototype[:state] ? prototype[:state].call(value) : :ok
|
57
|
+
tags = ((prototype[:tags] || []) + (defaults[:tags] || [])).uniq
|
58
|
+
ttl = prototype[:ttl] || defaults[:ttl]
|
59
|
+
|
60
|
+
if prototype[:diff]
|
61
|
+
last = @old_metrics[service]
|
62
|
+
if last && last <= value
|
63
|
+
value = value - last
|
64
|
+
else
|
65
|
+
value = nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
if value
|
70
|
+
defaults.merge({
|
71
|
+
service: service,
|
72
|
+
metric: value,
|
73
|
+
state: state.to_s,
|
74
|
+
tags: tags,
|
75
|
+
ttl: ttl
|
76
|
+
})
|
77
|
+
end
|
78
|
+
end.compact
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: solanum
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Greg Look
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-07-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: riemann-client
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.2.2
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.2.2
|
27
|
+
description:
|
28
|
+
email: greg@greg-look.net
|
29
|
+
executables:
|
30
|
+
- solanum
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- lib/solanum/config.rb
|
35
|
+
- lib/solanum/matcher.rb
|
36
|
+
- lib/solanum/source.rb
|
37
|
+
- lib/solanum.rb
|
38
|
+
- bin/solanum
|
39
|
+
- README.md
|
40
|
+
homepage: https://github.com/greglook/solanum
|
41
|
+
licenses:
|
42
|
+
- Public Domain
|
43
|
+
metadata: {}
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - '>='
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 1.9.1
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - '>='
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '0'
|
58
|
+
requirements: []
|
59
|
+
rubyforge_project:
|
60
|
+
rubygems_version: 2.0.14
|
61
|
+
signing_key:
|
62
|
+
specification_version: 4
|
63
|
+
summary: DSL for custom monitoring configuration
|
64
|
+
test_files: []
|