prometheus_exporter 0.7.0 → 2.1.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +82 -25
  3. data/Appraisals +7 -3
  4. data/CHANGELOG +104 -24
  5. data/Dockerfile +9 -0
  6. data/README.md +258 -51
  7. data/bin/prometheus_exporter +19 -6
  8. data/examples/custom_collector.rb +1 -1
  9. data/gemfiles/ar_70.gemfile +5 -0
  10. data/lib/prometheus_exporter/client.rb +48 -23
  11. data/lib/prometheus_exporter/instrumentation/active_record.rb +11 -29
  12. data/lib/prometheus_exporter/instrumentation/delayed_job.rb +5 -2
  13. data/lib/prometheus_exporter/instrumentation/good_job.rb +30 -0
  14. data/lib/prometheus_exporter/instrumentation/method_profiler.rb +63 -23
  15. data/lib/prometheus_exporter/instrumentation/periodic_stats.rb +62 -0
  16. data/lib/prometheus_exporter/instrumentation/process.rb +5 -21
  17. data/lib/prometheus_exporter/instrumentation/puma.rb +34 -27
  18. data/lib/prometheus_exporter/instrumentation/resque.rb +35 -0
  19. data/lib/prometheus_exporter/instrumentation/sidekiq.rb +53 -23
  20. data/lib/prometheus_exporter/instrumentation/sidekiq_process.rb +52 -0
  21. data/lib/prometheus_exporter/instrumentation/sidekiq_queue.rb +32 -24
  22. data/lib/prometheus_exporter/instrumentation/sidekiq_stats.rb +37 -0
  23. data/lib/prometheus_exporter/instrumentation/unicorn.rb +10 -15
  24. data/lib/prometheus_exporter/instrumentation.rb +5 -0
  25. data/lib/prometheus_exporter/metric/base.rb +12 -10
  26. data/lib/prometheus_exporter/metric/gauge.rb +4 -0
  27. data/lib/prometheus_exporter/metric/histogram.rb +15 -3
  28. data/lib/prometheus_exporter/middleware.rb +45 -19
  29. data/lib/prometheus_exporter/server/active_record_collector.rb +9 -12
  30. data/lib/prometheus_exporter/server/collector.rb +4 -0
  31. data/lib/prometheus_exporter/server/delayed_job_collector.rb +24 -18
  32. data/lib/prometheus_exporter/server/good_job_collector.rb +52 -0
  33. data/lib/prometheus_exporter/server/metrics_container.rb +66 -0
  34. data/lib/prometheus_exporter/server/process_collector.rb +8 -13
  35. data/lib/prometheus_exporter/server/puma_collector.rb +14 -12
  36. data/lib/prometheus_exporter/server/resque_collector.rb +50 -0
  37. data/lib/prometheus_exporter/server/runner.rb +14 -3
  38. data/lib/prometheus_exporter/server/sidekiq_collector.rb +1 -1
  39. data/lib/prometheus_exporter/server/sidekiq_process_collector.rb +43 -0
  40. data/lib/prometheus_exporter/server/sidekiq_queue_collector.rb +6 -7
  41. data/lib/prometheus_exporter/server/sidekiq_stats_collector.rb +48 -0
  42. data/lib/prometheus_exporter/server/type_collector.rb +2 -0
  43. data/lib/prometheus_exporter/server/unicorn_collector.rb +32 -33
  44. data/lib/prometheus_exporter/server/web_collector.rb +17 -17
  45. data/lib/prometheus_exporter/server/web_server.rb +72 -41
  46. data/lib/prometheus_exporter/server.rb +4 -0
  47. data/lib/prometheus_exporter/version.rb +1 -1
  48. data/lib/prometheus_exporter.rb +12 -13
  49. data/prometheus_exporter.gemspec +6 -6
  50. metadata +53 -14
@@ -1,32 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'yaml'
3
+ require "yaml"
4
4
 
