Skip to content

Commit ac59efe

Browse files
committed
- Add the ability to pass custom collectors and type collectors to the web instrumenter
- Add base collector and type collector classes for ease of development of custom integrations - Change railtie to after initialization to allow for customization
1 parent 681fd6e commit ac59efe

12 files changed

Lines changed: 288 additions & 106 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ Changelog for the bc-prometheus-ruby gem.
22

33
h3. Pending Release
44

5+
- Add the ability to pass custom collectors and type collectors to the web instrumenter
6+
- Add base collector and type collector classes for ease of development of custom integrations
7+
- Change railtie to after initialization to allow for customization
8+
59
h3. 0.1.5
610

711
- Fix issue where puma collector was not being registered on the server

lib/bigcommerce/prometheus.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
require_relative 'prometheus/server'
3131
require_relative 'prometheus/client'
3232

33+
require_relative 'prometheus/collectors/base'
3334
require_relative 'prometheus/collectors/resque'
35+
require_relative 'prometheus/type_collectors/base'
3436
require_relative 'prometheus/type_collectors/resque'
3537

3638
require_relative 'prometheus/instrumentors/web'
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright (c) 2019-present, BigCommerce Pty. Ltd. All rights reserved
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6+
# documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8+
# persons to whom the Software is furnished to do so, subject to the following conditions:
9+
#
10+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11+
# Software.
12+
#
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17+
#
18+
module Bigcommerce
19+
module Prometheus
20+
module Collectors
21+
##
22+
# Base class for collectors
23+
#
24+
class Base
25+
##
26+
# Start the collector
27+
#
28+
def self.start(*args, &block)
29+
process_collector = new(*args, &block)
30+
31+
stop if @thread
32+
33+
@thread = Thread.new do
34+
Kernel.loop do
35+
process_collector.run
36+
end
37+
end
38+
end
39+
40+
##
41+
# Stop the collector
42+
#
43+
def self.stop
44+
t = @thread
45+
return unless t
46+
47+
t.kill
48+
@thread = nil
49+
end
50+
51+
##
52+
# @param [PrometheusExporter::Client] client
53+
# @param [String] type
54+
# @param [Integer] frequency
55+
# @param [Hash] options
56+
#
57+
def initialize(client: nil, type: nil, frequency: nil, options: nil)
58+
@client = client || PrometheusExporter::Client.default
59+
@type = type || self.class.to_s.downcase.gsub('::', '_').gsub('collector', '')
60+
@frequency = frequency || 15
61+
@options = options || {}
62+
@logger = Bigcommerce::Prometheus.logger
63+
end
64+
65+
##
66+
# Run the collector and send stats
67+
#
68+
def run
69+
metrics = { type: @type }
70+
metrics = collect(metrics)
71+
@logger.debug "[bigcommerce-prometheus] Pushing #{@type} metrics to type collector: #{metrics.inspect}"
72+
@client.send_json(metrics)
73+
rescue StandardError => e
74+
@logger.error("Prometheus Exporter Failed To Collect #{@type} Stats #{e}")
75+
ensure
76+
sleep @frequency
77+
end
78+
79+
##
80+
# Collect metrics. This should be overridden in derivative collectors
81+
#
82+
# @param [Hash] metrics
83+
# @return [Hash]
84+
#
85+
def collect(metrics = {})
86+
metrics
87+
end
88+
end
89+
end
90+
end
91+
end

lib/bigcommerce/prometheus/collectors/resque.rb

Lines changed: 11 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -22,54 +22,21 @@ module Collectors
2222
# Collect metrics to push to the server type collector
2323
#
2424
class Resque
25-
include Bigcommerce::Prometheus::Loggable
26-
27-
##
28-
# @param [Bigcommerce::Prometheus::Client] client
29-
# @param [Integer] frequency
30-
#
31-
def initialize(client:, frequency: nil)
32-
@client = client || Bigcommerce::Prometheus.client
33-
@frequency = frequency || 15
34-
end
35-
3625
##
37-
# Start the collector
26+
# @param [Hash] metrics
27+
# @return [Hash]
3828
#
39-
def self.start(client: nil, frequency: nil)
40-
collector = new(client: client, frequency: frequency)
41-
Thread.new do
42-
loop do
43-
collector.run
44-
end
45-
end
46-
end
47-
48-
def run
49-
metric = collect
50-
logger.debug "[bigcommerce-prometheus] Pushing resque metrics to type collector: #{metric.inspect}"
51-
@client.send_json metric
52-
rescue StandardError => e
53-
logger.error "[bigcommerce-prometheus] Failed to collect resque prometheus stats: #{e.message}"
54-
ensure
55-
sleep @frequency
56-
end
57-
58-
private
59-
60-
def collect
29+
def collect(metrics = {})
6130
info = ::Resque.info
6231

