solanum 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|