5
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',
6
+ JOB_WRAPPER_CLASS_NAME =
7
+ "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
8
+ DELAYED_CLASS_NAMES = %w[
9
+ Sidekiq::Extensions::DelayedClass
10
+ Sidekiq::Extensions::DelayedModel
11
+ Sidekiq::Extensions::DelayedMailer
11
12
  ]
12
13
 
13
14
  class Sidekiq
14
15
  def self.death_handler
15
- -> (job, ex) do
16
+ ->(job, ex) do
16
17
  job_is_fire_and_forget = job["retry"] == false
17
18
 
19
+ worker_class = Object.const_get(job["class"])
20
+ worker_custom_labels = self.get_worker_custom_labels(worker_class, job)
21
+
18
22
  unless job_is_fire_and_forget
19
23
  PrometheusExporter::Client.default.send_json(
20
24
  type: "sidekiq",
21
- name: job["class"],
25
+ name: get_name(job["class"], job),
22
26
  dead: true,
27
+ custom_labels: worker_custom_labels
23
28
  )
24
29
  end
25
30
  end
26
31
  end
27
32
 
28
- def initialize(client: nil)
29
- @client = client || PrometheusExporter::Client.default
33
+ def self.get_worker_custom_labels(worker_class, msg)
34
+ return {} unless worker_class.respond_to?(:custom_labels)
35
+
36
+ # TODO remove when version 3.0.0 is released
37
+ method_arity = worker_class.method(:custom_labels).arity
38
+
39
+ if method_arity > 0
40
+ worker_class.custom_labels(msg)
41
+ else
42
+ worker_class.custom_labels
43
+ end
44
+ end
45
+
46
+ def initialize(options = { client: nil })
47
+ @client =
48
+ options.fetch(:client, nil) || PrometheusExporter::Client.default
30
49
  end
31
50
 
32
51
  def call(worker, msg, queue)
@@ -43,18 +62,18 @@ module PrometheusExporter::Instrumentation
43
62
  duration = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start
44
63
  @client.send_json(
45
64
  type: "sidekiq",
46
- name: get_name(worker, msg),
65
+ name: self.class.get_name(worker.class.to_s, msg),
47
66
  queue: queue,
48
67
  success: success,
49
68
  shutdown: shutdown,
50
- duration: duration
69
+ duration: duration,
70
+ custom_labels: self.class.get_worker_custom_labels(worker.class, msg)
51
71
  )
52
72
  end
53
73
 
54
74
  private
55
75
 
56
- def get_name(worker, msg)
57
- class_name = worker.class.to_s
76
+ def self.get_name(class_name, msg)
58
77
  if class_name == JOB_WRAPPER_CLASS_NAME
59
78
  get_job_wrapper_name(msg)
60
79
  elsif DELAYED_CLASS_NAMES.include?(class_name)
@@ -64,24 +83,35 @@ module PrometheusExporter::Instrumentation
64
83
  end
65
84
  end
66
85
 
67
- def get_job_wrapper_name(msg)
68
- msg['wrapped']
86
+ def self.get_job_wrapper_name(msg)
87
+ msg["wrapped"]
69
88
  end
70
89
 
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
90
+ def self.get_delayed_name(msg, class_name)
75
91
  begin
76
- (target, method_name, _args) = YAML.load(msg['args'].first)
92
+ # fallback to class_name since we're relying on the internal implementation
93
+ # of the delayed extensions
94
+ # https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/extensions/class_methods.rb
95
+ target, method_name, _args = YAML.load(msg["args"].first)
77
96
  if target.class == Class
78
97
  "#{target.name}##{method_name}"
79
98
  else
80
99
  "#{target.class.name}##{method_name}"
81
100
  end
82
- rescue
83
- class_name
101
+ rescue Psych::DisallowedClass, ArgumentError
102
+ parsed = Psych.parse(msg["args"].first)
103
+ children = parsed.root.children
104
+ target = (children[0].value || children[0].tag).sub("!", "")
105
+ method_name = (children[1].value || children[1].tag).sub(":", "")
106
+
107
+ if target && method_name
108
+ "#{target}##{method_name}"
109
+ else
110
+ class_name
111
+ end
84
112
  end
