prometheus_exporter 0.4.17 → 0.6.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.
- 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
|