prometheus_exporter 0.4.17 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +36 -0
- data/.rubocop.yml +2 -1
- data/CHANGELOG +23 -1
- data/README.md +248 -7
- data/bin/prometheus_exporter +28 -1
- data/lib/prometheus_exporter.rb +14 -0
- data/lib/prometheus_exporter/client.rb +31 -3
- data/lib/prometheus_exporter/instrumentation.rb +2 -0
- data/lib/prometheus_exporter/instrumentation/active_record.rb +2 -13
- data/lib/prometheus_exporter/instrumentation/process.rb +3 -12
- data/lib/prometheus_exporter/instrumentation/shoryuken.rb +31 -0
- data/lib/prometheus_exporter/instrumentation/sidekiq.rb +44 -3
- data/lib/prometheus_exporter/instrumentation/sidekiq_queue.rb +50 -0
- data/lib/prometheus_exporter/metric/base.rb +4 -0
- data/lib/prometheus_exporter/metric/counter.rb +4 -0
- data/lib/prometheus_exporter/metric/gauge.rb +4 -0
- data/lib/prometheus_exporter/metric/histogram.rb +6 -0
- data/lib/prometheus_exporter/metric/summary.rb +7 -0
- data/lib/prometheus_exporter/middleware.rb +13 -2
- data/lib/prometheus_exporter/server.rb +2 -0
- data/lib/prometheus_exporter/server/active_record_collector.rb +1 -0
- data/lib/prometheus_exporter/server/collector.rb +2 -0
- data/lib/prometheus_exporter/server/delayed_job_collector.rb +11 -0
- data/lib/prometheus_exporter/server/hutch_collector.rb +6 -0
- data/lib/prometheus_exporter/server/runner.rb +26 -27
- data/lib/prometheus_exporter/server/shoryuken_collector.rb +67 -0
- data/lib/prometheus_exporter/server/sidekiq_collector.rb +11 -2
- data/lib/prometheus_exporter/server/sidekiq_queue_collector.rb +46 -0
- data/lib/prometheus_exporter/server/web_collector.rb +5 -0
- data/lib/prometheus_exporter/server/web_server.rb +29 -16
- data/lib/prometheus_exporter/version.rb +1 -1
- data/prometheus_exporter.gemspec +16 -14
- metadata +17 -12
- data/.travis.yml +0 -12
data/bin/prometheus_exporter
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require 'optparse'
|
5
|
+
require 'json'
|
5
6
|
|
6
7
|
require_relative "./../lib/prometheus_exporter"
|
7
8
|
require_relative "./../lib/prometheus_exporter/server"
|
@@ -19,6 +20,12 @@ def run
|
|
19
20
|
"Port exporter should listen on (default: #{PrometheusExporter::DEFAULT_PORT})") do |o|
|
20
21
|
options[:port] = o.to_i
|
21
22
|
end
|
23
|
+
opt.on('-b',
|
24
|
+
'--bind STRING',
|
25
|
+
String,
|
26
|
+
"IP address exporter should listen on (default: #{PrometheusExporter::DEFAULT_BIND_ADDRESS})") do |o|
|
27
|
+
options[:bind] = o.to_s
|
28
|
+
end
|
22
29
|
opt.on('-t',
|
23
30
|
'--timeout INTEGER',
|
24
31
|
Integer,
|
@@ -28,6 +35,9 @@ def run
|
|
28
35
|
opt.on('--prefix METRIC_PREFIX', "Prefix to apply to all metrics (default: #{PrometheusExporter::DEFAULT_PREFIX})") do |o|
|
29
36
|
options[:prefix] = o.to_s
|
30
37
|
end
|
38
|
+
opt.on('--label METRIC_LABEL', "Label to apply to all metrics (default: #{PrometheusExporter::DEFAULT_LABEL})") do |o|
|
39
|
+
options[:label] = JSON.parse(o.to_s)
|
40
|
+
end
|
31
41
|
opt.on('-c', '--collector FILE', String, "(optional) Custom collector to run") do |o|
|
32
42
|
custom_collector_filename = o.to_s
|
33
43
|
end
|
@@ -37,6 +47,12 @@ def run
|
|
37
47
|
opt.on('-v', '--verbose') do |o|
|
38
48
|
options[:verbose] = true
|
39
49
|
end
|
50
|
+
opt.on('--auth FILE', String, "(optional) enable basic authentication using a htpasswd FILE") do |o|
|
51
|
+
options[:auth] = o
|
52
|
+
end
|
53
|
+
opt.on('--realm REALM', String, "(optional) Use REALM for basic authentication (default: \"#{PrometheusExporter::DEFAULT_REALM}\")") do |o|
|
54
|
+
options[:realm] = o
|
55
|
+
end
|
40
56
|
|
41
57
|
opt.on('--unicorn-listen-address ADDRESS', String, '(optional) Address where unicorn listens on (unix or TCP address)') do |o|
|
42
58
|
options[:unicorn_listen_address] = o
|
@@ -47,6 +63,17 @@ def run
|
|
47
63
|
end
|
48
64
|
end.parse!
|
49
65
|
|
66
|
+
if options.has_key?(:realm) && !options.has_key?(:auth)
|
67
|
+
STDERR.puts "[Warn] Providing REALM without AUTH has no effect"
|
68
|
+
end
|
69
|
+
|
70
|
+
if options.has_key?(:auth)
|
71
|
+
unless File.exist?(options[:auth]) && File.readable?(options[:auth])
|
72
|
+
STDERR.puts "[Error] The AUTH file either doesn't exist or we don't have access to it"
|
73
|
+
exit 1
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
50
77
|
if custom_collector_filename
|
51
78
|
eval File.read(custom_collector_filename), nil, File.expand_path(custom_collector_filename)
|
52
79
|
found = false
|
@@ -81,7 +108,7 @@ def run
|
|
81
108
|
|
82
109
|
runner = PrometheusExporter::Server::Runner.new(options)
|
83
110
|
|
84
|
-
puts "#{Time.now} Starting prometheus exporter on
|
111
|
+
puts "#{Time.now} Starting prometheus exporter on #{runner.bind}:#{runner.port}"
|
85
112
|
runner.start
|
86
113
|
sleep
|
87
114
|
end
|
data/lib/prometheus_exporter.rb
CHANGED
@@ -7,8 +7,11 @@ require "thread"
|
|
7
7
|
module PrometheusExporter
|
8
8
|
# per: https://github.com/prometheus/prometheus/wiki/Default-port-allocations
|
9
9
|
DEFAULT_PORT = 9394
|
10
|
+
DEFAULT_BIND_ADDRESS = 'localhost'
|
10
11
|
DEFAULT_PREFIX = 'ruby_'
|
12
|
+
DEFAULT_LABEL = {}
|
11
13
|
DEFAULT_TIMEOUT = 2
|
14
|
+
DEFAULT_REALM = 'Prometheus Exporter'
|
12
15
|
|
13
16
|
class OjCompat
|
14
17
|
def self.parse(obj)
|
@@ -19,6 +22,17 @@ module PrometheusExporter
|
|
19
22
|
end
|
20
23
|
end
|
21
24
|
|
25
|
+
def self.hostname
|
26
|
+
@hostname ||=
|
27
|
+
begin
|
28
|
+
require 'socket'
|
29
|
+
Socket.gethostname
|
30
|
+
rescue => e
|
31
|
+
STDERR.puts "Unable to lookup hostname #{e}"
|
32
|
+
"unknown-host"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
22
36
|
def self.detect_json_serializer(preferred)
|
23
37
|
if preferred.nil?
|
24
38
|
preferred = :oj if has_oj?
|
@@ -53,12 +53,21 @@ module PrometheusExporter
|
|
53
53
|
MAX_SOCKET_AGE = 25
|
54
54
|
MAX_QUEUE_SIZE = 10_000
|
55
55
|
|
56
|
-
def initialize(
|
56
|
+
def initialize(
|
57
|
+
host: ENV.fetch('PROMETHEUS_EXPORTER_HOST', 'localhost'),
|
58
|
+
port: ENV.fetch('PROMETHEUS_EXPORTER_PORT', PrometheusExporter::DEFAULT_PORT),
|
59
|
+
max_queue_size: nil,
|
60
|
+
thread_sleep: 0.5,
|
61
|
+
json_serializer: nil,
|
62
|
+
custom_labels: nil
|
63
|
+
)
|
57
64
|
@metrics = []
|
58
65
|
|
59
66
|
@queue = Queue.new
|
67
|
+
|
60
68
|
@socket = nil
|
61
69
|
@socket_started = nil
|
70
|
+
@socket_pid = nil
|
62
71
|
|
63
72
|
max_queue_size ||= MAX_QUEUE_SIZE
|
64
73
|
max_queue_size = max_queue_size.to_i
|
@@ -100,7 +109,16 @@ module PrometheusExporter
|
|
100
109
|
end
|
101
110
|
|
102
111
|
def send_json(obj)
|
103
|
-
payload =
|
112
|
+
payload =
|
113
|
+
if @custom_labels
|
114
|
+
if obj[:custom_labels]
|
115
|
+
obj.merge(custom_labels: @custom_labels.merge(obj[:custom_labels]))
|
116
|
+
else
|
117
|
+
obj.merge(custom_labels: @custom_labels)
|
118
|
+
end
|
119
|
+
else
|
120
|
+
obj
|
121
|
+
end
|
104
122
|
send(@json_serializer.dump(payload))
|
105
123
|
end
|
106
124
|
|
@@ -184,12 +202,20 @@ module PrometheusExporter
|
|
184
202
|
end
|
185
203
|
|
186
204
|
def close_socket_if_old!
|
187
|
-
if @socket && ((@socket_started + MAX_SOCKET_AGE) < Time.now.to_f)
|
205
|
+
if @socket_pid == Process.pid && @socket && @socket_started && ((@socket_started + MAX_SOCKET_AGE) < Time.now.to_f)
|
188
206
|
close_socket!
|
189
207
|
end
|
190
208
|
end
|
191
209
|
|
192
210
|
def ensure_socket!
|
211
|
+
# if process was forked socket may be owned by parent
|
212
|
+
# leave it alone and reset
|
213
|
+
if @socket_pid != Process.pid
|
214
|
+
@socket = nil
|
215
|
+
@socket_started = nil
|
216
|
+
@socket_pid = nil
|
217
|
+
end
|
218
|
+
|
193
219
|
close_socket_if_old!
|
194
220
|
if !@socket
|
195
221
|
@socket = TCPSocket.new @host, @port
|
@@ -200,12 +226,14 @@ module PrometheusExporter
|
|
200
226
|
@socket.write("Content-Type: application/octet-stream\r\n")
|
201
227
|
@socket.write("\r\n")
|
202
228
|
@socket_started = Time.now.to_f
|
229
|
+
@socket_pid = Process.pid
|
203
230
|
end
|
204
231
|
|
205
232
|
nil
|
206
233
|
rescue
|
207
234
|
@socket = nil
|
208
235
|
@socket_started = nil
|
236
|
+
@socket_pid = nil
|
209
237
|
raise
|
210
238
|
end
|
211
239
|
|
@@ -4,8 +4,10 @@ require_relative "client"
|
|
4
4
|
require_relative "instrumentation/process"
|
5
5
|
require_relative "instrumentation/method_profiler"
|
6
6
|
require_relative "instrumentation/sidekiq"
|
7
|
+
require_relative "instrumentation/sidekiq_queue"
|
7
8
|
require_relative "instrumentation/delayed_job"
|
8
9
|
require_relative "instrumentation/puma"
|
9
10
|
require_relative "instrumentation/hutch"
|
10
11
|
require_relative "instrumentation/unicorn"
|
11
12
|
require_relative "instrumentation/active_record"
|
13
|
+
require_relative "instrumentation/shoryuken"
|
@@ -51,17 +51,6 @@ module PrometheusExporter::Instrumentation
|
|
51
51
|
def initialize(metric_labels, config_labels)
|
52
52
|
@metric_labels = metric_labels
|
53
53
|
@config_labels = config_labels
|
54
|
-
@hostname = nil
|
55
|
-
end
|
56
|
-
|
57
|
-
def hostname
|
58
|
-
@hostname ||=
|
59
|
-
begin
|
60
|
-
`hostname`.strip
|
61
|
-
rescue => e
|
62
|
-
STDERR.puts "Unable to lookup hostname #{e}"
|
63
|
-
"unknown-host"
|
64
|
-
end
|
65
54
|
end
|
66
55
|
|
67
56
|
def collect
|
@@ -80,14 +69,14 @@ module PrometheusExporter::Instrumentation
|
|
80
69
|
|
81
70
|
labels_from_config = pool.spec.config
|
82
71
|
.select { |k, v| @config_labels.include? k }
|
83
|
-
.map { |k, v| [k.to_s.prepend("dbconfig_"), v] }
|
72
|
+
.map { |k, v| [k.to_s.dup.prepend("dbconfig_"), v] }
|
84
73
|
|
85
74
|
labels = @metric_labels.merge(pool_name: pool.spec.name).merge(Hash[labels_from_config])
|
86
75
|
|
87
76
|
metric = {
|
88
77
|
pid: pid,
|
89
78
|
type: "active_record",
|
90
|
-
hostname: hostname,
|
79
|
+
hostname: ::PrometheusExporter.hostname,
|
91
80
|
metric_labels: labels
|
92
81
|
}
|
93
82
|
metric.merge!(pool.stat)
|
@@ -3,6 +3,8 @@
|
|
3
3
|
# collects stats from currently running process
|
4
4
|
module PrometheusExporter::Instrumentation
|
5
5
|
class Process
|
6
|
+
@thread = nil if !defined?(@thread)
|
7
|
+
|
6
8
|
def self.start(client: nil, type: "ruby", frequency: 30, labels: nil)
|
7
9
|
|
8
10
|
metric_labels =
|
@@ -42,24 +44,13 @@ module PrometheusExporter::Instrumentation
|
|
42
44
|
|
43
45
|
def initialize(metric_labels)
|
44
46
|
@metric_labels = metric_labels
|
45
|
-
@hostname = nil
|
46
|
-
end
|
47
|
-
|
48
|
-
def hostname
|
49
|
-
@hostname ||=
|
50
|
-
begin
|
51
|
-
`hostname`.strip
|
52
|
-
rescue => e
|
53
|
-
STDERR.puts "Unable to lookup hostname #{e}"
|
54
|
-
"unknown-host"
|
55
|
-
end
|
56
47
|
end
|
57
48
|
|
58
49
|
def collect
|
59
50
|
metric = {}
|
60
51
|
metric[:type] = "process"
|
61
52
|
metric[:metric_labels] = @metric_labels
|
62
|
-
metric[:hostname] = hostname
|
53
|
+
metric[:hostname] = ::PrometheusExporter.hostname
|
63
54
|
collect_gc_stats(metric)
|
64
55
|
collect_v8_stats(metric)
|
65
56
|
collect_process_stats(metric)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PrometheusExporter::Instrumentation
|
4
|
+
class Shoryuken
|
5
|
+
|
6
|
+
def initialize(client: nil)
|
7
|
+
@client = client || PrometheusExporter::Client.default
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(worker, queue, msg, body)
|
11
|
+
success = false
|
12
|
+
start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
13
|
+
result = yield
|
14
|
+
success = true
|
15
|
+
result
|
16
|
+
rescue ::Shoryuken::Shutdown => e
|
17
|
+
shutdown = true
|
18
|
+
raise e
|
19
|
+
ensure
|
20
|
+
duration = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start
|
21
|
+
@client.send_json(
|
22
|
+
type: "shoryuken",
|
23
|
+
queue: queue,
|
24
|
+
name: worker.class.name,
|
25
|
+
success: success,
|
26
|
+
shutdown: shutdown,
|
27
|
+
duration: duration
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -1,6 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'yaml'
|
4
|
+
|
3
5
|
module PrometheusExporter::Instrumentation
|
6
|
+
JOB_WRAPPER_CLASS_NAME = 'ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper'
|
7
|
+
DELAYED_CLASS_NAMES = [
|
8
|
+
'Sidekiq::Extensions::DelayedClass',
|
9
|
+
'Sidekiq::Extensions::DelayedModel',
|
10
|
+
'Sidekiq::Extensions::DelayedMailer',
|
11
|
+
]
|
12
|
+
|
4
13
|
class Sidekiq
|
5
14
|
def self.death_handler
|
6
15
|
-> (job, ex) do
|
@@ -32,15 +41,47 @@ module PrometheusExporter::Instrumentation
|
|
32
41
|
raise e
|
33
42
|
ensure
|
34
43
|
duration = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start
|
35
|
-
class_name = worker.class.to_s == 'ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper' ?
|
36
|
-
msg['wrapped'] : worker.class.to_s
|
37
44
|
@client.send_json(
|
38
45
|
type: "sidekiq",
|
39
|
-
name:
|
46
|
+
name: get_name(worker, msg),
|
47
|
+
queue: queue,
|
40
48
|
success: success,
|
41
49
|
shutdown: shutdown,
|
42
50
|
duration: duration
|
43
51
|
)
|
44
52
|
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def get_name(worker, msg)
|
57
|
+
class_name = worker.class.to_s
|
58
|
+
if class_name == JOB_WRAPPER_CLASS_NAME
|
59
|
+
get_job_wrapper_name(msg)
|
60
|
+
elsif DELAYED_CLASS_NAMES.include?(class_name)
|
61
|
+
get_delayed_name(msg, class_name)
|
62
|
+
else
|
63
|
+
class_name
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_job_wrapper_name(msg)
|
68
|
+
msg['wrapped']
|
69
|
+
end
|
70
|
+
|
71
|
+
def get_delayed_name(msg, class_name)
|
72
|
+
# fallback to class_name since we're relying on the internal implementation
|
73
|
+
# of the delayed extensions
|
74
|
+
# https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/extensions/class_methods.rb
|
75
|
+
begin
|
76
|
+
(target, method_name, _args) = YAML.load(msg['args'].first)
|
77
|
+
if target.class == Class
|
78
|
+
"#{target.name}##{method_name}"
|
79
|
+
else
|
80
|
+
"#{target.class.name}##{method_name}"
|
81
|
+
end
|
82
|
+
rescue
|
83
|
+
class_name
|
84
|
+
end
|
85
|
+
end
|
45
86
|
end
|
46
87
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PrometheusExporter::Instrumentation
|
4
|
+
class SidekiqQueue
|
5
|
+
def self.start(client: nil, frequency: 30)
|
6
|
+
client ||= PrometheusExporter::Client.default
|
7
|
+
sidekiq_queue_collector = new
|
8
|
+
|
9
|
+
Thread.new do
|
10
|
+
loop do
|
11
|
+
begin
|
12
|
+
client.send_json(sidekiq_queue_collector.collect)
|
13
|
+
rescue StandardError => e
|
14
|
+
STDERR.puts("Prometheus Exporter Failed To Collect Sidekiq Queue metrics #{e}")
|
15
|
+
ensure
|
16
|
+
sleep frequency
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def collect
|
23
|
+
{
|
24
|
+
type: 'sidekiq_queue',
|
25
|
+
queues: collect_queue_stats
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def collect_queue_stats
|
30
|
+
hostname = Socket.gethostname
|
31
|
+
pid = ::Process.pid
|
32
|
+
ps = ::Sidekiq::ProcessSet.new
|
33
|
+
|
34
|
+
process = ps.find do |sp|
|
35
|
+
sp['hostname'] == hostname && sp['pid'] == pid
|
36
|
+
end
|
37
|
+
|
38
|
+
queues = process.nil? ? [] : process['queues']
|
39
|
+
|
40
|
+
::Sidekiq::Queue.all.map do |queue|
|
41
|
+
next unless queues.include? queue.name
|
42
|
+
{
|
43
|
+
backlog_total: queue.size,
|
44
|
+
latency_seconds: queue.latency.to_i,
|
45
|
+
labels: { queue: queue.name }
|
46
|
+
}
|
47
|
+
end.compact
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|