113
+ rescue StandardError
114
+ class_name
85
115
  end
86
116
  end
87
117
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrometheusExporter::Instrumentation
4
+ class SidekiqProcess < PeriodicStats
5
+ def self.start(client: nil, frequency: 30)
6
+ client ||= PrometheusExporter::Client.default
7
+ sidekiq_process_collector = new
8
+
9
+ worker_loop do
10
+ client.send_json(sidekiq_process_collector.collect)
11
+ end
12
+
13
+ super
14
+ end
15
+
16
+ def initialize
17
+ @pid = ::Process.pid
18
+ @hostname = Socket.gethostname
19
+ end
20
+
21
+ def collect
22
+ {
23
+ type: 'sidekiq_process',
24
+ process: collect_stats
25
+ }
26
+ end
27
+
28
+ def collect_stats
29
+ process = current_process
30
+ return {} unless process
31
+
32
+ {
33
+ busy: process['busy'],
34
+ concurrency: process['concurrency'],
35
+ labels: {
36
+ labels: process['labels'].sort.join(','),
37
+ queues: process['queues'].sort.join(','),
38
+ quiet: process['quiet'],
39
+ tag: process['tag'],
40
+ hostname: process['hostname'],
41
+ identity: process['identity'],
42
+ }
43
+ }
44
+ end
45
+
46
+ def current_process
47
+ ::Sidekiq::ProcessSet.new.find do |sp|
48
+ sp['hostname'] == @hostname && sp['pid'] == @pid
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,22 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PrometheusExporter::Instrumentation
4
- class SidekiqQueue
5
- def self.start(client: nil, frequency: 30)
4
+ class SidekiqQueue < PeriodicStats
5
+ def self.start(client: nil, frequency: 30, all_queues: false)
6
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
7
+ sidekiq_queue_collector = new(all_queues: all_queues)
8
+
9
+ worker_loop do
10
+ client.send_json(sidekiq_queue_collector.collect)
19
11
  end
12
+
13
+ super
14
+ end
15
+
16
+ def initialize(all_queues: false)
17
+ @all_queues = all_queues
18
+ @pid = ::Process.pid
19
+ @hostname = Socket.gethostname
20
20
  end
21
21
 
22
22
  def collect
@@ -27,24 +27,32 @@ 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
30
+ sidekiq_queues = ::Sidekiq::Queue.all
33
31
 
34
- process = ps.find do |sp|
35
- sp['hostname'] == hostname && sp['pid'] == pid
32
+ unless @all_queues
33
+ queues = collect_current_process_queues
34
+ sidekiq_queues.select! { |sidekiq_queue| queues.include?(sidekiq_queue.name) }
36
35
  end
37
36
 
38
- queues = process.nil? ? [] : process['queues']
39
-
40
- ::Sidekiq::Queue.all.map do |queue|
41
- next unless queues.include? queue.name
37
+ sidekiq_queues.map do |queue|
42
38
  {
43
- backlog_total: queue.size,
39
+ backlog: queue.size,
44
40
  latency_seconds: queue.latency.to_i,
45
41
  labels: { queue: queue.name }
46
42
  }
47
43
  end.compact
48
44
  end
45
+
46
+ private
47
+
48
+ def collect_current_process_queues
49
+ ps = ::Sidekiq::ProcessSet.new
50
+
51
+ process = ps.find do |sp|
52
+ sp['hostname'] == @hostname && sp['pid'] == @pid
53
+ end
54
+
55
+ process.nil? ? [] : process['queues']
56
+ end
49
57
  end
