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.
data/lib/salus/cli.rb ADDED
@@ -0,0 +1,53 @@
1
+ require "thor"
2
+ require "yaml"
3
+ require "salus/cli/baseutils"
4
+ require "salus/cli/zabbix"
5
+
6
+ module Salus
7
+ class CLI < Thor
8
+ include BaseCliUtils
9
+ include Thor::Actions
10
+
11
+ register Salus::ZabbixCli, :zabbix, "zabbix", "Zabbix specific actions"
12
+
13
+ desc "once", "Run check once"
14
+ method_option :file, aliases: "-f", :type => :array, desc: "File(s) with metrics' definition"
15
+ method_option :state, aliases: "-s", :type => :string, desc: "State file location"
16
+ method_option :debug, aliases: "-d", :type => :boolean, :default => false
17
+ method_option :renderer, aliases: "-r", :type => :array, desc: "Append predefined renderers"
18
+ def once
19
+ Salus.logger.level = options[:debug] ? Logger::DEBUG : Logger::WARN
20
+ load_files(get_files(options))
21
+ state_file = get_state_file(options)
22
+ load_state(state_file)
23
+ append_renderers(options)
24
+ Salus.tick
25
+ save_state(state_file)
26
+ end
27
+
28
+ desc "loop", "Run check loop"
29
+ method_option :file, aliases: "-f", :type => :array, desc: "File(s) with metrics' definition"
30
+ method_option :debug, aliases: "-d", :type => :boolean, :default => false
31
+ method_option :renderer, aliases: "-r", :type => :array, desc: "Append predefined renderers"
32
+ def loop
33
+ Salus.logger.level = options[:debug] ? Logger::DEBUG : Logger::WARN
34
+ load_files(get_files(options))
35
+ append_renderers(options)
36
+ Salus.run
37
+ end
38
+
39
+ default_task :once
40
+
41
+ private
42
+ def append_renderers(options={})
43
+ renderers = options.fetch(:renderer, Salus.renders.empty? ? ["stdout"] : [])
44
+
45
+ BaseRenderer.descendants.each do |m|
46
+ sym = m.name.split('::').last.downcase.sub(/renderer$/, '')
47
+ if renderers.include?(sym)
48
+ Salus.render(m.new)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,92 @@
1
+ module Salus
2
+ module BaseCliUtils
3
+ include Logging
4
+ SALUS_STATE_FILE = "salus.state.yml"
5
+ SALUS_FILE = "Salusfile"
6
+ SALUS_GLOB = "*.salus"
7
+
8
+ def read_file(file)
9
+ ret = nil
10
+ File.open(file, "r") do |f|
11
+ f.flock(File::LOCK_SH)
12
+ ret = f.read
13
+ f.flock(File::LOCK_UN)
14
+ end
15
+ ret
16
+ end
17
+
18
+ def write_file(file, data)
19
+ ret = nil
20
+ File.open(file, File::RDWR|File::CREAT) do |f|
21
+ f.flock(File::LOCK_EX)
22
+ begin
23
+ f.rewind
24
+ ret = f.write(data)
25
+ f.flush
26
+ f.truncate(f.pos)
27
+ ensure
28
+ f.flock(File::LOCK_UN)
29
+ end
30
+ end
31
+ ret
32
+ end
33
+
34
+ def load_files(files)
35
+ raise "No metric definition files found" if files.empty?
36
+ files.each do |file|
37
+ begin
38
+ Salus.load(file)
39
+ rescue Exception => e
40
+ log ERROR, "Failed to load #{file}: " + e.message
41
+ end
42
+ end
43
+ end
44
+
45
+ def load_state(file)
46
+ return unless file
47
+ Salus.load_state do
48
+ begin
49
+ YAML.load(read_file(file)) if File.exists?(file)
50
+ rescue Exception => e
51
+ log ERROR, "Failed to load state #{file}: " + e.message
52
+ end
53
+ end
54
+ end
55
+
56
+ def save_state(file)
57
+ return unless file
58
+ Salus.save_state do |data|
59
+ begin
60
+ write_file(file, data.to_yaml)
61
+ rescue Exception => e
62
+ log ERROR, "Failed to save state #{file}: " + e.message
63
+ end
64
+ end
65
+ end
66
+
67
+ def get_state_file(options={})
68
+ options.fetch(:state,
69
+ Salus.var(:state_file,
70
+ File.join(Dir.pwd, SALUS_STATE_FILE)))
71
+ end
72
+
73
+ def get_files(options={})
74
+ if options.key?(:file)
75
+ ret = []
76
+ options[:file].each do |file|
77
+ next unless File.exists?(file)
78
+ if File.directory?(file)
79
+ ret += Dir.glob(File.join(file, SALUS_GLOB)).sort
80
+ else
81
+ ret.push(file)
82
+ end
83
+ end
84
+ ret
85
+ elsif File.exists?(SALUS_FILE)
86
+ [SALUS_FILE]
87
+ else
88
+ Dir.glob(SALUS_GLOB).sort
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,150 @@
1
+ require "salus/zabbix"
2
+
3
+ module Salus
4
+ class ZabbixCacheRenderer < BaseRenderer
5
+ ZABBIX_DEFAULT_TTL = 60
6
+ attr_reader :data
7
+
8
+ def render(data)
9
+ @data = {}
10
+ iterate(data) do |name, metric|
11
+ name = name.gsub(/\.\[/, '[')
12
+ value = metric.value
13
+ # Metric cache TTL is a half of real metric TTL
14
+ ttl = metric.ttl.nil? ? ZABBIX_DEFAULT_TTL : (metric.ttl / 2)
15
+ @data[name] = {timestamp: metric.timestamp, cache_ttl: ttl, value: value}
16
+ end
17
+ end
18
+ end
19
+
20
+ class ZabbixCli < Thor
21
+ include BaseCliUtils
22
+ include Thor::Actions
23
+ ZABBIX_CACHE_FILE = "zabbix.cache.yml"
24
+
25
+ desc "discover NAME", "Run discovery"
26
+ method_option :file, aliases: "-f", :type => :array, desc: "File(s) with metrics' definition"
27
+ method_option :debug, aliases: "-d", :type => :boolean, :default => false
28
+ def discover(name)
29
+ Salus.logger.level = options[:debug] ? Logger::DEBUG : Logger::WARN
30
+ load_files(get_files(options))
31
+ puts Salus.discovery(name)
32
+ end
33
+
34
+ desc "parameter NAME", "Get a requested parameter"
35
+ method_option :file, aliases: "-f", :type => :array, desc: "File(s) with metrics' definition"
36
+ method_option :state, aliases: "-s", :type => :string, desc: "State file location"
37
+ method_option :cache, aliases: "-c", :type => :string, desc: "Cache file location"
38
+ method_option :cache_ttl, aliases: "-t", :type => :numeric, desc: "Force metric cache ttl"
39
+ method_option :debug, aliases: "-d", :type => :boolean, :default => false
40
+ def parameter(name)
41
+ Salus.logger.level = options[:debug] ? Logger::DEBUG : Logger::WARN
42
+ load_files(get_files(options))
43
+
44
+ cache_file = options.fetch(:cache,
45
+ Salus.vars.fetch(:zabbix_cache_file,
46
+ File.join(Dir.pwd, ZABBIX_CACHE_FILE)))
47
+ cache = load_cache(cache_file)
48
+
49
+ if (cache.key?(name) && !expired?(cache[name], options))
50
+ STDOUT.puts cache[name][:value] unless cache[name][:value].nil?
51
+ return
52
+ end
53
+
54
+ state_file = get_state_file(options)
55
+ load_state(state_file)
56
+
57
+ render = ZabbixCacheRenderer.new
58
+ Salus.renders.clear
59
+ Salus.render(render)
60
+ Salus.tick
61
+ cache = render.data
62
+
63
+ if (cache.key?(name))
64
+ STDOUT.puts cache[name][:value] unless cache[name][:value].nil?
65
+ end
66
+
67
+ save_state(state_file)
68
+ save_cache(cache_file, cache)
69
+ raise "Unknown parameter #{name}" unless cache.key?(name)
70
+ end
71
+
72
+ desc "bulk GROUP", "Get a bunch of parameters under the GROUP group"
73
+ method_option :file, aliases: "-f", :type => :array, desc: "File(s) with metrics' definition"
74
+ method_option :state, aliases: "-s", :type => :string, desc: "State file location"
75
+ method_option :debug, aliases: "-d", :type => :boolean, :default => false
76
+ def bulk(group=nil)
77
+ Salus.logger.level = options[:debug] ? Logger::DEBUG : Logger::WARN
78
+ load_files(get_files(options))
79
+
80
+ cache_file = options.fetch(:cache,
81
+ Salus.var(:zabbix_cache_file,
82
+ File.join(Dir.pwd, ZABBIX_CACHE_FILE)))
83
+ cache = load_cache(cache_file)
84
+
85
+ re = group.nil? ? // : /^#{Regexp.escape(group)}\./
86
+ keys = cache.keys.grep(re)
87
+ if !keys.empty? && (keys.reduce(true) { |x, v| x &= !expired?(cache[v], options) })
88
+ result = {}
89
+ keys.each do |key|
90
+ name = key.sub(re, '')
91
+ name = name.gsub(/\.\[/, '[')
92
+
93
+ parts = name.split(/\./)
94
+ node = result
95
+ parts[0...-1].each do |part|
96
+ node[part] = {} unless node.key?(part)
97
+ node = node[part]
98
+ end
99
+ node[parts.last] = cache[key][:value]
100
+ end
101
+ STDOUT.puts result.to_json
102
+ return
103
+ end
104
+
105
+ state_file = get_state_file(options)
106
+ load_state(state_file)
107
+
108
+ Salus.renders.clear
109
+ Salus.render(ZabbixBulkRenderer.new(group: group))
110
+ render = ZabbixCacheRenderer.new
111
+ Salus.render(render)
112
+ Salus.tick
113
+ cache = render.data
114
+
115
+ save_state(state_file)
116
+ save_cache(cache_file, cache)
117
+ end
118
+
119
+ private
120
+ def expired?(metric, options={})
121
+ return true if metric.nil?
122
+ ttl = options.fetch(:cache_ttl, metric[:cache_ttl])
123
+ ttl ||= 0
124
+ (Time.now.to_f > metric[:timestamp] + ttl)
125
+ end
126
+
127
+ def load_cache(file)
128
+ return {} unless file
129
+ begin
130
+ if File.exists?(file)
131
+ YAML.load(read_file(file))
132
+ else
133
+ {}
134
+ end
135
+ rescue Exception => e
136
+ log ERROR, "Failed to load state #{file}: " + e.message
137
+ {}
138
+ end
139
+ end
140
+
141
+ def save_cache(file, data)
142
+ return unless file
143
+ begin
144
+ write_file(file, data.to_yaml)
145
+ rescue Exception => e
146
+ log ERROR, "Failed to save state #{file}: " + e.message
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,38 @@
1
+ module Salus
2
+ module Configuration
3
+ # An array of valid keys in the options hash when configuring Salus.
4
+ VALID_OPTIONS_KEYS = %i(min_threads max_threads interval tick_timeout render_timeout logger).freeze
5
+
6
+ # @private
7
+ attr_accessor(*VALID_OPTIONS_KEYS)
8
+
9
+ # Sets all configuration options to their default values
10
+ # when this module is extended.
11
+ def self.extended(base)
12
+ base.reset
13
+ end
14
+
15
+ # Convenience method to allow configuration options to be set in a block.
16
+ def configure
17
+ yield self
18
+ end
19
+
20
+ # Creates a hash of options and their values.
21
+ def options
22
+ VALID_OPTIONS_KEYS.inject({}) do |option, key|
23
+ option.merge!(key => send(key))
24
+ end
25
+ end
26
+
27
+ # Resets all configuration options to the defaults.
28
+ def reset
29
+ self.min_threads = CPU.count / 2
30
+ self.min_threads = 1 if self.min_threads == 0
31
+ self.max_threads = CPU.count * 2
32
+ self.interval = 30
33
+ self.tick_timeout = 15
34
+ self.render_timeout = 10
35
+ self.logger = Logger.new(STDERR)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,159 @@
1
+ require "forwardable"
2
+ require "salus/metric"
3
+ require "salus/metric/absolute"
4
+ require "salus/metric/counter"
5
+ require "salus/metric/derive"
6
+ require "salus/metric/gauge"
7
+ require "salus/metric/text"
8
+
9
+ module Salus
10
+ class Group
11
+ extend Forwardable
12
+ include Lockable
13
+
14
+ def_delegators :@_metrics, :[], :key?, :values_at, :fetch, :length, :delete, :empty?
15
+
16
+ def initialize(defaults={}, &block)
17
+ @_metrics = {}
18
+ @_groups = {}
19
+ @_cache = {}
20
+ @_proc = block
21
+ @_opts = defaults.clone
22
+ end
23
+
24
+ Metric.descendants.each do |m|
25
+ sym = m.name.split('::').last.downcase.to_sym
26
+ define_method(sym) do |title, args={}, &blk|
27
+ raise ArgumentError, "Metric needs a name!" if title.nil? or !title.is_a?(String)
28
+
29
+ unless @_metrics.key?(title)
30
+ @_metrics[title] = m.new(@_opts)
31
+ end
32
+
33
+ @_metrics[title].push(args, &blk)
34
+ end
35
+ end
36
+
37
+ def on_win?
38
+ Salus.on_win?
39
+ end
40
+
41
+ def var(arg, default=nil, &block)
42
+ Salus.var(arg, default, &block)
43
+ end
44
+
45
+ def default(opts)
46
+ return unless opts.is_a?(Hash)
47
+ opts.each do |k, v|
48
+ next if [:value, :timestamp].include?(k)
49
+ @_opts[k] = v
50
+ end
51
+ end
52
+
53
+ def group(title, &block)
54
+ synchronize do
55
+ unless @_groups.key?(title)
56
+ @_groups[title] = Group.new(@_opts, &block)
57
+ if @_cache.key?(title)
58
+ @_groups[title].load(@_cache[title])
59
+ @_cache.delete(title)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def groups
66
+ synchronize { @_groups }
67
+ end
68
+
69
+ def has_subgroups?
70
+ synchronize { !@_groups.empty? }
71
+ end
72
+
73
+ def value(title)
74
+ synchronize { @_metrics.key?(title) ? @_metrics[title].value : nil }
75
+ end
76
+
77
+ def keys(allow_mute=false)
78
+ synchronize do
79
+ if allow_mute
80
+ @_metrics.keys
81
+ else
82
+ @_metrics.keys.select { |x| !@_metrics[x].mute? }
83
+ end
84
+ end
85
+ end
86
+
87
+ def values(allow_mute=false)
88
+ synchronize do
89
+ if allow_mute
90
+ @_metrics.values
91
+ else
92
+ @_metrics.values.select { |x| !x.mute? }
93
+ end
94
+ end
95
+ end
96
+
97
+ def each(allow_mute=false, &block)
98
+ synchronize do
99
+ if allow_mute
100
+ @_metrics.each(&block)
101
+ else
102
+ @_metrics.select { |k, v| !v.mute? }.each(&block)
103
+ end
104
+ end
105
+ end
106
+
107
+ def load(data)
108
+ return unless data
109
+ return if data.empty?
110
+ synchronize do
111
+ if data.key?(:defaults)
112
+ @_opts = data[:defaults].clone
113
+ end
114
+ if data.key?(:metrics)
115
+ types = Metric.descendants.map{ |x| x.name.split("::").last }
116
+ data[:metrics].each do |k, v|
117
+ next unless v.key?(:type)
118
+ next unless types.include?(v[:type])
119
+ @_metrics[k] = Object.const_get("Salus::" + v[:type]).new(@_opts)
120
+ @_metrics[k].load(v)
121
+ end
122
+ end
123
+ if data.key?(:groups)
124
+ @_cache = data[:groups]
125
+ end
126
+ end
127
+ end
128
+
129
+ def save
130
+ to_h
131
+ end
132
+
133
+ def to_h
134
+ ret = {}
135
+ synchronize do
136
+ unless @_metrics.empty?
137
+ ret[:metrics] = {}
138
+ @_metrics.each { |k, v| ret[:metrics][k] = v.to_h }
139
+ end
140
+ unless @_groups.empty?
141
+ ret[:groups] = {}
142
+ @_groups.each { |k, v| ret[:groups][k] = v.to_h }
143
+ end
144
+ unless @_opts.empty?
145
+ ret[:defaults] = @_opts
146
+ end
147
+ ret
148
+ end
149
+ end
150
+
151
+ def tick
152
+ instance_eval(&@_proc)
153
+ @_groups.each do |k, v|
154
+ v.tick
155
+ end
156
+ @_cache.clear unless @_cache.empty?
157
+ end
158
+ end
159
+ end