prometheus_exporter 0.5.3 → 0.8.1
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 +42 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +5 -0
- data/Appraisals +10 -0
- data/CHANGELOG +28 -0
- data/README.md +138 -16
- data/bin/prometheus_exporter +29 -2
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/ar_60.gemfile +5 -0
- data/gemfiles/ar_61.gemfile +7 -0
- data/lib/prometheus_exporter.rb +1 -0
- data/lib/prometheus_exporter/client.rb +39 -8
- data/lib/prometheus_exporter/instrumentation.rb +1 -0
- data/lib/prometheus_exporter/instrumentation/active_record.rb +19 -12
- data/lib/prometheus_exporter/instrumentation/delayed_job.rb +3 -2
- data/lib/prometheus_exporter/instrumentation/method_profiler.rb +2 -1
- data/lib/prometheus_exporter/instrumentation/process.rb +1 -1
- data/lib/prometheus_exporter/instrumentation/puma.rb +17 -5
- data/lib/prometheus_exporter/instrumentation/resque.rb +40 -0
- data/lib/prometheus_exporter/instrumentation/sidekiq.rb +44 -3
- data/lib/prometheus_exporter/instrumentation/sidekiq_queue.rb +13 -2
- data/lib/prometheus_exporter/instrumentation/unicorn.rb +1 -1
- data/lib/prometheus_exporter/middleware.rb +40 -17
- data/lib/prometheus_exporter/server.rb +1 -0
- data/lib/prometheus_exporter/server/active_record_collector.rb +2 -1
- data/lib/prometheus_exporter/server/collector.rb +1 -0
- data/lib/prometheus_exporter/server/delayed_job_collector.rb +9 -8
- data/lib/prometheus_exporter/server/puma_collector.rb +9 -1
- data/lib/prometheus_exporter/server/resque_collector.rb +54 -0
- data/lib/prometheus_exporter/server/runner.rb +13 -3
- data/lib/prometheus_exporter/server/sidekiq_collector.rb +2 -2
- data/lib/prometheus_exporter/server/web_collector.rb +2 -5
- data/lib/prometheus_exporter/server/web_server.rb +33 -23
- data/lib/prometheus_exporter/version.rb +1 -1
- data/prometheus_exporter.gemspec +7 -3
- metadata +59 -11
- data/.travis.yml +0 -12
data/lib/prometheus_exporter.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'socket'
|
4
4
|
require 'thread'
|
5
|
+
require 'logger'
|
5
6
|
|
6
7
|
module PrometheusExporter
|
7
8
|
class Client
|
@@ -53,24 +54,32 @@ module PrometheusExporter
|
|
53
54
|
MAX_SOCKET_AGE = 25
|
54
55
|
MAX_QUEUE_SIZE = 10_000
|
55
56
|
|
57
|
+
attr_reader :logger
|
58
|
+
|
56
59
|
def initialize(
|
57
60
|
host: ENV.fetch('PROMETHEUS_EXPORTER_HOST', 'localhost'),
|
58
61
|
port: ENV.fetch('PROMETHEUS_EXPORTER_PORT', PrometheusExporter::DEFAULT_PORT),
|
59
62
|
max_queue_size: nil,
|
60
63
|
thread_sleep: 0.5,
|
61
64
|
json_serializer: nil,
|
62
|
-
custom_labels: nil
|
65
|
+
custom_labels: nil,
|
66
|
+
logger: Logger.new(STDERR),
|
67
|
+
log_level: Logger::WARN
|
63
68
|
)
|
69
|
+
@logger = logger
|
70
|
+
@logger.level = log_level
|
64
71
|
@metrics = []
|
65
72
|
|
66
73
|
@queue = Queue.new
|
74
|
+
|
67
75
|
@socket = nil
|
68
76
|
@socket_started = nil
|
77
|
+
@socket_pid = nil
|
69
78
|
|
70
79
|
max_queue_size ||= MAX_QUEUE_SIZE
|
71
80
|
max_queue_size = max_queue_size.to_i
|
72
81
|
|
73
|
-
if max_queue_size
|
82
|
+
if max_queue_size <= 0
|
74
83
|
raise ArgumentError, "max_queue_size must be larger than 0"
|
75
84
|
end
|
76
85
|
|
@@ -107,14 +116,23 @@ module PrometheusExporter
|
|
107
116
|
end
|
108
117
|
|
109
118
|
def send_json(obj)
|
110
|
-
payload =
|
119
|
+
payload =
|
120
|
+
if @custom_labels
|
121
|
+
if obj[:custom_labels]
|
122
|
+
obj.merge(custom_labels: @custom_labels.merge(obj[:custom_labels]))
|
123
|
+
else
|
124
|
+
obj.merge(custom_labels: @custom_labels)
|
125
|
+
end
|
126
|
+
else
|
127
|
+
obj
|
128
|
+
end
|
111
129
|
send(@json_serializer.dump(payload))
|
112
130
|
end
|
113
131
|
|
114
132
|
def send(str)
|
115
133
|
@queue << str
|
116
134
|
if @queue.length > @max_queue_size
|
117
|
-
|
135
|
+
logger.warn "Prometheus Exporter client is dropping message cause queue is full"
|
118
136
|
@queue.pop
|
119
137
|
end
|
120
138
|
|
@@ -132,7 +150,7 @@ module PrometheusExporter
|
|
132
150
|
@socket.write(message)
|
133
151
|
@socket.write("\r\n")
|
134
152
|
rescue => e
|
135
|
-
|
153
|
+
logger.warn "Prometheus Exporter is dropping a message: #{e}"
|
136
154
|
@socket = nil
|
137
155
|
raise
|
138
156
|
end
|
@@ -157,7 +175,7 @@ module PrometheusExporter
|
|
157
175
|
close_socket_if_old!
|
158
176
|
process_queue
|
159
177
|
rescue => e
|
160
|
-
|
178
|
+
logger.error "Prometheus Exporter, failed to send message #{e}"
|
161
179
|
end
|
162
180
|
|
163
181
|
def ensure_worker_thread!
|
@@ -173,11 +191,14 @@ module PrometheusExporter
|
|
173
191
|
end
|
174
192
|
end
|
175
193
|
end
|
194
|
+
rescue ThreadError => e
|
195
|
+
raise unless e.message =~ /can't alloc thread/
|
196
|
+
logger.error "Prometheus Exporter, failed to send message ThreadError #{e}"
|
176
197
|
end
|
177
198
|
|
178
199
|
def close_socket!
|
179
200
|
begin
|
180
|
-
if @socket
|
201
|
+
if @socket && !@socket.closed?
|
181
202
|
@socket.write("0\r\n")
|
182
203
|
@socket.write("\r\n")
|
183
204
|
@socket.flush
|
@@ -191,12 +212,20 @@ module PrometheusExporter
|
|
191
212
|
end
|
192
213
|
|
193
214
|
def close_socket_if_old!
|
194
|
-
if @socket && ((@socket_started + MAX_SOCKET_AGE) < Time.now.to_f)
|
215
|
+
if @socket_pid == Process.pid && @socket && @socket_started && ((@socket_started + MAX_SOCKET_AGE) < Time.now.to_f)
|
195
216
|
close_socket!
|
196
217
|
end
|
197
218
|
end
|
198
219
|
|
199
220
|
def ensure_socket!
|
221
|
+
# if process was forked socket may be owned by parent
|
222
|
+
# leave it alone and reset
|
223
|
+
if @socket_pid != Process.pid
|
224
|
+
@socket = nil
|
225
|
+
@socket_started = nil
|
226
|
+
@socket_pid = nil
|
227
|
+
end
|
228
|
+
|
200
229
|
close_socket_if_old!
|
201
230
|
if !@socket
|
202
231
|
@socket = TCPSocket.new @host, @port
|
@@ -207,12 +236,14 @@ module PrometheusExporter
|
|
207
236
|
@socket.write("Content-Type: application/octet-stream\r\n")
|
208
237
|
@socket.write("\r\n")
|
209
238
|
@socket_started = Time.now.to_f
|
239
|
+
@socket_pid = Process.pid
|
210
240
|
end
|
211
241
|
|
212
242
|
nil
|
213
243
|
rescue
|
214
244
|
@socket = nil
|
215
245
|
@socket_started = nil
|
246
|
+
@socket_pid = nil
|
216
247
|
raise
|
217
248
|
end
|
218
249
|
|
@@ -7,9 +7,11 @@ module PrometheusExporter::Instrumentation
|
|
7
7
|
|
8
8
|
def self.start(client: nil, frequency: 30, custom_labels: {}, config_labels: [])
|
9
9
|
|
10
|
-
|
10
|
+
client ||= PrometheusExporter::Client.default
|
11
|
+
|
12
|
+
# Not all rails versions support connection pool stats
|
11
13
|
unless ::ActiveRecord::Base.connection_pool.respond_to?(:stat)
|
12
|
-
|
14
|
+
client.logger.error("ActiveRecord connection pool stats not supported in your rails version")
|
13
15
|
return
|
14
16
|
end
|
15
17
|
|
@@ -18,8 +20,6 @@ module PrometheusExporter::Instrumentation
|
|
18
20
|
|
19
21
|
active_record_collector = new(custom_labels, config_labels)
|
20
22
|
|
21
|
-
client ||= PrometheusExporter::Client.default
|
22
|
-
|
23
23
|
stop if @thread
|
24
24
|
|
25
25
|
@thread = Thread.new do
|
@@ -28,7 +28,7 @@ module PrometheusExporter::Instrumentation
|
|
28
28
|
metrics = active_record_collector.collect
|
29
29
|
metrics.each { |metric| client.send_json metric }
|
30
30
|
rescue => e
|
31
|
-
|
31
|
+
client.logger.error("Prometheus Exporter Failed To Collect Process Stats #{e}")
|
32
32
|
ensure
|
33
33
|
sleep frequency
|
34
34
|
end
|
@@ -67,21 +67,28 @@ module PrometheusExporter::Instrumentation
|
|
67
67
|
ObjectSpace.each_object(::ActiveRecord::ConnectionAdapters::ConnectionPool) do |pool|
|
68
68
|
next if pool.connections.nil?
|
69
69
|
|
70
|
-
labels_from_config = pool.spec.config
|
71
|
-
.select { |k, v| @config_labels.include? k }
|
72
|
-
.map { |k, v| [k.to_s.prepend("dbconfig_"), v] }
|
73
|
-
|
74
|
-
labels = @metric_labels.merge(pool_name: pool.spec.name).merge(Hash[labels_from_config])
|
75
|
-
|
76
70
|
metric = {
|
77
71
|
pid: pid,
|
78
72
|
type: "active_record",
|
79
73
|
hostname: ::PrometheusExporter.hostname,
|
80
|
-
metric_labels: labels
|
74
|
+
metric_labels: labels(pool)
|
81
75
|
}
|
82
76
|
metric.merge!(pool.stat)
|
83
77
|
metrics << metric
|
84
78
|
end
|
85
79
|
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def labels(pool)
|
84
|
+
if ::ActiveRecord.version < Gem::Version.new("6.1.0.rc1")
|
85
|
+
@metric_labels.merge(pool_name: pool.spec.name).merge(pool.spec.config
|
86
|
+
.select { |k, v| @config_labels.include? k }
|
87
|
+
.map { |k, v| [k.to_s.dup.prepend("dbconfig_"), v] }.to_h)
|
88
|
+
else
|
89
|
+
@metric_labels.merge(pool_name: pool.db_config.name).merge(
|
90
|
+
@config_labels.each_with_object({}) { |l, acc| acc["dbconfig_#{l}"] = pool.db_config.public_send(l) })
|
91
|
+
end
|
92
|
+
end
|
86
93
|
end
|
87
94
|
end
|
@@ -13,8 +13,8 @@ module PrometheusExporter::Instrumentation
|
|
13
13
|
callbacks do |lifecycle|
|
14
14
|
lifecycle.around(:invoke_job) do |job, *args, &block|
|
15
15
|
max_attempts = Delayed::Worker.max_attempts
|
16
|
-
enqueued_count = Delayed::Job.count
|
17
|
-
pending_count = Delayed::Job.where(attempts: 0, locked_at: nil).count
|
16
|
+
enqueued_count = Delayed::Job.where(queue: job.queue).count
|
17
|
+
pending_count = Delayed::Job.where(attempts: 0, locked_at: nil, queue: job.queue).count
|
18
18
|
instrumenter.call(job, max_attempts, enqueued_count, pending_count, *args, &block)
|
19
19
|
end
|
20
20
|
end
|
@@ -41,6 +41,7 @@ module PrometheusExporter::Instrumentation
|
|
41
41
|
@client.send_json(
|
42
42
|
type: "delayed_job",
|
43
43
|
name: job.handler.to_s.match(JOB_CLASS_REGEXP).to_a[1].to_s,
|
44
|
+
queue_name: job.queue,
|
44
45
|
success: success,
|
45
46
|
duration: duration,
|
46
47
|
attempts: attempts,
|
@@ -5,6 +5,7 @@ module PrometheusExporter::Instrumentation; end
|
|
5
5
|
|
6
6
|
class PrometheusExporter::Instrumentation::MethodProfiler
|
7
7
|
def self.patch(klass, methods, name)
|
8
|
+
patch_source_line = __LINE__ + 3
|
8
9
|
patches = methods.map do |method_name|
|
9
10
|
<<~RUBY
|
10
11
|
unless defined?(#{method_name}__mp_unpatched)
|
@@ -26,7 +27,7 @@ class PrometheusExporter::Instrumentation::MethodProfiler
|
|
26
27
|
RUBY
|
27
28
|
end.join("\n")
|
28
29
|
|
29
|
-
klass.class_eval patches
|
30
|
+
klass.class_eval patches, __FILE__, patch_source_line
|
30
31
|
end
|
31
32
|
|
32
33
|
def self.transfer
|
@@ -27,7 +27,7 @@ module PrometheusExporter::Instrumentation
|
|
27
27
|
metric = process_collector.collect
|
28
28
|
client.send_json metric
|
29
29
|
rescue => e
|
30
|
-
|
30
|
+
client.logger.error("Prometheus Exporter Failed To Collect Process Stats #{e}")
|
31
31
|
ensure
|
32
32
|
sleep frequency
|
33
33
|
end
|
@@ -5,8 +5,8 @@ require "json"
|
|
5
5
|
# collects stats from puma
|
6
6
|
module PrometheusExporter::Instrumentation
|
7
7
|
class Puma
|
8
|
-
def self.start(client: nil, frequency: 30)
|
9
|
-
puma_collector = new
|
8
|
+
def self.start(client: nil, frequency: 30, labels: {})
|
9
|
+
puma_collector = new(labels)
|
10
10
|
client ||= PrometheusExporter::Client.default
|
11
11
|
Thread.new do
|
12
12
|
while true
|
@@ -14,7 +14,7 @@ module PrometheusExporter::Instrumentation
|
|
14
14
|
metric = puma_collector.collect
|
15
15
|
client.send_json metric
|
16
16
|
rescue => e
|
17
|
-
|
17
|
+
client.logger.error("Prometheus Exporter Failed To Collect Puma Stats #{e}")
|
18
18
|
ensure
|
19
19
|
sleep frequency
|
20
20
|
end
|
@@ -22,13 +22,25 @@ module PrometheusExporter::Instrumentation
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
+
def initialize(metric_labels = {})
|
26
|
+
@metric_labels = metric_labels
|
27
|
+
end
|
28
|
+
|
25
29
|
def collect
|
26
|
-
metric = {
|
27
|
-
|
30
|
+
metric = {
|
31
|
+
pid: pid,
|
32
|
+
type: "puma",
|
33
|
+
hostname: ::PrometheusExporter.hostname,
|
34
|
+
metric_labels: @metric_labels
|
35
|
+
}
|
28
36
|
collect_puma_stats(metric)
|
29
37
|
metric
|
30
38
|
end
|
31
39
|
|
40
|
+
def pid
|
41
|
+
@pid = ::Process.pid
|
42
|
+
end
|
43
|
+
|
32
44
|
def collect_puma_stats(metric)
|
33
45
|
stats = JSON.parse(::Puma.stats)
|
34
46
|
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# collects stats from resque
|
4
|
+
module PrometheusExporter::Instrumentation
|
5
|
+
class Resque
|
6
|
+
def self.start(client: nil, frequency: 30)
|
7
|
+
resque_collector = new
|
8
|
+
client ||= PrometheusExporter::Client.default
|
9
|
+
Thread.new do
|
10
|
+
while true
|
11
|
+
begin
|
12
|
+
client.send_json(resque_collector.collect)
|
13
|
+
rescue => e
|
14
|
+
client.logger.error("Prometheus Exporter Failed To Collect Resque Stats #{e}")
|
15
|
+
ensure
|
16
|
+
sleep frequency
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def collect
|
23
|
+
metric = {}
|
24
|
+
metric[:type] = "resque"
|
25
|
+
collect_resque_stats(metric)
|
26
|
+
metric
|
27
|
+
end
|
28
|
+
|
29
|
+
def collect_resque_stats(metric)
|
30
|
+
info = ::Resque.info
|
31
|
+
|
32
|
+
metric[:processed_jobs_total] = info[:processed]
|
33
|
+
metric[:failed_jobs_total] = info[:failed]
|
34
|
+
metric[:pending_jobs_total] = info[:pending]
|
35
|
+
metric[:queues_total] = info[:queues]
|
36
|
+
metric[:worker_total] = info[:workers]
|
37
|
+
metric[:working_total] = info[:working]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
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
|
@@ -11,7 +11,7 @@ module PrometheusExporter::Instrumentation
|
|
11
11
|
begin
|
12
12
|
client.send_json(sidekiq_queue_collector.collect)
|
13
13
|
rescue StandardError => e
|
14
|
-
|
14
|
+
client.logger.error("Prometheus Exporter Failed To Collect Sidekiq Queue metrics #{e}")
|
15
15
|
ensure
|
16
16
|
sleep frequency
|
17
17
|
end
|
@@ -27,13 +27,24 @@ module PrometheusExporter::Instrumentation
|
|
27
27
|
end
|
28
28
|
|
29
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
|
+
|
30
40
|
::Sidekiq::Queue.all.map do |queue|
|
41
|
+
next unless queues.include? queue.name
|
31
42
|
{
|
32
43
|
backlog_total: queue.size,
|
33
44
|
latency_seconds: queue.latency.to_i,
|
34
45
|
labels: { queue: queue.name }
|
35
46
|
}
|
36
|
-
end
|
47
|
+
end.compact
|
37
48
|
end
|
38
49
|
end
|
39
50
|
end
|