63-
metric = {}
64-
metric[:type] = 'resque'
65-
metric[:environment] = info[:environment].to_s
66-
metric[:workers_total] = info[:workers].to_i
67-
metric[:jobs_failed_total] = info[:failed].to_i
68-
metric[:jobs_pending_total] = info[:pending].to_i
69-
metric[:jobs_processed_total] = info[:processed].to_i
70-
metric[:queues_total] = info[:queues].to_i
71-
metric[:queues] = queue_sizes
72-
metric
32+
metrics[:environment] = info[:environment].to_s
33+
metrics[:workers_total] = info[:workers].to_i
34+
metrics[:jobs_failed_total] = info[:failed].to_i
35+
metrics[:jobs_pending_total] = info[:pending].to_i
36+
metrics[:jobs_processed_total] = info[:processed].to_i
37+
metrics[:queues_total] = info[:queues].to_i
38+
metrics[:queues] = queue_sizes
39+
metrics
7340
end
7441

7542
def queue_sizes

lib/bigcommerce/prometheus/configuration.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ module Configuration
3434
server_host: '0.0.0.0',
3535
server_port: PrometheusExporter::DEFAULT_PORT,
3636
server_timeout: PrometheusExporter::DEFAULT_TIMEOUT,
37-
server_prefix: PrometheusExporter::DEFAULT_PREFIX
37+
server_prefix: PrometheusExporter::DEFAULT_PREFIX,
38+
web_collectors: [],
39+
web_type_collectors: []
3840
}.freeze
3941

4042
attr_accessor *VALID_CONFIG_KEYS.keys
@@ -89,6 +91,7 @@ def reset
8991

9092
self.puma_process_label = ENV.fetch('PROMETHEUS_PUMA_PROCESS_LABEL', 'web').to_s
9193
self.puma_collection_frequency = ENV.fetch('PROMETHEUS_PUMA_COLLECTION_FREQUENCY', 30).to_i
94+
self.web_type_collectors = []
9295
end
9396

9497
##

lib/bigcommerce/prometheus/instrumentors/web.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ def initialize(app:)
3131
@server_timeout = Bigcommerce::Prometheus.server_timeout
3232
@server_prefix = Bigcommerce::Prometheus.server_prefix
3333
@process_name = Bigcommerce::Prometheus.process_name
34+
@collectors = Bigcommerce::Prometheus.web_collectors || []
35+
@type_collectors = Bigcommerce::Prometheus.web_type_collectors || []
3436
end
3537

3638
##
@@ -65,6 +67,9 @@ def setup_before_fork
6567
server.add_type_collector(PrometheusExporter::Server::ActiveRecordCollector.new)
6668
server.add_type_collector(PrometheusExporter::Server::WebCollector.new)
6769
server.add_type_collector(PrometheusExporter::Server::PumaCollector.new)
70+
@type_collectors.each do |tc|
71+
server.add_type_collector(tc)
72+
end
6873
server.start
6974
end
7075
end
@@ -73,6 +78,7 @@ def setup_after_fork
7378
@app.config.after_fork_callbacks = [] unless @app.config.after_fork_callbacks
7479
@app.config.after_fork_callbacks << lambda do
7580
::Bigcommerce::Prometheus::Integrations::Puma.start
81+
@collectors.each(&:start)
7682
end
7783
end
7884

lib/bigcommerce/prometheus/integrations/railtie.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ module Integrations
2222
# Railtie for automatic configuration of Rails environments
2323
#
2424
class Railtie < ::Rails::Railtie
25-
initializer 'zzz_bigcommerce.prometheus.configure_rails_initialization' do |app|
25+
config.after_initialize do |app|
2626
Bigcommerce::Prometheus::Instrumentors::Web.new(app: app).start
2727
end
2828
end
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright (c) 2019-present, BigCommerce Pty. Ltd. All rights reserved
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6+
# documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8+
# persons to whom the Software is furnished to do so, subject to the following conditions:
9+
#
10+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11+
# Software.
12+
#
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17+
#
18+
module Bigcommerce
19+
module Prometheus
20+
module TypeCollectors
21+
##
22+
# Base type collector class
23+
#
24+
class Base < PrometheusExporter::Server::TypeCollector
25+
attr_reader :type
26+
27+
##
28+
# @param [String] type
29+
# @param [Hash] default_labels
30+
#
31+
def initialize(type: nil, default_labels: {})
32+
@type = type || self.class.to_s.downcase.gsub('::', '_').gsub('typecollector', '')
33+
@default_labels = default_labels || {}
34+
@metrics = build_metrics
35+
end
36+
37+
##
38+
# @return [Array]
39+
#
40+
def metrics
41+
return [] unless @metrics.any?
42+
43+
@metrics.values
44+
end
45+
46+
##
47+
# @param [Symbol] key
48+
#
49+
def metric(key)
50+
@metrics.fetch(key.to_sym, nil)
51+
end
52+
53+
##
54+
# Collect metrics from input data
55+
#
56+
# @param [Hash] data
57+
#
58+
def collect(data)
59+
custom_labels = data.fetch('custom_labels', nil)
60+
labels = custom_labels.nil? ? @default_labels : @default_labels.merge(custom_labels)
61+
collect_metrics(data: data, labels: labels)
62+
end
63+
64+
private
65+
66+
##
67+
# Collect metrics. Implementations of translating metrics to observed calls should happen here.
68+
#
69+
def collect_metrics(*)
70+
raise NotImplementedError, 'Must implement collect_metrics'
71+
end
72+
73+
##
74+
# Build and return all observable metrics. This should be a hash of symbol keys that map to
75+
# PrometheusExporter::Metric::Base objects.
76+
#
77+
# @return [Hash]
78+
#
79+
def build_metrics
80+
{}
81+
end
82+
end
83+
end
84+
end
85+
end

