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.
- 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)
|