50
58
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrometheusExporter::Instrumentation
4
+ class SidekiqStats < PeriodicStats
5
+ def self.start(client: nil, frequency: 30)
6
+ client ||= PrometheusExporter::Client.default
7
+ sidekiq_stats_collector = new
8
+
9
+ worker_loop do
10
+ client.send_json(sidekiq_stats_collector.collect)
11
+ end
12
+
13
+ super
14
+ end
15
+
16
+ def collect
17
+ {
18
+ type: 'sidekiq_stats',
19
+ stats: collect_stats
20
+ }
21
+ end
22
+
23
+ def collect_stats
24
+ stats = ::Sidekiq::Stats.new
25
+ {
26
+ 'dead_size' => stats.dead_size,
27
+ 'enqueued' => stats.enqueued,
28
+ 'failed' => stats.failed,
29
+ 'processed' => stats.processed,
30
+ 'processes_size' => stats.processes_size,
31
+ 'retry_size' => stats.retry_size,
32
+ 'scheduled_size' => stats.scheduled_size,
33
+ 'workers_size' => stats.workers_size,
34
+ }
35
+ end
36
+ end
37
+ end
@@ -8,22 +8,17 @@ end
8
8
 
9
9
  module PrometheusExporter::Instrumentation
10
10
  # collects stats from unicorn
11
- class Unicorn
11
+ class Unicorn < PeriodicStats
12
12
  def self.start(pid_file:, listener_address:, client: nil, frequency: 30)
13
13
  unicorn_collector = new(pid_file: pid_file, listener_address: listener_address)
14
14
  client ||= PrometheusExporter::Client.default
15
- Thread.new do
16
- loop do
17
- begin
18
- metric = unicorn_collector.collect
19
- client.send_json metric
20
- rescue StandardError => e
21
- STDERR.puts("Prometheus Exporter Failed To Collect Unicorn Stats #{e}")
22
- ensure
23
- sleep frequency
24
- end
25
- end
15
+
16
+ worker_loop do
17
+ metric = unicorn_collector.collect
18
+ client.send_json metric
26
19
  end
20
+
21
+ super
27
22
  end
28
23
 
29
24
  def initialize(pid_file:, listener_address:)
@@ -42,9 +37,9 @@ module PrometheusExporter::Instrumentation
42
37
  def collect_unicorn_stats(metric)
43
38
  stats = listener_address_stats
44
39
 
45
- metric[:active_workers_total] = stats.active
46
- metric[:request_backlog_total] = stats.queued
47
- metric[:workers_total] = worker_process_count
40
+ metric[:active_workers] = stats.active
41
+ metric[:request_backlog] = stats.queued
42
+ metric[:workers] = worker_process_count
48
43
  end
49
44
 
50
45
  private
@@ -1,13 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "client"
4
+ require_relative "instrumentation/periodic_stats"
4
5
  require_relative "instrumentation/process"
5
6
  require_relative "instrumentation/method_profiler"
6
7
  require_relative "instrumentation/sidekiq"
7
8
  require_relative "instrumentation/sidekiq_queue"
9
+ require_relative "instrumentation/sidekiq_process"
10
+ require_relative "instrumentation/sidekiq_stats"
8
11
  require_relative "instrumentation/delayed_job"
9
12
  require_relative "instrumentation/puma"
10
13
  require_relative "instrumentation/hutch"
11
14
  require_relative "instrumentation/unicorn"
12
15
  require_relative "instrumentation/active_record"
13
16
  require_relative "instrumentation/shoryuken"
17
+ require_relative "instrumentation/resque"
18
+ require_relative "instrumentation/good_job"
@@ -5,6 +5,7 @@ module PrometheusExporter::Metric
5
5
 
6
6
  @default_prefix = nil if !defined?(@default_prefix)
7
7
  @default_labels = nil if !defined?(@default_labels)
8
+ @default_aggregation = nil if !defined?(@default_aggregation)
8
9
 
9
10
  # prefix applied to all metrics
10
11
  def self.default_prefix=(name)
@@ -23,6 +24,14 @@ module PrometheusExporter::Metric
23
24
  @default_labels || {}
24
25
  end
25
26
 
27
+ def self.default_aggregation=(aggregation)
28
+ @default_aggregation = aggregation
29
+ end
30
+
31
+ def self.default_aggregation
32
+ @default_aggregation ||= Summary
33
+ end
34
+
26
35
  attr_accessor :help, :name, :data
27
36
 