lib/bigcommerce/prometheus/type_collectors/resque.rb

Lines changed: 19 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -21,59 +21,33 @@ module TypeCollectors
2121
##
2222
# Collect resque data from collectors and parse them into metrics
2323
#
24-
class Resque < PrometheusExporter::Server::TypeCollector
24+
class Resque < Bigcommerce::Prometheus::TypeCollectors::Base
2525
##
2626
# Initialize the collector
2727
#
28-
def initialize
29-
@workers_total = PrometheusExporter::Metric::Gauge.new('resque_workers_total', 'Number of active workers')
30-
@jobs_failed_total = PrometheusExporter::Metric::Gauge.new('jobs_failed_total', 'Number of failed jobs')
31-
@jobs_pending_total = PrometheusExporter::Metric::Gauge.new('jobs_pending_total', 'Number of pending jobs')
32-
@jobs_processed_total = PrometheusExporter::Metric::Gauge.new('jobs_processed_total', 'Number of processed jobs')
33-
@queues_total = PrometheusExporter::Metric::Gauge.new('queues_total', 'Number of total queues')
34-
@queue_sizes = PrometheusExporter::Metric::Gauge.new('queue_sizes', 'Size of each queue')
35-
end
36-
37-
##
38-
# @return [String]
39-
#
40-
def type
41-
'resque'
42-
end
43-
44-
##
45-
# @return [Array]
46-
#
47-
def metrics
48-
return [] unless @workers_total
49-
50-
[
51-
@workers_total,
52-
@jobs_failed_total,
53-
@jobs_pending_total,
54-
@jobs_processed_total,
55-
@queues_total,
56-
@queue_sizes
57-
]
28+
def build_metrics
29+
{
30+
workers_total: PrometheusExporter::Metric::Gauge.new('resque_workers_total', 'Number of active workers'),
31+
jobs_failed_total: PrometheusExporter::Metric::Gauge.new('jobs_failed_total', 'Number of failed jobs'),
32+
jobs_pending_total: PrometheusExporter::Metric::Gauge.new('jobs_pending_total', 'Number of pending jobs'),
33+
jobs_processed_total: PrometheusExporter::Metric::Gauge.new('jobs_processed_total', 'Number of processed jobs'),
34+
queues_total: PrometheusExporter::Metric::Gauge.new('queues_total', 'Number of total queues'),
35+
queue_sizes: PrometheusExporter::Metric::Gauge.new('queue_sizes', 'Size of each queue')
36+
}
5837
end
5938

6039
##
6140
# Collect resque metrics from input data
6241
#
63-
def collect(obj)
64-
default_labels = { environment: obj['environment'] }
65-
66-
custom_labels = obj['custom_labels']
67-
labels = custom_labels.nil? ? default_labels : default_labels.merge(custom_labels)
68-
69-
@workers_total.observe(obj['workers_total'], labels)
70-
@jobs_failed_total.observe(obj['jobs_failed_total'], labels)
71-
@jobs_pending_total.observe(obj['jobs_pending_total'], labels)
72-
@jobs_processed_total.observe(obj['jobs_processed_total'], labels)
73-
@queues_total.observe(obj['queues_total'], labels)
74-
75-
obj['queues'].each do |name, size|
76-
@queue_sizes.observe(size, labels.merge(queue: name))
42+
def collect_metrics(data:, labels: {})
43+
metric(:workers_total).observe(obj['workers_total'], labels)
44+
metric(:jobs_failed_total).observe(obj['jobs_failed_total'], labels)
45+
metric(:jobs_pending_total).observe(obj['jobs_pending_total'], labels)
46+
metric(:jobs_processed_total).observe(obj['jobs_processed_total'], labels)
47+
metric(:queues_total).observe(obj['queues_total'], labels)
48+
49+
data['queues'].each do |name, size|
50+
metric(:queue_sizes).observe(size, labels.merge(queue: name))
7751
end
7852
end
7953
end

0 commit comments

Comments
 (0)