salus 0.1.2
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/.gitignore +12 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/LICENSE.md +29 -0
- data/README.md +254 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/salus +12 -0
- data/lib/salus.rb +183 -0
- data/lib/salus/cli.rb +53 -0
- data/lib/salus/cli/baseutils.rb +92 -0
- data/lib/salus/cli/zabbix.rb +150 -0
- data/lib/salus/configuration.rb +38 -0
- data/lib/salus/group.rb +159 -0
- data/lib/salus/logging.rb +15 -0
- data/lib/salus/metric.rb +173 -0
- data/lib/salus/metric/absolute.rb +21 -0
- data/lib/salus/metric/counter.rb +41 -0
- data/lib/salus/metric/derive.rb +36 -0
- data/lib/salus/metric/gauge.rb +5 -0
- data/lib/salus/metric/text.rb +9 -0
- data/lib/salus/renderer.rb +8 -0
- data/lib/salus/renderer/base.rb +50 -0
- data/lib/salus/renderer/block.rb +13 -0
- data/lib/salus/renderer/collectd.rb +21 -0
- data/lib/salus/renderer/graphite.rb +14 -0
- data/lib/salus/renderer/stdout.rb +18 -0
- data/lib/salus/renderer/zabbixbulk.rb +30 -0
- data/lib/salus/renderer/zabbixsender.rb +24 -0
- data/lib/salus/thread.rb +8 -0
- data/lib/salus/thread/cpu.rb +18 -0
- data/lib/salus/thread/future.rb +168 -0
- data/lib/salus/thread/latch.rb +28 -0
- data/lib/salus/thread/lockable.rb +56 -0
- data/lib/salus/thread/monotonictime.rb +15 -0
- data/lib/salus/thread/observable.rb +117 -0
- data/lib/salus/thread/pool.rb +482 -0
- data/lib/salus/version.rb +3 -0
- data/lib/salus/zabbix.rb +30 -0
- data/salus.gemspec +29 -0
- metadata +143 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require "logger"
|
|
2
|
+
|
|
3
|
+
module Salus
|
|
4
|
+
# Loosely based on code from https://github.com/ruby-concurrency/concurrent-ruby/
|
|
5
|
+
module Logging
|
|
6
|
+
include Logger::Severity
|
|
7
|
+
|
|
8
|
+
def log(level, message = nil, progname = nil, &block)
|
|
9
|
+
(@logger || Salus.logger).add level, message, progname, &block
|
|
10
|
+
rescue => error
|
|
11
|
+
$stderr.puts "Failed to log #{[level, progname, message, block]}\n" +
|
|
12
|
+
"#{error.message} (#{error.class})\n#{error.backtrace.join "\n"}"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/salus/metric.rb
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
module Salus
|
|
2
|
+
class Fifo
|
|
3
|
+
extend Forwardable
|
|
4
|
+
def_delegators :@data, :[], :each, :length, :empty?, :clear, :last, :first, :hash, :map
|
|
5
|
+
|
|
6
|
+
def initialize(maxlen)
|
|
7
|
+
@maxlen = maxlen
|
|
8
|
+
@data = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def <<(value)
|
|
12
|
+
@data << value
|
|
13
|
+
@data.shift if @data.length > @maxlen
|
|
14
|
+
self
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Metric
|
|
19
|
+
include Logging
|
|
20
|
+
include Lockable
|
|
21
|
+
STORAGE_DEPTH = 2
|
|
22
|
+
|
|
23
|
+
Value = Struct.new(:value, :timestamp, :ttl) do
|
|
24
|
+
def expired?(ts=nil)
|
|
25
|
+
return false if ttl.nil?
|
|
26
|
+
ts ||= Time.now.to_f
|
|
27
|
+
ts > (timestamp + ttl)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.inherited(subclass)
|
|
32
|
+
@@descendants ||= []
|
|
33
|
+
@@descendants << subclass
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.descendants
|
|
37
|
+
@@descendants || []
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initialize(defaults={})
|
|
41
|
+
@values = Fifo.new(self.class::STORAGE_DEPTH)
|
|
42
|
+
@opts = defaults.clone
|
|
43
|
+
@attributes = {}
|
|
44
|
+
@last_calced_value = nil
|
|
45
|
+
@last_calced_ts = nil
|
|
46
|
+
@needs_update = true
|
|
47
|
+
|
|
48
|
+
option :mute, TrueClass, FalseClass
|
|
49
|
+
option :value, Numeric
|
|
50
|
+
option :timestamp, Numeric
|
|
51
|
+
option :ttl, Numeric
|
|
52
|
+
|
|
53
|
+
@opts.each do |k, v|
|
|
54
|
+
validate(k, v)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def mute?
|
|
59
|
+
synchronize { @opts[:mute] || false }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def push(opts={}, &block)
|
|
63
|
+
opts = {} unless opts.is_a?(Hash)
|
|
64
|
+
|
|
65
|
+
synchronize do
|
|
66
|
+
opts.each do |k, v|
|
|
67
|
+
validate(k, v)
|
|
68
|
+
@opts[k] = v unless [:value, :ttl, :timestamp].include?(k)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
if block_given?
|
|
72
|
+
v = begin
|
|
73
|
+
yield
|
|
74
|
+
rescue Exception => e
|
|
75
|
+
log DEBUG, e
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
validate(:value, v)
|
|
79
|
+
opts[:value] = v
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
@values << Value.new(opts[:value], opts[:timestamp] || Time.now.to_f, opts[:ttl] || @opts[:ttl])
|
|
83
|
+
@needs_update = true
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def timestamp
|
|
88
|
+
synchronize do
|
|
89
|
+
calc if @needs_update
|
|
90
|
+
@last_calced_ts
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def value
|
|
95
|
+
synchronize do
|
|
96
|
+
calc if @needs_update
|
|
97
|
+
@last_calced_value
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def ttl
|
|
102
|
+
synchronize do
|
|
103
|
+
@values.empty? ? nil : @values.last.ttl
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def expired?(ts=nil)
|
|
108
|
+
synchronize do
|
|
109
|
+
if @values.empty?
|
|
110
|
+
true
|
|
111
|
+
else
|
|
112
|
+
@values.last.expired?(ts)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def load(data)
|
|
118
|
+
return if data.nil?
|
|
119
|
+
return if data.empty?
|
|
120
|
+
return unless data.key?(:values)
|
|
121
|
+
synchronize do
|
|
122
|
+
if data.key?(:mute)
|
|
123
|
+
@opts[:mute] = data[:mute]
|
|
124
|
+
end
|
|
125
|
+
data[:values].each do |v|
|
|
126
|
+
@values << Value.new(v[:value], v[:timestamp], v[:ttl])
|
|
127
|
+
end
|
|
128
|
+
@needs_update = true
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def save
|
|
133
|
+
to_h
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def to_h
|
|
137
|
+
return {} if @values.empty?
|
|
138
|
+
synchronize do
|
|
139
|
+
{
|
|
140
|
+
type: self.class.name.split('::').last,
|
|
141
|
+
mute: mute?,
|
|
142
|
+
values: @values.map { |x| x.to_h }
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
protected
|
|
148
|
+
def option(key, *types)
|
|
149
|
+
@attributes[key] = types
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def validate(key, value)
|
|
153
|
+
return if value.nil?
|
|
154
|
+
return unless @attributes.key?(key)
|
|
155
|
+
unless @attributes[key].any? { |t| value.is_a?(t) }
|
|
156
|
+
raise ArgumentError, "Option #{key} should be #{@attributes[key].join(" or ")}"
|
|
157
|
+
end
|
|
158
|
+
value
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def calc
|
|
162
|
+
if @values.empty?
|
|
163
|
+
@last_calced_ts = nil
|
|
164
|
+
@last_calced_value = nil
|
|
165
|
+
@needs_update = true
|
|
166
|
+
else
|
|
167
|
+
@last_calced_ts = @values.last.timestamp
|
|
168
|
+
@last_calced_value = @values.last.value
|
|
169
|
+
@needs_update = false
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Salus
|
|
2
|
+
class Absolute < Metric
|
|
3
|
+
def calc
|
|
4
|
+
super
|
|
5
|
+
@last_calced_value = nil
|
|
6
|
+
|
|
7
|
+
if @values.length < STORAGE_DEPTH
|
|
8
|
+
return
|
|
9
|
+
elsif @values[0].expired?(@values[1].timestamp)
|
|
10
|
+
return
|
|
11
|
+
elsif !@values[1].value.is_a?(Numeric)
|
|
12
|
+
return
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
@last_calced_value = begin
|
|
16
|
+
dt = (@values[1].timestamp - @values[0].timestamp)
|
|
17
|
+
(dt == 0) ? nil : (@values[1].value / dt)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Salus
|
|
2
|
+
class Counter < Metric
|
|
3
|
+
INT32_MAX = 2**32
|
|
4
|
+
def initialize(defaults={})
|
|
5
|
+
super(defaults)
|
|
6
|
+
option :maximum, Numeric
|
|
7
|
+
validate(:maximum, @opts[:maximum]) if @opts.key?(:maximum)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def calc
|
|
11
|
+
super
|
|
12
|
+
@last_calced_value = nil
|
|
13
|
+
|
|
14
|
+
if @values.length < STORAGE_DEPTH
|
|
15
|
+
return
|
|
16
|
+
elsif @values[0].expired?(@values[1].timestamp)
|
|
17
|
+
return
|
|
18
|
+
elsif !@values[0].value.is_a?(Numeric)
|
|
19
|
+
return
|
|
20
|
+
elsif !@values[1].value.is_a?(Numeric)
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@last_calced_value = begin
|
|
25
|
+
dt = (@values[1].timestamp - @values[0].timestamp)
|
|
26
|
+
dv = if @values[1].value < @values[0].value
|
|
27
|
+
w = @values[0].value < INT32_MAX ? 32 : 64
|
|
28
|
+
(2**w - @values[0].value + @values[1].value)
|
|
29
|
+
else
|
|
30
|
+
(@values[1].value - @values[0].value)
|
|
31
|
+
end
|
|
32
|
+
r = (dt == 0) ? nil : (dv / dt)
|
|
33
|
+
if @opts.key?(:maximum) && !r.nil? && r > @opts[:maximum]
|
|
34
|
+
nil
|
|
35
|
+
else
|
|
36
|
+
r
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Salus
|
|
2
|
+
class Derive < Metric
|
|
3
|
+
def initialize(defaults={})
|
|
4
|
+
super(defaults)
|
|
5
|
+
option :minimum, Numeric
|
|
6
|
+
validate(:minimum, @opts[:minimum]) if @opts.key?(:minimum)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
protected
|
|
10
|
+
def calc
|
|
11
|
+
super
|
|
12
|
+
@last_calced_value = nil
|
|
13
|
+
|
|
14
|
+
if @values.length < STORAGE_DEPTH
|
|
15
|
+
return
|
|
16
|
+
elsif @values[0].expired?(@values[1].timestamp)
|
|
17
|
+
return
|
|
18
|
+
elsif !@values[0].value.is_a?(Numeric)
|
|
19
|
+
return
|
|
20
|
+
elsif !@values[1].value.is_a?(Numeric)
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@last_calced_value = begin
|
|
25
|
+
dt = (@values[1].timestamp - @values[0].timestamp)
|
|
26
|
+
dv = (@values[1].value - @values[0].value)
|
|
27
|
+
r = (dt == 0) ? nil : (dv / dt)
|
|
28
|
+
if @opts.key?(:minimum) && !r.nil? && r < @opts[:minimum]
|
|
29
|
+
nil
|
|
30
|
+
else
|
|
31
|
+
r
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Salus
|
|
2
|
+
class BaseRenderer
|
|
3
|
+
include Logging
|
|
4
|
+
def self.inherited(subclass)
|
|
5
|
+
@@descendants ||= []
|
|
6
|
+
@@descendants << subclass
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.descendants
|
|
10
|
+
@@descendants || []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(opts={})
|
|
14
|
+
@separator = opts.fetch(:separator, '.')
|
|
15
|
+
@allow_mute = opts.fetch(:allow_mute, false)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def render(data)
|
|
19
|
+
# Implement renderer
|
|
20
|
+
raise "Unimplemented"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def iterate(node, prefix="", &block)
|
|
24
|
+
case node
|
|
25
|
+
when Hash
|
|
26
|
+
node.each do |name, item|
|
|
27
|
+
iterate(item, join_name(prefix, name), &block)
|
|
28
|
+
end
|
|
29
|
+
when Salus::Group
|
|
30
|
+
node.each(@allow_mute) do |name, metric|
|
|
31
|
+
iterate(metric, join_name(prefix, name), &block)
|
|
32
|
+
end
|
|
33
|
+
if node.has_subgroups?
|
|
34
|
+
node.groups.each do |name, group|
|
|
35
|
+
iterate(group, join_name(prefix, name), &block)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
when Salus::Metric
|
|
39
|
+
block.call(prefix, node) unless node.expired?
|
|
40
|
+
else
|
|
41
|
+
log WARN, "Unknown node type #{node.class}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
protected
|
|
46
|
+
def join_name(prefix, name)
|
|
47
|
+
prefix + (prefix.empty? ? '' : @separator) + name
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Salus
|
|
2
|
+
class BlockRenderer < BaseRenderer
|
|
3
|
+
def initialize(opts={}, &block)
|
|
4
|
+
super(opts)
|
|
5
|
+
raise ArgumentError, "Block must be supplied" unless block_given?
|
|
6
|
+
@proc = block
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def render(data)
|
|
10
|
+
instance_exec(data, &@proc)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Salus
|
|
2
|
+
class CollectdRenderer < BaseRenderer
|
|
3
|
+
def initialize(opts={})
|
|
4
|
+
opts[:separator] = opts.fetch(:separator, '/')
|
|
5
|
+
super(opts)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def render(data)
|
|
9
|
+
hostname = ENV.fetch('COLLECTD_HOSTNAME', 'localhost')
|
|
10
|
+
options = ENV.key?('COLLECTD_INTERVAL') ? "interval=#{ENV['COLLECTD_INTERVAL']} " : ''
|
|
11
|
+
iterate(data) do |name, metric|
|
|
12
|
+
# Text metrics are unsupported
|
|
13
|
+
next if metric.is_a? Salus::Text
|
|
14
|
+
unless metric.timestamp.nil?
|
|
15
|
+
# Effectively all salus metrics are gauges for collectd, with exception to text
|
|
16
|
+
STDOUT.puts "PUTVAL #{hostname}#{@separator}#{name} #{options}#{metric.timestamp.to_i}:#{metric.value.nil? ? 'U' : metric.value}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Salus
|
|
2
|
+
class GraphiteRenderer < BaseRenderer
|
|
3
|
+
def render(data)
|
|
4
|
+
iterate(data) do |name, metric|
|
|
5
|
+
# Text metrics are unsupported
|
|
6
|
+
next if metric.is_a? Salus::Text
|
|
7
|
+
# Nil value means nothing collected, so just ignore it
|
|
8
|
+
unless metric.value.nil? || metric.timestamp.nil?
|
|
9
|
+
STDOUT.puts "#{name} #{metric.value} #{metric.timestamp.to_i}"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Salus
|
|
2
|
+
class StdoutRenderer < BaseRenderer
|
|
3
|
+
def initialize(opts={})
|
|
4
|
+
super(opts)
|
|
5
|
+
@precision = opts.fetch(:precision, 2)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def render(data)
|
|
9
|
+
iterate(data) do |name, metric|
|
|
10
|
+
value = metric.value
|
|
11
|
+
unless metric.is_a?(Salus::Text)
|
|
12
|
+
value = "%.#{@precision}f" % value unless value.nil?
|
|
13
|
+
end
|
|
14
|
+
STDOUT.puts "[#{Time.at(metric.timestamp)}] #{name} - #{value}" unless metric.timestamp.nil?
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|