sensu-plugins-prometheus-checks 2.3.0

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,9 @@
1
+ module Sensu
2
+ module Plugins
3
+ module Prometheus
4
+ module Checks
5
+ VERSION = '2.3.0'.freeze
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,62 @@
1
+ require 'json'
2
+ require 'net/http'
3
+
4
+ require 'sensu/plugins/utils/log'
5
+
6
+ module Sensu
7
+ module Plugins
8
+ module Prometheus
9
+ # Prometheus http-client, able to query and validate results.
10
+ class Client
11
+ include Sensu::Plugins::Utils::Log
12
+
13
+ # Instantiates the http-client with `prometheus_endpoint`.
14
+ def initialize
15
+ host, port = prometheus_endpoint.split(':')
16
+ log.info("Prometheus at '#{host}':'#{port}'")
17
+ @client = Net::HTTP.new(host, port)
18
+ @client.read_timeout = 3
19
+ @client.open_timeout = 3
20
+ end
21
+
22
+ # Execute query on Prometheus and validate payload. When successful it
23
+ # will return payload inner `result`, otherwise nil.
24
+ def query(prometheus_query)
25
+ log.debug("Prometheus Query: '#{prometheus_query}'")
26
+ prometheus_query = CGI.escape(prometheus_query)
27
+
28
+ begin
29
+ get_request = Net::HTTP::Get.new("/api/v1/query?query=#{prometheus_query}")
30
+ response_body = @client.request(get_request).body
31
+ rescue SystemCallError => e
32
+ log.error("Communication error with Prometheus: '#{e}'")
33
+ raise "Can't send query to Prometheus!"
34
+ end
35
+
36
+ payload = JSON.parse(response_body)
37
+ if !payload.key?('data') || !payload['data'].key?('result')
38
+ log.error("Can't find 'data' and/or 'result' keys on query result!")
39
+ return nil
40
+ end
41
+ payload['data']['result']
42
+ end
43
+
44
+ # String placeholders to calculate percentage free.
45
+ def percent_query_free(total, available)
46
+ "100-((#{available}/#{total})*100)"
47
+ end
48
+
49
+ private
50
+
51
+ # Reads `PROMETHEUS_ENDPOINT` from environment or use default localhost,
52
+ # applies validation to make sure ':' is present.
53
+ def prometheus_endpoint
54
+ endpoint = ENV['PROMETHEUS_ENDPOINT'] || '127.0.0.1:9090'
55
+ raise "Invalid endpoint 'PROMETHEUS_ENDPOINT=" + endpoint + "'" \
56
+ unless endpoint.include?(':')
57
+ endpoint
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,205 @@
1
+ require 'sensu/plugins/utils/log'
2
+
3
+ module Sensu
4
+ module Plugins
5
+ module Prometheus
6
+ # Represents the queryies that will be fired against Prometheus to collect
7
+ # information about monitored resources. Ideally all public methods on
8
+ # this class will be available as a final check.
9
+ class Metrics
10
+ include Sensu::Plugins::Utils::Log
11
+
12
+ def initialize(prometheus_client)
13
+ @client = prometheus_client
14
+ end
15
+
16
+ # Execute query informed on check's configuration and makes no
17
+ # modifications on value.
18
+ def custom(cfg)
19
+ metrics = []
20
+ @client.query(cfg['query']).each do |result|
21
+ source = if result['metric']['instance'] =~ /^\d+/
22
+ result['metric']['app']
23
+ else
24
+ result['metric']['instance']
25
+ end
26
+
27
+ metrics << {
28
+ 'source' => source,
29
+ 'value' => result['value'][1]
30
+ }
31
+ end
32
+ metrics
33
+ end
34
+
35
+ # Query percentage of mountpoint total disk space size compared with
36
+ # avaiable.
37
+ def disk(cfg)
38
+ mountpoint = "mountpoint=\"#{cfg['mount']}\""
39
+ query = @client.percent_query_free(
40
+ "node_filesystem_size{#{mountpoint}}",
41
+ "node_filesystem_avail{#{mountpoint}}"
42
+ )
43
+ prepare_metrics('disk', @client.query(query))
44
+ end
45
+
46
+ # Query percentage of free space on file-systems, ignoring by default
47
+ # `tmpfs` or the regexp configured on check.
48
+ def disk_all(cfg)
49
+ ignored = cfg['ignore_fs'] || 'tmpfs'
50
+ ignore_fs = "fstype!~\"#{ignored}\""
51
+ query = @client.percent_query_free(
52
+ "node_filesystem_files{#{ignore_fs}}",
53
+ "node_filesystem_files_free{#{ignore_fs}}"
54
+ )
55
+ prepare_metrics('disk_all', @client.query(query))
56
+ end
57
+
58
+ # Queyr percentage of free inodes on check's configured mountpoint.
59
+ def inode(cfg)
60
+ mountpoint = "mountpoint=\"#{cfg['mount']}\""
61
+ query = @client.percent_query_free(
62
+ "node_filesystem_files{#{mountpoint}}",
63
+ "node_filesystem_files_free{#{mountpoint}}"
64
+ )
65
+ prepare_metrics('inode', @client.query(query))
66
+ end
67
+
68
+ # Compose query to predict disk usage on the last day.
69
+ def predict_disk_all(cfg)
70
+ days = cfg['days'].to_i
71
+ days_in_seconds = days.to_i * 86_400
72
+ filter = cfg['filter'] || {}
73
+ range_vector = cfg['sample_size'] || '24h'
74
+ query = format(
75
+ 'predict_linear(node_filesystem_avail%s[%s], %i) < 0',
76
+ filter,
77
+ range_vector,
78
+ days_in_seconds
79
+ )
80
+ prepare_metrics('predict_disk_all', @client.query(query))
81
+ end
82
+
83
+ # Service metrics will contain it's "state" as "value".
84
+ def service(cfg)
85
+ defaults = { 'state' => 'active' }
86
+ cfg = defaults.merge(cfg)
87
+ query = format(
88
+ "node_systemd_unit_state{name='%s',state='%s'}",
89
+ cfg['name'], cfg['state']
90
+ )
91
+ prepare_metrics('service', @client.query(query))
92
+ end
93
+
94
+ # Query the percentage free memory.
95
+ def memory(_)
96
+ query = @client.percent_query_free(
97
+ 'node_memory_MemTotal',
98
+ 'node_memory_MemAvailable'
99
+ )
100
+ prepare_metrics('memory', @client.query(query))
101
+ end
102
+
103
+ # Percentage free memory cluster wide.
104
+ def memory_per_cluster(cfg)
105
+ cluster = cfg['cluster']
106
+ query = @client.percent_query_free(
107
+ "sum(node_memory_MemTotal{job=\"#{cluster}\"})",
108
+ "sum(node_memory_MemAvailable{job=\"#{cluster}\"})"
109
+ )
110
+
111
+ metrics = []
112
+ source = cfg['source']
113
+ @client.query(query).each do |result|
114
+ value = result['value'][1].to_f.round(2)
115
+ log.debug("[memory_per_cluster] value: '#{value}', source: '#{source}'")
116
+ metrics << { 'source' => source, 'value' => value }
117
+ end
118
+ metrics
119
+ end
120
+
121
+ # Calculates the load of an entire cluster.
122
+ def load_per_cluster(cfg)
123
+ cluster = cfg['cluster']
124
+ query = format(
125
+ 'sum(node_load5{job="%s"})/count(node_cpu{mode="system",job="%s"})',
126
+ cluster,
127
+ cluster
128
+ )
129
+
130
+ result = @client.query(query).first
131
+ source = cfg['source']
132
+ value = result['value'][1].to_f.round(2)
133
+ log.debug(
134
+ "[load_per_cluster] value: '#{value}', source: '#{source}'"
135
+ )
136
+
137
+ [{ 'source' => source, 'value' => value }]
138
+ end
139
+
140
+ # Returns a single metric entry, with the sum of the total load on
141
+ # cluster divided by the total amount of CPUs.
142
+ def load_per_cluster_minus_n(cfg)
143
+ cluster = cfg['cluster']
144
+ minus_n = cfg['minus_n']
145
+ sum_load = "sum(node_load5{job=\"#{cluster}\"})"
146
+ total_cpus = "count(node_cpu{mode=\"system\",job=\"#{cluster}\"})"
147
+ total_nodes = "count(node_load5{job=\"#{cluster}\"})"
148
+
149
+ query = format(
150
+ '%s/(%s-(%s/%s)*%d)',
151
+ sum_load, total_cpus, total_cpus, total_nodes, minus_n
152
+ )
153
+ result = @client.query(query).first
154
+ value = result['value'][1].to_f.round(2)
155
+ source = cfg['source']
156
+ log.debug(
157
+ "[load_per_cluster_minus_n] value: '#{value}', source: '#{source}'"
158
+ )
159
+
160
+ [{ 'source' => source, 'value' => value }]
161
+ end
162
+
163
+ # Current load per CPU.
164
+ def load_per_cpu(_)
165
+ cpu_per_source = {}
166
+ @client.query(
167
+ '(count(node_cpu{mode="system"})by(instance))'
168
+ ).each do |result|
169
+ source = result['metric']['instance']
170
+ cpu_per_source[source] = result['value'][1]
171
+ end
172
+
173
+ metrics = []
174
+ @client.query('node_load5').each do |result|
175
+ source = result['metric']['instance']
176
+ value = result['value'][1].to_f.round(2)
177
+ load_on_cpu = value / cpu_per_source[source].to_f
178
+ log.debug(
179
+ "[load_per_cpu] value: '#{load_on_cpu}', source: '#{source}'"
180
+ )
181
+ metrics << {
182
+ 'source' => source,
183
+ 'value' => load_on_cpu
184
+ }
185
+ end
186
+ metrics
187
+ end
188
+
189
+ private
190
+
191
+ # Prepare metrics with integer valutes, the most common case in the class.
192
+ def prepare_metrics(metric_name, results)
193
+ metrics = []
194
+ results.each do |result|
195
+ source = result['metric']['instance']
196
+ value = result['value'][1].to_i
197
+ log.debug("[#{metric_name}] value: '#{value}', source: '#{source}'")
198
+ metrics << { 'source' => source, 'value' => value }
199
+ end
200
+ metrics
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,24 @@
1
+ require 'logger'
2
+
3
+ module Sensu
4
+ module Plugins
5
+ module Utils
6
+ # Helper class to handle application logging. To be included in other
7
+ # classes and referenced by `log`.
8
+ module Log
9
+ def log
10
+ Log.log
11
+ end
12
+
13
+ # Creates a new logger instance a single time and set the log level. The
14
+ # level is determined by 'PROM_DEBUG' environment variable, when set the
15
+ # logger will use `0` (DEEBUG) otherwise `1` (INFO).
16
+ def self.log
17
+ @log ||= Logger.new(STDOUT)
18
+ @log.level = 1 if @log && !ENV.key?('PROM_DEBUG')
19
+ @log
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'sensu/plugins/prometheus/checks/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'sensu-plugins-prometheus-checks'
10
+ spec.version = Sensu::Plugins::Prometheus::Checks::VERSION
11
+ spec.authors = ['Michael Russell', 'Otávio Fernandes']
12
+ spec.email = ['mrussell@schubergphilis.com', 'ofernandes@schubergphilis.com']
13
+
14
+ spec.summary = 'Sensu plugin for monitoring servers by querying Prometheus'
15
+ spec.description = 'Sensu plugin to compose complex Prometheus queries and ' \
16
+ 'execute result-set evaluation'
17
+ spec.homepage = 'https://github.com/schubergphilis/sensu-plugins-prometheus-checks'
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
20
+ f.match(%r{^(test|spec|features)/})
21
+ end
22
+ spec.bindir = 'bin'
23
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
24
+ spec.require_paths = ['lib']
25
+
26
+ spec.add_development_dependency 'bundler', '~> 1.14'
27
+ spec.add_development_dependency 'rake', '~> 12.0'
28
+ spec.add_development_dependency 'rspec', '~> 3.5'
29
+ spec.add_development_dependency 'rubocop', '~> 0.47'
30
+ spec.add_development_dependency 'simplecov', '~> 0.13'
31
+ spec.add_development_dependency 'codecov', '~> 0.1.10'
32
+ spec.add_development_dependency 'vcr', '~> 3.0'
33
+ spec.add_development_dependency 'webmock', '~> 2.3'
34
+ spec.add_development_dependency 'pry', '~> 0.10'
35
+ end
data/test.rb ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+
6
+ `docker network create check_prometheus`
7
+ `docker-compose stop`
8
+ `docker-compose rm -f`
9
+ `docker-compose build`
10
+ `docker-compose create`
11
+ `docker-compose start`
12
+
13
+ prom_endpoint = '127.0.0.1:19090'
14
+ ENV['PROMETHEUS_ENDPOINT'] = prom_endpoint
15
+
16
+ count = 0
17
+ until count == 10
18
+ host, port = prom_endpoint.split(':')
19
+ http = Net::HTTP.new(host, port)
20
+ http.read_timeout = 3
21
+ http.open_timeout = 3
22
+ request = Net::HTTP::Get.new('/api/v1/query?query=count(up)')
23
+ begin
24
+ value = JSON.load(http.request(request).body)['data']['result'][0]['value'][1]
25
+ rescue
26
+ puts "Prometheus not ready yet ... #{count}"
27
+ end
28
+ break if value == '3'
29
+ count += 1
30
+ sleep(3)
31
+ end
32
+
33
+ if count == 10
34
+ puts 'ERROR: Prometheus failed to start'
35
+ exit(1)
36
+ end
37
+
38
+ puts 'starting rspec'
39
+ puts `rspec -c`
40
+ exit_code = `echo $?`
41
+ exit(exit_code.to_i)
data/test.sh ADDED
@@ -0,0 +1,5 @@
1
+ #!/bin/bash -e
2
+
3
+ docker build -t saas/check_prometheus .
4
+ docker run --rm saas/check_prometheus rspec -c -f d
5
+ docker run --rm saas/check_prometheus rubocop
metadata ADDED
@@ -0,0 +1,199 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sensu-plugins-prometheus-checks
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael Russell
8
+ - Otávio Fernandes
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2017-03-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.14'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1.14'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rake
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '12.0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '12.0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rspec
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '3.5'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '3.5'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rubocop
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '0.47'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '0.47'
70
+ - !ruby/object:Gem::Dependency
71
+ name: simplecov
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '0.13'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '0.13'
84
+ - !ruby/object:Gem::Dependency
85
+ name: codecov
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: 0.1.10
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: 0.1.10
98
+ - !ruby/object:Gem::Dependency
99
+ name: vcr
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: '3.0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: '3.0'
112
+ - !ruby/object:Gem::Dependency
113
+ name: webmock
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - "~>"
117
+ - !ruby/object:Gem::Version
118
+ version: '2.3'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - "~>"
124
+ - !ruby/object:Gem::Version
125
+ version: '2.3'
126
+ - !ruby/object:Gem::Dependency
127
+ name: pry
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - "~>"
131
+ - !ruby/object:Gem::Version
132
+ version: '0.10'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - "~>"
138
+ - !ruby/object:Gem::Version
139
+ version: '0.10'
140
+ description: Sensu plugin to compose complex Prometheus queries and execute result-set
141
+ evaluation
142
+ email:
143
+ - mrussell@schubergphilis.com
144
+ - ofernandes@schubergphilis.com
145
+ executables:
146
+ - check_prometheus.rb
147
+ extensions: []
148
+ extra_rdoc_files: []
149
+ files:
150
+ - ".gitignore"
151
+ - ".rspec"
152
+ - ".rubocop.yml"
153
+ - ".ruby-version"
154
+ - ".simplecov"
155
+ - ".travis.yml"
156
+ - Dockerfile
157
+ - Gemfile
158
+ - Gemfile.lock
159
+ - LICENSE
160
+ - README.md
161
+ - Rakefile
162
+ - bin/check_prometheus.rb
163
+ - docker-compose.yml
164
+ - lib/sensu/plugins/events/dispatcher.rb
165
+ - lib/sensu/plugins/prometheus/checks.rb
166
+ - lib/sensu/plugins/prometheus/checks/namespace.rb
167
+ - lib/sensu/plugins/prometheus/checks/output.rb
168
+ - lib/sensu/plugins/prometheus/checks/runner.rb
169
+ - lib/sensu/plugins/prometheus/checks/version.rb
170
+ - lib/sensu/plugins/prometheus/client.rb
171
+ - lib/sensu/plugins/prometheus/metrics.rb
172
+ - lib/sensu/plugins/utils/log.rb
173
+ - sensu-plugins-prometheus-checks.gemspec
174
+ - test.rb
175
+ - test.sh
176
+ homepage: https://github.com/schubergphilis/sensu-plugins-prometheus-checks
177
+ licenses: []
178
+ metadata: {}
179
+ post_install_message:
180
+ rdoc_options: []
181
+ require_paths:
182
+ - lib
183
+ required_ruby_version: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ required_rubygems_version: !ruby/object:Gem::Requirement
189
+ requirements:
190
+ - - ">="
191
+ - !ruby/object:Gem::Version
192
+ version: '0'
193
+ requirements: []
194
+ rubyforge_project:
195
+ rubygems_version: 2.5.2
196
+ signing_key:
197
+ specification_version: 4
198
+ summary: Sensu plugin for monitoring servers by querying Prometheus
199
+ test_files: []