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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +82 -25
- data/Appraisals +7 -3
- data/CHANGELOG +104 -24
- data/Dockerfile +9 -0
- data/README.md +258 -51
- data/bin/prometheus_exporter +19 -6
- data/examples/custom_collector.rb +1 -1
- data/gemfiles/ar_70.gemfile +5 -0
- data/lib/prometheus_exporter/client.rb +48 -23
- data/lib/prometheus_exporter/instrumentation/active_record.rb +11 -29
- data/lib/prometheus_exporter/instrumentation/delayed_job.rb +5 -2
- data/lib/prometheus_exporter/instrumentation/good_job.rb +30 -0
- data/lib/prometheus_exporter/instrumentation/method_profiler.rb +63 -23
- data/lib/prometheus_exporter/instrumentation/periodic_stats.rb +62 -0
- data/lib/prometheus_exporter/instrumentation/process.rb +5 -21
- data/lib/prometheus_exporter/instrumentation/puma.rb +34 -27
- data/lib/prometheus_exporter/instrumentation/resque.rb +35 -0
- data/lib/prometheus_exporter/instrumentation/sidekiq.rb +53 -23
- data/lib/prometheus_exporter/instrumentation/sidekiq_process.rb +52 -0
- data/lib/prometheus_exporter/instrumentation/sidekiq_queue.rb +32 -24
- data/lib/prometheus_exporter/instrumentation/sidekiq_stats.rb +37 -0
- data/lib/prometheus_exporter/instrumentation/unicorn.rb +10 -15
- data/lib/prometheus_exporter/instrumentation.rb +5 -0
- data/lib/prometheus_exporter/metric/base.rb +12 -10
- data/lib/prometheus_exporter/metric/gauge.rb +4 -0
- data/lib/prometheus_exporter/metric/histogram.rb +15 -3
- data/lib/prometheus_exporter/middleware.rb +45 -19
- data/lib/prometheus_exporter/server/active_record_collector.rb +9 -12
- data/lib/prometheus_exporter/server/collector.rb +4 -0
- data/lib/prometheus_exporter/server/delayed_job_collector.rb +24 -18
- data/lib/prometheus_exporter/server/good_job_collector.rb +52 -0
- data/lib/prometheus_exporter/server/metrics_container.rb +66 -0
- data/lib/prometheus_exporter/server/process_collector.rb +8 -13
- data/lib/prometheus_exporter/server/puma_collector.rb +14 -12
- data/lib/prometheus_exporter/server/resque_collector.rb +50 -0
- data/lib/prometheus_exporter/server/runner.rb +14 -3
- data/lib/prometheus_exporter/server/sidekiq_collector.rb +1 -1
- data/lib/prometheus_exporter/server/sidekiq_process_collector.rb +43 -0
- data/lib/prometheus_exporter/server/sidekiq_queue_collector.rb +6 -7
- data/lib/prometheus_exporter/server/sidekiq_stats_collector.rb +48 -0
- data/lib/prometheus_exporter/server/type_collector.rb +2 -0
- data/lib/prometheus_exporter/server/unicorn_collector.rb +32 -33
- data/lib/prometheus_exporter/server/web_collector.rb +17 -17
- data/lib/prometheus_exporter/server/web_server.rb +72 -41
- data/lib/prometheus_exporter/server.rb +4 -0
- data/lib/prometheus_exporter/version.rb +1 -1
- data/lib/prometheus_exporter.rb +12 -13
- data/prometheus_exporter.gemspec +6 -6
- metadata +53 -14
@@ -1,32 +1,51 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "yaml"
|
4
4
|
|
5
5
|
module PrometheusExporter::Instrumentation
|
6
|
-
JOB_WRAPPER_CLASS_NAME =
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
->
|
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
|
29
|
-
|
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(
|
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[
|
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
|
-
|
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
|
-
|
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
|
-
|
10
|
-
|
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
|
-
|
31
|
-
pid = ::Process.pid
|
32
|
-
ps = ::Sidekiq::ProcessSet.new
|
30
|
+
sidekiq_queues = ::Sidekiq::Queue.all
|
33
31
|
|
34
|
-
|
35
|
-
|
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
|
-
|
39
|
-
|
40
|
-
::Sidekiq::Queue.all.map do |queue|
|
41
|
-
next unless queues.include? queue.name
|
37
|
+
sidekiq_queues.map do |queue|
|
42
38
|
{
|
43
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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[:
|
46
|
-
metric[:
|
47
|
-
metric[:
|
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 || {})
|
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
|
-
|
101
|
-
|
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,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] ||
|
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.
|
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:
|
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?
|
15
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
|
102
|
-
|
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
|
-
|
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)
|