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.
@@ -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
@@ -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,5 @@
1
+ module Salus
2
+ class Gauge < Metric
3
+ STORAGE_DEPTH = 1
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ module Salus
2
+ class Text < Metric
3
+ STORAGE_DEPTH = 1
4
+ def initialize(defaults={})
5
+ super(defaults)
6
+ option :value, Symbol, String
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ require "json"
2
+ require "salus/renderer/base"
3
+ require "salus/renderer/block"
4
+ require "salus/renderer/stdout"
5
+ require "salus/renderer/collectd"
6
+ require "salus/renderer/graphite"
7
+ require "salus/renderer/zabbixbulk"
8
+ require "salus/renderer/zabbixsender"
@@ -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