prometheus_exporter 0.5.3 → 0.8.1

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +42 -0
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +5 -0
  5. data/Appraisals +10 -0
  6. data/CHANGELOG +28 -0
  7. data/README.md +138 -16
  8. data/bin/prometheus_exporter +29 -2
  9. data/gemfiles/.bundle/config +2 -0
  10. data/gemfiles/ar_60.gemfile +5 -0
  11. data/gemfiles/ar_61.gemfile +7 -0
  12. data/lib/prometheus_exporter.rb +1 -0
  13. data/lib/prometheus_exporter/client.rb +39 -8
  14. data/lib/prometheus_exporter/instrumentation.rb +1 -0
  15. data/lib/prometheus_exporter/instrumentation/active_record.rb +19 -12
  16. data/lib/prometheus_exporter/instrumentation/delayed_job.rb +3 -2
  17. data/lib/prometheus_exporter/instrumentation/method_profiler.rb +2 -1
  18. data/lib/prometheus_exporter/instrumentation/process.rb +1 -1
  19. data/lib/prometheus_exporter/instrumentation/puma.rb +17 -5
  20. data/lib/prometheus_exporter/instrumentation/resque.rb +40 -0
  21. data/lib/prometheus_exporter/instrumentation/sidekiq.rb +44 -3
  22. data/lib/prometheus_exporter/instrumentation/sidekiq_queue.rb +13 -2
  23. data/lib/prometheus_exporter/instrumentation/unicorn.rb +1 -1
  24. data/lib/prometheus_exporter/middleware.rb +40 -17
  25. data/lib/prometheus_exporter/server.rb +1 -0
  26. data/lib/prometheus_exporter/server/active_record_collector.rb +2 -1
  27. data/lib/prometheus_exporter/server/collector.rb +1 -0
  28. data/lib/prometheus_exporter/server/delayed_job_collector.rb +9 -8
  29. data/lib/prometheus_exporter/server/puma_collector.rb +9 -1
  30. data/lib/prometheus_exporter/server/resque_collector.rb +54 -0
  31. data/lib/prometheus_exporter/server/runner.rb +13 -3
  32. data/lib/prometheus_exporter/server/sidekiq_collector.rb +2 -2
  33. data/lib/prometheus_exporter/server/web_collector.rb +2 -5
  34. data/lib/prometheus_exporter/server/web_server.rb +33 -23
  35. data/lib/prometheus_exporter/version.rb +1 -1
  36. data/prometheus_exporter.gemspec +7 -3
  37. metadata +59 -11
  38. data/.travis.yml +0 -12
@@ -0,0 +1,5 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 6.1.0"
6
+
7
+ gemspec path: "../"
@@ -11,6 +11,7 @@ module PrometheusExporter
11
11
  DEFAULT_PREFIX = 'ruby_'
12
12
  DEFAULT_LABEL = {}
13
13
  DEFAULT_TIMEOUT = 2
14
+ DEFAULT_REALM = 'Prometheus Exporter'
14
15
 
15
16
  class OjCompat
16
17
  def self.parse(obj)
@@ -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.to_i <= 0
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 = @custom_labels.nil? ? obj : obj.merge(custom_labels: @custom_labels)
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
- STDERR.puts "Prometheus Exporter client is dropping message cause queue is full"
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
- STDERR.puts "Prometheus Exporter is dropping a message: #{e}"
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
- STDERR.puts "Prometheus Exporter, failed to send message #{e}"
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
 
@@ -11,3 +11,4 @@ require_relative "instrumentation/hutch"
11
11
  require_relative "instrumentation/unicorn"
12
12
  require_relative "instrumentation/active_record"
13
13
  require_relative "instrumentation/shoryuken"
14
+ require_relative "instrumentation/resque"
@@ -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
- # Not all rails versions support coonection pool stats
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
- STDERR.puts("ActiveRecord connection pool stats not supported in your rails version")
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
- STDERR.puts("Prometheus Exporter Failed To Collect Process Stats #{e}")
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
- STDERR.puts("Prometheus Exporter Failed To Collect Process Stats #{e}")
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
- STDERR.puts("Prometheus Exporter Failed To Collect Puma Stats #{e}")
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
- metric[:type] = "puma"
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: class_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
- STDERR.puts("Prometheus Exporter Failed To Collect Sidekiq Queue metrics #{e}")
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