prometheus_exporter 0.7.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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)