28
37
  def initialize(name, help)
@@ -66,7 +75,7 @@ module PrometheusExporter::Metric
66
75
  end
67
76
 
68
77
  def labels_text(labels)
69
- labels = (labels || {}).merge(Base.default_labels)
78
+ labels = Base.default_labels.merge(labels || {})
70
79
  if labels && labels.length > 0
71
80
  s = labels.map do |key, value|
72
81
  value = value.to_s
@@ -97,15 +106,8 @@ module PrometheusExporter::Metric
97
106
  end
98
107
  end
99
108
 
100
- # when we drop Ruby 2.3 we can drop this
101
- if "".respond_to? :match?
102
- def needs_escape?(str)
103
- str.match?(/[\n"\\]/m)
104
- end
105
- else
106
- def needs_escape?(str)
107
- !!str.match(/[\n"\\]/m)
108
- end
109
+ def needs_escape?(str)
110
+ str.match?(/[\n"\\]/m)
109
111
  end
110
112
 
111
113
  end
@@ -5,6 +5,10 @@ module PrometheusExporter::Metric
5
5
  attr_reader :data
6
6
 
7
7
  def initialize(name, help)
8
+ if name.end_with?("_total")
9
+ raise ArgumentError, "The metric name of gauge must not have _total suffix. Given: #{name}"
10
+ end
11
+
8
12
  super
9
13
  reset!
10
14
  end
@@ -5,9 +5,21 @@ module PrometheusExporter::Metric
5
5
 
6
6
  DEFAULT_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5.0, 10.0].freeze
7
7
 
8
+ @default_buckets = nil if !defined?(@default_buckets)
9
+
10
+ def self.default_buckets
11
+ @default_buckets || DEFAULT_BUCKETS
12
+ end
13
+
14
+ def self.default_buckets=(buckets)
15
+ @default_buckets = buckets
16
+ end
17
+
18
+ attr_reader :buckets
19
+
8
20
  def initialize(name, help, opts = {})
9
21
  super(name, help)
10
- @buckets = (opts[:buckets] || DEFAULT_BUCKETS).sort.reverse
22
+ @buckets = (opts[:buckets] || self.class.default_buckets).sort
11
23
  reset!
12
24
  end
13
25
 
@@ -45,11 +57,11 @@ module PrometheusExporter::Metric
45
57
  first = false
46
58
  count = @counts[labels]
47
59
  sum = @sums[labels]
48
- text << "#{prefix(@name)}_bucket#{labels_text(with_bucket(labels, "+Inf"))} #{count}\n"
49
60
  @buckets.each do |bucket|
50
61
  value = @observations[labels][bucket]
51
62
  text << "#{prefix(@name)}_bucket#{labels_text(with_bucket(labels, bucket.to_s))} #{value}\n"
52
63
  end
64
+ text << "#{prefix(@name)}_bucket#{labels_text(with_bucket(labels, "+Inf"))} #{count}\n"
53
65
  text << "#{prefix(@name)}_count#{labels_text(labels)} #{count}\n"
54
66
  text << "#{prefix(@name)}_sum#{labels_text(labels)} #{sum}"
55
67
  end
@@ -79,7 +91,7 @@ module PrometheusExporter::Metric
79
91
  end
80
92
 
81
93
  def fill_buckets(value, buckets)
82
- @buckets.each do |b|
94
+ @buckets.reverse_each do |b|
83
95
  break if value > b
84
96
  buckets[b] += 1
85
97
  end
@@ -6,23 +6,30 @@ require 'prometheus_exporter/client'
6
6
  class PrometheusExporter::Middleware
7
7
  MethodProfiler = PrometheusExporter::Instrumentation::MethodProfiler
8
8
 
9
- def initialize(app, config = { instrument: true, client: nil })
9
+ def initialize(app, config = { instrument: :alias_method, client: nil })
10
10
  @app = app
11
11
  @client = config[:client] || PrometheusExporter::Client.default
12
12
 
13
13
  if config[:instrument]
14
- if defined? Redis::Client
15
- MethodProfiler.patch(Redis::Client, [:call, :call_pipeline], :redis)
14
+ if defined?(RedisClient)
15
+ apply_redis_client_middleware!
16
+ end
17
+ if defined?(Redis::VERSION) && (Gem::Version.new(Redis::VERSION) >= Gem::Version.new('5.0.0'))
18
+ # redis 5 support handled via RedisClient
19
+ elsif defined? Redis::Client
20
+ MethodProfiler.patch(Redis::Client, [
21
+ :call, :call_pipeline
22
+ ], :redis, instrument: config[:instrument])
16
23
  end
17
24
  if defined? PG::Connection
18
25
  MethodProfiler.patch(PG::Connection, [
19
26
  :exec, :async_exec, :exec_prepared, :send_query_prepared, :query
20
- ], :sql)
27
+ ], :sql, instrument: config[:instrument])
21
28
  end
22
29
  if defined? Mysql2::Client
23
- MethodProfiler.patch(Mysql2::Client, [:query], :sql)
24
- MethodProfiler.patch(Mysql2::Statement, [:execute], :sql)
25
- MethodProfiler.patch(Mysql2::Result, [:each], :sql)
30
+ MethodProfiler.patch(Mysql2::Client, [:query], :sql, instrument: config[:instrument])
31
+ MethodProfiler.patch(Mysql2::Statement, [:execute], :sql, instrument: config[:instrument])
32
+ MethodProfiler.patch(Mysql2::Result, [:each], :sql, instrument: config[:instrument])
26
33
  end
27
34
  end
28
35
  end
@@ -36,11 +43,12 @@ class PrometheusExporter::Middleware
36
43
 
37
44
  result
38
45
  ensure
39
-
46
+ status = (result && result[0]) || -1
40
47
  obj = {
41
48
  type: "web",
42
49
  timings: info,
43
50
  queue_time: queue_time,
51
+ status: status,
44
52
  default_labels: default_labels(env, result)
45
53
  }
46
54
  labels = custom_labels(env)
@@ -52,18 +60,21 @@ class PrometheusExporter::Middleware
52
60
  end
53
61
 
54
62
  def default_labels(env, result)
55
- status = (result && result[0]) || -1
56
63
  params = env["action_dispatch.request.parameters"]
57
64
  action = controller = nil
58
65
  if params
59
66
  action = params["action"]
60
67
  controller = params["controller"]
68
+ elsif (cors = env["rack.cors"]) && cors.respond_to?(:preflight?) && cors.preflight?
69
+ # if the Rack CORS Middleware identifies the request as a preflight request,
70
+ # the stack doesn't get to the point where controllers/actions are defined
71
+ action = "preflight"
72
+ controller = "preflight"
61
73
  end
62
74
 
63
75
  {
64
76
  action: action || "other",
65
- controller: controller || "other",
66
- status: status
77
+ controller: controller || "other"
67
78
  }
68
79
  end
69
80
 
@@ -90,19 +101,34 @@ class PrometheusExporter::Middleware
90
101
  Process.clock_gettime(Process::CLOCK_REALTIME)
91
102
  end
92
103
 
93
- # get the content of the x-queue-start or x-request-start header
104
+ # determine queue start from well-known trace headers
94
105
  def queue_start(env)
106
+
107
+ # get the content of the x-queue-start or x-request-start header
95
108
  value = env['HTTP_X_REQUEST_START'] || env['HTTP_X_QUEUE_START']
96
109
  unless value.nil? || value == ''
97
- convert_header_to_ms(value.to_s)
110
+ # nginx returns time as milliseconds with 3 decimal places
111
+ # apache returns time as microseconds without decimal places
112
+ # this method takes care to convert both into a proper second + fractions timestamp
113
+ value = value.to_s.gsub(/t=|\./, '')
114
+ return "#{value[0, 10]}.#{value[10, 13]}".to_f
98
115
  end
116
+
117
+ # get the content of the x-amzn-trace-id header
118
+ # see also: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-request-tracing.html
119
+ value = env['HTTP_X_AMZN_TRACE_ID']
120
+ value&.split('Root=')&.last&.split('-')&.fetch(1)&.to_i(16)
121
+
122
+ end
123
+
124
+ private
125
+
126
+ module RedisInstrumenter
127
+ MethodProfiler.define_methods_on_module(self, ["call", "call_pipelined"], "redis")
99
128
  end
100
129
 
101
- # nginx returns time as milliseconds with 3 decimal places
102
- # apache returns time as microseconds without decimal places
103
- # this method takes care to convert both into a proper second + fractions timestamp
104
- def convert_header_to_ms(str)
105
- str = str.gsub(/t=|\./, '')
106
- "#{str[0, 10]}.#{str[10, 13]}".to_f
130
+ def apply_redis_client_middleware!
131
+ RedisClient.register(RedisInstrumenter)
107
132
  end
133
+
108
134
  end
@@ -2,7 +2,8 @@
2
2
 
3
3
  module PrometheusExporter::Server
4
4
  class ActiveRecordCollector < TypeCollector
5
- MAX_ACTIVERECORD_METRIC_AGE = 60
5
+ MAX_METRIC_AGE = 60
6
+
6
7
  ACTIVE_RECORD_GAUGES = {
7
8
  connections: "Total connections in pool",
8
9
  busy: "Connections in use in pool",
@@ -13,7 +14,12 @@ module PrometheusExporter::Server
13
14
  }
14
15
 
15
16
  def initialize
16
- @active_record_metrics = []
17
+ @active_record_metrics = MetricsContainer.new(ttl: MAX_METRIC_AGE)
18
+ @active_record_metrics.filter = -> (new_metric, old_metric) do
19
+ new_metric["pid"] == old_metric["pid"] &&
20
+ new_metric["hostname"] == old_metric["hostname"] &&
21
+ new_metric["metric_labels"]["pool_name"] == old_metric["metric_labels"]["pool_name"]
22
+ end
17
23
  end
18
24
 
19
25
  def type
@@ -26,7 +32,7 @@ module PrometheusExporter::Server
26
32
  metrics = {}
27
33
 
28
34
  @active_record_metrics.map do |m|
29
- metric_key = (m["metric_labels"] || {}).merge("pid" => m["pid"])
35
+ metric_key = (m["metric_labels"] || {}).merge("pid" => m["pid"], "hostname" => m["hostname"])
30
36
  metric_key.merge!(m["custom_labels"]) if m["custom_labels"]
31
37
 
32
38
  ACTIVE_RECORD_GAUGES.map do |k, help|
@@ -42,15 +48,6 @@ module PrometheusExporter::Server
42
48
  end
43
49
 
44
50
  def collect(obj)
45
- now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
46
-
47
- obj["created_at"] = now
48
-
49
- @active_record_metrics.delete_if do |current|
50
- (obj["pid"] == current["pid"] && obj["hostname"] == current["hostname"]) ||
51
- (current["created_at"] + MAX_ACTIVERECORD_METRIC_AGE < now)
52
- end
53
-
54
51
  @active_record_metrics << obj
55
52
  end
56
53
  end
@@ -14,12 +14,16 @@ module PrometheusExporter::Server
14
14
  register_collector(ProcessCollector.new)
15
15
  register_collector(SidekiqCollector.new)
16
16
  register_collector(SidekiqQueueCollector.new)
17
+ register_collector(SidekiqProcessCollector.new)
18
+ register_collector(SidekiqStatsCollector.new)
17
19
  register_collector(DelayedJobCollector.new)
18
20
  register_collector(PumaCollector.new)
19
21
  register_collector(HutchCollector.new)
20
22
  register_collector(UnicornCollector.new)
21
23
  register_collector(ActiveRecordCollector.new)
22
24
  register_collector(ShoryukenCollector.new)
25
+ register_collector(ResqueCollector.new)
26
+ register_collector(GoodJobCollector.new)
23
27
  end
24
28
 
25
29
  def register_collector(collector)