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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0adb5863d10056a25ec392981e682cdc8a27fe88
4
+ data.tar.gz: 54011bd7dc54a824778e604ea85f54040ad1445c
5
+ SHA512:
6
+ metadata.gz: bfc09ce8d330fea7668c58062fba29cff5d80d6cfd4394fa80f922d6a97d6fe636cb49cef1f2c344d3915578ab715200adb28a871857c9de87b1da2daf53b262
7
+ data.tar.gz: a23b0c69dcd2e6e5f6caf23f8513a5a8678f58857aa30b710652096ef121c0946fa1f4fbf32b85f62e88f2df2621f26f84d5a176deb24a2a08cffa0904e3b5a2
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.1
5
+ before_install: gem install bundler -v 1.15.3
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in salus.gemspec
6
+ gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,29 @@
1
+ Salus is licensed under the BSD license.
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are
5
+ met:
6
+
7
+ (1) Redistributions of source code must retain the above copyright
8
+ notice, this list of conditions and the following disclaimer.
9
+
10
+ (2) Redistributions in binary form must reproduce the above copyright
11
+ notice, this list of conditions and the following disclaimer in
12
+ the documentation and/or other materials provided with the
13
+ distribution.
14
+
15
+ (3) The name of the author may not be used to
16
+ endorse or promote products derived from this software without
17
+ specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
20
+ IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
23
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
27
+ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
28
+ IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
+ POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,254 @@
1
+ # Salus
2
+
3
+ Salus is a simple DSL for writing collector agents for different monitoring systems. I'm just tired of rewriting those primitives from scratch for every new check I'm willing to add to a monitoring system.
4
+
5
+ _This is alpha quality software right now, but you might help to improve it_
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'salus'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install salus
22
+
23
+ ## Usage
24
+
25
+ The gem can be used from your own script or by using CLI.
26
+
27
+ Quick sample:
28
+
29
+ ```ruby
30
+ require "json"
31
+ default ttl: 60
32
+ var state_file: "system.state.yml"
33
+
34
+ group "cpu" do
35
+ data = File.open("/proc/stat").read.split(/\n/).grep(/^cpu /)
36
+ data.each do |l|
37
+ name, user, nice, csystem, idle, iowait, irq, softirq = l.split(/\s+/)
38
+
39
+ busy = user.to_i + nice.to_i + csystem.to_i + iowait.to_i + irq.to_i + softirq.to_i
40
+ total = busy + idle.to_i
41
+
42
+ counter "busy", value: busy, mute: true
43
+ counter "total", value: total, mute: true
44
+ gauge "usage" do
45
+ value("busy") / value("total") * 100
46
+ end
47
+ end
48
+ end
49
+
50
+ group "memory" do
51
+ data = File.open("/proc/meminfo").read.split(/\n/).grep(/^Mem/)
52
+ data.each do |l|
53
+ name, value = l.match(/Mem(?<name>.+):\s+(?<value>\d+)\s/)[1,2]
54
+ gauge name.downcase, value: value.to_i, mute: true
55
+ end
56
+ gauge "usage" do
57
+ (value("total") - value("free")) / value("total").to_f * 100
58
+ end
59
+ end
60
+
61
+ render do |data|
62
+ iterate(data) do |name, metric|
63
+ puts ({:name => name, :value => metric.value, :timestamp => metric.timestamp, :ttl => metric.ttl}.to_json)
64
+ end
65
+ end
66
+ ```
67
+
68
+ Save it as `sample.salus` and run `salus`.
69
+
70
+ ```bash
71
+ $ salus -f sample.salus
72
+ {"name":"cpu.usage","value":6.433521607455635,"timestamp":1510699623.8592708,"ttl":60}
73
+ {"name":"memory.usage","value":25.60121068625779,"timestamp":1510699623.863265,"ttl":60}
74
+ ```
75
+
76
+ Because `cpu.usage` is made of counters, you'll have to run the command at least twice. Be aware of `salus` making `system.state.yml` for persisting it's state. You may redefine file name and location with `-s` switch.
77
+
78
+ You can also run in infinite loop mode
79
+
80
+ ```bash
81
+ $ salus loop -f sample.salus
82
+ {"name":"cpu.usage","value":12.528473606797895,"timestamp":1510700076.4455612,"ttl":60}
83
+ {"name":"memory.usage","value":25.57683405209171,"timestamp":1510700076.447059,"ttl":60}
84
+ {"name":"cpu.usage","value":8.239946939924048,"timestamp":1510700106.3334389,"ttl":60}
85
+ {"name":"memory.usage","value":25.535366304014968,"timestamp":1510700106.33444,"ttl":60}
86
+ {"name":"cpu.usage","value":2.840158185017875,"timestamp":1510700136.364328,"ttl":60}
87
+ {"name":"memory.usage","value":25.537265316671707,"timestamp":1510700136.36533,"ttl":60}
88
+ ^C
89
+ ```
90
+
91
+ You may invoke several salus scripts at once, just specify all of them in a space delimited list. You might also specify a directory with `Salusfile` or many `*.salus` files. By default `salus` does search `*.salus` and `Salusfile` in the current directory.
92
+
93
+ ### Primitives
94
+
95
+ #### Group
96
+
97
+ Group is the base unit of work. You should write your metric collecting code inside groups. Groups can be nested. Top level groups are run in separate threads. Group might include metrics. Groups must be named.
98
+
99
+ ```ruby
100
+ group "test" do
101
+ gauge "test1", value: 10
102
+ gauge "test2", value: 30, mute: true
103
+ end
104
+ ```
105
+
106
+ #### Metrics
107
+
108
+ Could be one of the following (mimicking RRDtool data sources):
109
+ * Gauge: values stored as is
110
+ * Derive: a rate of something per second, best suites for values that rarely overflow
111
+ * Counter: almost same as derive, but with overflow detection for 32- and 64-bit counters
112
+ * Absolute: a rate of a counter, which resets on reading
113
+ * Text: just a text stored as is
114
+
115
+ A metric should have a name and a value at the very minimum. You might also specify a custom or default TTL. A metric can be also `mute`, which means it wouldn't appear in the output, unless told to do so.
116
+
117
+ An expired metric is considered invalid, so if you use counter or derive with ttl less than collecting interval, you'll always get nils.
118
+
119
+ ```ruby
120
+ group "test" do
121
+ gauge "test1", value: 10, ttl: 50
122
+ counter "test2", value: 10, mute: true
123
+ derive "test3", value: 30, timestamp: Time.now.to_f + 10
124
+ end
125
+ ```
126
+
127
+ You might also use a block to calculate metrics value. In this case any exception which happened in the block would be muted, and the nil value returned instead.
128
+
129
+ ```ruby
130
+ group "test" do
131
+ # this would always produce nil
132
+ gauge "division by zero" do
133
+ 100 / 0
134
+ end
135
+ end
136
+ ```
137
+
138
+ #### Renderers
139
+
140
+ A renderer is a class, which is used to render the actual output, would it be just STDOUT, a file or tcp/ip service.
141
+
142
+ ```ruby
143
+ render(StdoutRenderer.new(separator: "/"))
144
+ ```
145
+
146
+ This code would produce something like that
147
+
148
+ ```
149
+ [2017-11-15 02:25:16 +0300] cpu/usage - 4.00
150
+ [2017-11-15 02:25:16 +0300] memory/usage - 26.23
151
+ ```
152
+
153
+ You may add more than one renderer at once and send your data to as many monitoring services as you want.
154
+
155
+ Check sample renderers for examples.
156
+
157
+ ### Pipeline
158
+
159
+ Salus pipeline is rather straightforward and consists of two stages:
160
+ * Collect data (execute groups' code)
161
+ * Send data (execute renderers' code)
162
+
163
+ You might run it once by cron or in infinite loop mode. Each stage is executed using embed thread pool with pre-set timeouts.
164
+
165
+ Thread pool and timeouts could be configured using `configure`
166
+
167
+ ```ruby
168
+ Salus.configure do |config|
169
+ # Thread pool settings
170
+ config.min_threads = (CPU.count / 2 == 0) ? 1 : CPU.count / 2
171
+ config.max_threads = CPU.count * 2
172
+
173
+ config.interval = 30 # Interval between runs in loop mode
174
+ config.tick_timeout = 15 # Data collection timeout
175
+ config.render_timeout = 10 # Data rendering timeout
176
+ config.logger = Logger.new(STDERR) # Default logger
177
+ end
178
+ ```
179
+
180
+ ### Zabbix
181
+
182
+ Zabbix uses two stage collecting. First of all, it queries (discovers) the list of objects to be checked. Next, it would ask for exact values of specified metrics of an object one by one. Sometimes this means making a lot of requests to a monitored service. So many script writers use some kind of result caching to lower unnecessary work. Salus also writes a result cache file (`-c` flag). Cache TTL for a metric is a half of it's real TTL or 60 seconds if TTL is unspecified. Upon parameter request it is loaded from cache and if it's expired, whole cache is invalidated and recalculated using salus script.
183
+
184
+ Sample Salus script for collecting CPU usage ratio on Linux for Zabbix Agent is something like that:
185
+
186
+ ```ruby
187
+ require "salus/zabbix"
188
+
189
+ default ttl: 60
190
+ var state_file: "cpu.state.yml"
191
+ var zabbix_cache_file: "cpu.cache.yml"
192
+
193
+ discover "cpus" do |data|
194
+ stat = File.open("/proc/stat").read.split(/\n/).grep(/^cpu\d/)
195
+ stat.each do |l|
196
+ name, = l.split(/\s+/)
197
+ data << {"\#{CPUNAME}" => name}
198
+ end
199
+ end
200
+
201
+ group "cpu" do
202
+ stat = File.open("/proc/stat").read.split(/\n/).grep(/^cpu\d/)
203
+ stat.each do |l|
204
+ name, user, nice, csystem, idle, iowait, irq, softirq = l.split(/\s+/)
205
+
206
+ busy = user.to_i + nice.to_i + csystem.to_i + iowait.to_i + irq.to_i + softirq.to_i
207
+ total = busy + idle.to_i
208
+
209
+ counter "busy[#{name}]", value: busy, mute: true
210
+ counter "total[#{name}]", value: total, mute: true
211
+ gauge "usage[#{name}]" do
212
+ value("busy[#{name}]") / value("total[#{name}]") * 100
213
+ end
214
+ end
215
+ end
216
+ ```
217
+
218
+ You can run it using command `salus`.
219
+
220
+ ```bash
221
+ $ salus zabbix discover cpus -f zabbix.salus
222
+ {"data":[{"#{CPUNAME}":"cpu0"},{"#{CPUNAME}":"cpu1"},{"#{CPUNAME}":"cpu2"},{"#{CPUNAME}":"cpu3"},{"#{CPUNAME}":"cpu4"},{"#{CPUNAME}":"cpu5"},{"#{CPUNAME}":"cpu6"},{"#{CPUNAME}":"cpu7"}]}
223
+ ```
224
+
225
+ Later you can get a cpu usage ratio
226
+
227
+ ```bash
228
+ $ salus zabbix parameter cpu.usage[cpu5] -f zabbix.salus && sleep 31 && salus zabbix parameter cpu.usage[cpu5] -f zabbix.salus
229
+ 8.619550926524335
230
+ ```
231
+
232
+ **NOTE!** You won't get the result on the first run, because cpu usage on Linux needs to get two points in time to be calculated. `zabbix` subcommand uses caching of the results, so you have to wait for 30 seconds to get next result. But if you'll wait for more than TTL (60 seconds), you'll get empty result again.
233
+
234
+ New dependant items mode of Zabbix 3.4+ is also supported. Output is in JSON.
235
+
236
+ ```bash
237
+ $ salus zabbix bulk cpu -f zabbix.salus
238
+ {"usage[cpu0]":8.362702709145317,"usage[cpu1]":2.2395325673158424,"usage[cpu2]":3.570268951237728,"usage[cpu3]":4.641349636608478,"usage[cpu4]":3.7313418117547834,"usage[cpu5]":4.023359928822469,"usage[cpu6]":3.0834133549568365,"usage[cpu7]":4.47470699450407}
239
+ ```
240
+
241
+ ## Development
242
+
243
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
244
+
245
+ ## Special thanks
246
+ * meh for [ruby-thread](https://github.com/meh/ruby-thread)
247
+ * all folks of [concurrent](https://github.com/ruby-concurrency/concurrent-ruby) project
248
+ * all folks of [thor](https://github.com/erikhuda/thor) project
249
+
250
+ Salus uses portions of code (meh's thread pool and future implementation) and concepts from both project and thor for CLI implementation.
251
+
252
+ ## Contributing
253
+
254
+ Bug reports and pull requests are welcome on GitHub at https://github.com/divanikus/salus.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "salus"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/salus ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ require "salus"
3
+ require "salus/cli"
4
+
5
+ begin
6
+ Salus::CLI.start
7
+ rescue Exception => e
8
+ unless e.is_a? Interrupt
9
+ STDERR.puts "Error: " + e.message
10
+ exit(1)
11
+ end
12
+ end
data/lib/salus.rb ADDED
@@ -0,0 +1,183 @@
1
+ require "salus/version"
2
+ require "salus/logging"
3
+ require "salus/thread"
4
+ require "salus/group"
5
+ require "salus/configuration"
6
+ require "salus/renderer"
7
+
8
+ module Salus
9
+ extend Configuration
10
+
11
+ class << self
12
+ include Logging
13
+ @@_groups = {}
14
+ @@_renders= []
15
+ @@_opts = {}
16
+ @@_vars = {}
17
+ @@_lazy = []
18
+
19
+ def on_win?
20
+ @@_win ||= !(RUBY_PLATFORM =~ /bccwin|cygwin|djgpp|mingw|mswin|wince/i).nil?
21
+ end
22
+
23
+ def group(title, &block)
24
+ unless @@_groups.key?(title)
25
+ @@_groups[title] = Group.new(@@_opts, &block)
26
+ end
27
+ end
28
+
29
+ def groups
30
+ @@_groups
31
+ end
32
+ alias root groups
33
+
34
+ def default(opts)
35
+ return unless opts.is_a?(Hash)
36
+ opts.each do |k, v|
37
+ next if [:value, :timestamp].include?(k)
38
+ @@_opts[k] = v
39
+ end
40
+ end
41
+
42
+ def defaults
43
+ @@_opts
44
+ end
45
+
46
+ def var(arg, default=nil, &block)
47
+ if arg.is_a?(Hash)
48
+ arg.each {|k, v| @@_vars[k] = v}
49
+ elsif block_given?
50
+ @@_vars[arg.to_sym] = block
51
+ else
52
+ value = @@_vars.fetch(arg.to_sym, default)
53
+ # Dumb lazy loading
54
+ @@_vars[arg.to_sym] = if value.is_a?(Proc)
55
+ begin
56
+ value = value.call
57
+ rescue Exception => e
58
+ log DEBUG, e
59
+ value = default
60
+ end
61
+ end
62
+ value
63
+ end
64
+ end
65
+ alias let var
66
+
67
+ def vars
68
+ @@_vars
69
+ end
70
+
71
+ def render(obj=nil, &block)
72
+ if block_given?
73
+ @@_renders << BlockRenderer.new(&block)
74
+ else
75
+ unless obj.is_a? Salus::BaseRenderer
76
+ log ERROR, "#{obj.class} must be a subclass of Salus::BaseRenderer"
77
+ return
78
+ end
79
+ @@_renders << obj
80
+ end
81
+ end
82
+
83
+ def renders
84
+ @@_renders
85
+ end
86
+
87
+ def reset
88
+ @@_groups = {}
89
+ @@_renders = []
90
+ @@_opts = {}
91
+ @@_vars = {}
92
+ @@_lazy = []
93
+ if defined?(@@_pool) && @@_pool.is_a?(Salus::ThreadPool)
94
+ @@_pool.shutdown!
95
+ @@_pool = nil
96
+ end
97
+ end
98
+
99
+ def lazy(&block)
100
+ raise ArgumentError, "Block should be given" unless block_given?
101
+ @@_lazy << block
102
+ end
103
+
104
+ def lazy_eval
105
+ # Lazy eval blocks once
106
+ return if @@_lazy.empty?
107
+ @@_lazy.each { |block| instance_eval(&block) }
108
+ @@_lazy.clear
109
+ end
110
+
111
+ def load(file)
112
+ instance_eval(File.read(file), File.basename(file), 0) if File.exists?(file)
113
+ end
114
+
115
+ def load_state(&block)
116
+ data = block.call
117
+ return unless data
118
+ return if data.empty?
119
+ lazy_eval
120
+ data.each do |k, v|
121
+ @@_groups[k].load(v) if @@_groups.key?(k)
122
+ end
123
+ end
124
+
125
+ def save_state(&block)
126
+ data = {}
127
+ @@_groups.each { |k, v| data[k] = v.to_h }
128
+ block.call(data)
129
+ end
130
+
131
+ def tick
132
+ lazy_eval
133
+ return if @@_groups.empty?
134
+ pause = (Salus.interval - Salus.tick_timeout - Salus.render_timeout) / 2
135
+ pause = 1 if (pause <= 0)
136
+
137
+ latch = CountDownLatch.new(@@_groups.count)
138
+ @@_groups.each do |k, v|
139
+ pool.process do
140
+ begin
141
+ v.tick
142
+ latch.count_down
143
+ rescue Exception => e
144
+ log ERROR, e
145
+ latch.count_down
146
+ end
147
+ end.timeout_after(Salus.tick_timeout)
148
+ end
149
+ latch.wait(Salus.tick_timeout + pause)
150
+ log DEBUG, "Collection finished. Threads: #{pool.spawned} spawned, #{pool.waiting} waiting, #{Thread.list.count} total"
151
+
152
+ return if @@_renders.empty?
153
+ latch = CountDownLatch.new(@@_renders.count)
154
+ @@_renders.each do |v|
155
+ pool.process do
156
+ begin
157
+ v.render(root)
158
+ latch.count_down
159
+ rescue Exception => e
160
+ log ERROR, e
161
+ latch.count_down
162
+ end
163
+ end.timeout_after(Salus.render_timeout)
164
+ end
165
+ latch.wait(Salus.render_timeout + pause)
166
+ log DEBUG, "Rendering finished. Threads: #{pool.spawned} spawned, #{pool.waiting} waiting, #{Thread.list.count} total"
167
+ end
168
+
169
+ def run
170
+ loop do
171
+ pool.process do
172
+ tick
173
+ end
174
+ sleep Salus.interval
175
+ end
176
+ end
177
+
178
+ protected
179
+ def pool
180
+ @@_pool ||= ThreadPool.new(self.min_threads, self.max_threads).auto_trim!
181
+ end
182
+ end
183
+ end