phobos_prometheus 0.3.2

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.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'phobos_prometheus'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
@@ -0,0 +1,46 @@
1
+ metrics_prefix: phobos_app
2
+
3
+ counters:
4
+ - instrumentation: listener.process_message
5
+ - instrumentation: listener.process_batch
6
+
7
+ histograms:
8
+ - instrumentation: listener.process_message
9
+ bucket_name: message
10
+ - instrumentation: listener.process_batch
11
+ bucket_name: batch
12
+
13
+ gauges:
14
+ - label: number_of_handlers
15
+ increment: listener.start_handler
16
+ decrement: listener.stop_handler
17
+
18
+ buckets:
19
+ - name: message
20
+ bins:
21
+ - 2
22
+ - 4
23
+ - 8
24
+ - 16
25
+ - 32
26
+ - 64
27
+ - 128
28
+ - 256
29
+ - 512
30
+ - 1024
31
+ - 2048
32
+ - 4096
33
+ - name: batch
34
+ bins:
35
+ - 64
36
+ - 128
37
+ - 256
38
+ - 512
39
+ - 1024
40
+ - 2048
41
+ - 4096
42
+ - 8192
43
+ - 16384
44
+ - 32768
45
+ - 65536
46
+ - 131072
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+ require 'yaml'
5
+
6
+ require 'phobos'
7
+ require 'prometheus/client'
8
+ require 'prometheus/client/formats/text'
9
+ require 'sinatra/base'
10
+
11
+ require 'phobos_prometheus/version'
12
+ require 'phobos_prometheus/errors'
13
+ require 'phobos_prometheus/logger'
14
+ require 'phobos_prometheus/config_parser'
15
+ require 'phobos_prometheus/collector/helper'
16
+ require 'phobos_prometheus/collector/error_logger'
17
+ require 'phobos_prometheus/collector/histogram'
18
+ require 'phobos_prometheus/collector/counter'
19
+ require 'phobos_prometheus/collector/gauge'
20
+ require 'phobos_prometheus/collector'
21
+ require 'phobos_prometheus/exporter_helper'
22
+ require 'phobos_prometheus/exporter'
23
+
24
+ # Prometheus collector for Phobos
25
+ module PhobosPrometheus
26
+ class << self
27
+ include Logger
28
+ attr_reader :config, :metrics
29
+
30
+ # Public - configure and validate configuration
31
+ def configure(path)
32
+ @metrics ||= []
33
+ @config = ConfigParser.new(path).config
34
+
35
+ log_info('PhobosPrometheus configured')
36
+ end
37
+
38
+ # Public - after configured create the prometheus metrics
39
+ def subscribe
40
+ subscribe_counters
41
+ subscribe_histograms
42
+ subscribe_gauges
43
+
44
+ log_info('PhobosPrometheus subscribed') unless @metrics.empty?
45
+
46
+ self
47
+ end
48
+
49
+ private
50
+
51
+ def subscribe_counters
52
+ @config.counters.each do |counter|
53
+ @metrics << PhobosPrometheus::Collector::Counter.create(counter)
54
+ end
55
+ end
56
+
57
+ def subscribe_histograms
58
+ @config.histograms.each do |histogram|
59
+ @metrics << PhobosPrometheus::Collector::Histogram.create(histogram)
60
+ end
61
+ end
62
+
63
+ def subscribe_gauges
64
+ @config.gauges.each do |gauge|
65
+ @metrics << PhobosPrometheus::Collector::Gauge.create(gauge)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhobosPrometheus
4
+ # Collector houses common metrics code
5
+ module Collector
6
+ EVENT_LABEL_BUILDER = proc do |event|
7
+ {
8
+ topic: event.payload[:topic],
9
+ group_id: event.payload[:group_id],
10
+ handler: event.payload[:handler]
11
+ }
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhobosPrometheus
4
+ module Collector
5
+ # Collector class to track counter events
6
+ class Counter
7
+ include Helper
8
+ attr_reader :counter
9
+
10
+ def self.create(config)
11
+ instrumentation = config[:instrumentation]
12
+ raise(InvalidConfigurationError, 'Counter requires :instrumentation') \
13
+ unless instrumentation
14
+ new(instrumentation: instrumentation)
15
+ end
16
+
17
+ def initialize(instrumentation:)
18
+ @metrics_prefix = @instrumentation = @registry = @counter = nil
19
+ setup_collector_module(instrumentation: instrumentation)
20
+ end
21
+
22
+ def init_metrics(prometheus_label)
23
+ @counter = @registry.counter(
24
+ :"#{@metrics_prefix}_#{prometheus_label}_total",
25
+ "The total number of #{@instrumentation} events handled."
26
+ )
27
+ end
28
+
29
+ def update_metrics(event_label, _event)
30
+ @counter.increment(event_label)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhobosPrometheus
4
+ module Collector
5
+ # ErrorLogger logs errors to stdout
6
+ class ErrorLogger
7
+ include Logger
8
+ def initialize(error, event, instrumentation_label)
9
+ @error = error
10
+ @event = event
11
+ @instrumentation_label = instrumentation_label
12
+ end
13
+
14
+ def log
15
+ log_error(
16
+ Hash(
17
+ message: 'PhobosPrometheus: Error occured in metrics handler for subscribed event',
18
+ instrumentation_label: @instrumentation_label,
19
+ event: @event,
20
+ exception_class: @error.class.to_s,
21
+ exception_message: @error.message,
22
+ backtrace: @error.backtrace
23
+ )
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhobosPrometheus
4
+ module Collector
5
+ # Collector class to track gauge events
6
+ class Gauge
7
+ attr_reader :gauge, :registry
8
+
9
+ def self.create(config)
10
+ label = config[:label]
11
+ increment = config[:increment]
12
+ decrement = config[:decrement]
13
+
14
+ raise(InvalidConfigurationError, 'Gauge requires :label, :increment and :decrement') \
15
+ unless label && increment && decrement
16
+ new(label: label, increment: increment, decrement: decrement)
17
+ end
18
+
19
+ def initialize(label:, increment:, decrement:)
20
+ @registry = Prometheus::Client.registry
21
+ @metrics_prefix = PhobosPrometheus.config.metrics_prefix || 'phobos_client'
22
+ @increment = increment
23
+ @decrement = decrement
24
+ @label = label
25
+ @gauge = @registry.gauge(
26
+ :"#{@metrics_prefix}_#{label}", "The current count of #{@label}"
27
+ )
28
+
29
+ subscribe_metrics
30
+ end
31
+
32
+ def subscribe_metrics
33
+ Phobos::Instrumentation.subscribe(@increment) do |event|
34
+ safely_update_metrics(event, :increment)
35
+ end
36
+
37
+ Phobos::Instrumentation.subscribe(@decrement) do |event|
38
+ safely_update_metrics(event, :decrement)
39
+ end
40
+ end
41
+
42
+ # rubocop:disable Lint/RescueWithoutErrorClass
43
+ def safely_update_metrics(event, operation)
44
+ event_label = EVENT_LABEL_BUILDER.call(event)
45
+ # .increment and .decrement is not released yet
46
+ # @gauge.public_send(operation, event_label)
47
+ current = @gauge.get(event_label) || 0
48
+ if operation == :increment
49
+ @gauge.set(event_label, current + 1)
50
+ else
51
+ @gauge.set(event_label, current - 1)
52
+ end
53
+ rescue => error
54
+ ErrorLogger.new(error, event, @label).log
55
+ end
56
+ # rubocop:enable Lint/RescueWithoutErrorClass
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhobosPrometheus
4
+ module Collector
5
+ # Shared code between collectors.
6
+ # Using module to avoid introducing inheritance
7
+ module Helper
8
+ attr_reader :registry
9
+
10
+ def setup_collector_module(instrumentation:)
11
+ @instrumentation = instrumentation
12
+ @registry = Prometheus::Client.registry
13
+ @metrics_prefix = PhobosPrometheus.config.metrics_prefix || 'phobos_client'
14
+
15
+ init_metrics(instrumentation.sub('.', '_'))
16
+ subscribe_metrics
17
+ end
18
+
19
+ def subscribe_metrics
20
+ Phobos::Instrumentation.subscribe(@instrumentation) do |event|
21
+ safely_update_metrics(event)
22
+ end
23
+ end
24
+
25
+ # rubocop:disable Lint/RescueWithoutErrorClass
26
+ def safely_update_metrics(event)
27
+ event_label = EVENT_LABEL_BUILDER.call(event)
28
+ update_metrics(event_label, event)
29
+ rescue => error
30
+ ErrorLogger.new(error, event, @instrumentation).log
31
+ end
32
+ # rubocop:enable Lint/RescueWithoutErrorClass
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhobosPrometheus
4
+ module Collector
5
+ # Collector class to track histogram events
6
+ class Histogram
7
+ include Helper
8
+ attr_reader :histogram
9
+
10
+ # Buckets in ms for histogram
11
+ BUCKETS = [5, 10, 25, 50, 100, 250, 500, 750, 1500, 3000, 5000].freeze
12
+
13
+ def self.create(config)
14
+ instrumentation = config[:instrumentation]
15
+ bucket_name = config[:bucket_name]
16
+ raise(InvalidConfigurationError, 'Histogram requires :bucket_name and :instrumentation') \
17
+ unless instrumentation && bucket_name
18
+ new(
19
+ instrumentation: instrumentation,
20
+ bucket_name: bucket_name
21
+ )
22
+ end
23
+
24
+ def initialize(instrumentation:, bucket_name:)
25
+ @metrics_prefix = @instrumentation = @registry = @histogram = nil
26
+ @buckets = fetch_bucket_size(bucket_name) || BUCKETS
27
+ setup_collector_module(instrumentation: instrumentation)
28
+ end
29
+
30
+ def fetch_bucket_size(bucket_name)
31
+ PhobosPrometheus.config.buckets.find { |bucket| bucket.name == bucket_name }.bins
32
+ end
33
+
34
+ def init_metrics(prometheus_label)
35
+ @histogram = @registry.histogram(
36
+ :"#{@metrics_prefix}_#{prometheus_label}_duration",
37
+ "The duration spent (in ms) consuming #{@instrumentation} events.",
38
+ {},
39
+ @buckets
40
+ )
41
+ end
42
+
43
+ def update_metrics(event_label, event)
44
+ @histogram.observe(event_label, event.duration)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhobosPrometheus
4
+ # Validate Counters
5
+ class CountersValidator
6
+ include Logger
7
+ COUNTER_INSTRUMENTATION_MISSING = 'Missing required key :instrumentation for counter'
8
+ COUNTER_INVALID_KEY = 'Invalid configuration option detected at counter level, ignoring'
9
+ COUNTER_KEYS = [:instrumentation].freeze
10
+
11
+ def initialize(counters)
12
+ @counters = counters
13
+ end
14
+
15
+ def validate
16
+ @counters.map do |counter|
17
+ validate_counter(counter)
18
+ end
19
+ end
20
+
21
+ def validate_counter(counter)
22
+ Helper.assert_required_key(counter, :instrumentation) || \
23
+ Helper.fail_config(COUNTER_INSTRUMENTATION_MISSING)
24
+ Helper.check_invalid_keys(COUNTER_KEYS, counter) || \
25
+ log_warn(COUNTER_INVALID_KEY)
26
+ end
27
+ end
28
+
29
+ # Validate Histograms
30
+ class HistogramsValidator
31
+ include Logger
32
+ HISTOGRAM_INSTRUMENTATION_MISSING = 'Missing required key :instrumentation for histogram'
33
+ HISTOGRAM_BUCKET_NAME_MISSING = 'Missing required key :bucket_name for histogram'
34
+ HISTOGRAM_INVALID_BUCKET = 'Invalid bucket reference specified for histogram'
35
+ HISTOGRAM_INVALID_KEY = 'Invalid configuration option detected at histogram level, ignoring'
36
+ HISTOGRAM_KEYS = [:instrumentation, :bucket_name].freeze
37
+
38
+ def initialize(histograms, buckets)
39
+ @histograms = histograms
40
+ @buckets = buckets
41
+ end
42
+
43
+ def validate
44
+ @histograms.map do |histogram|
45
+ validate_histogram(histogram)
46
+ end
47
+ end
48
+
49
+ def validate_histogram(histogram)
50
+ Helper.assert_required_key(histogram, :instrumentation) || \
51
+ Helper.fail_config(HISTOGRAM_INSTRUMENTATION_MISSING)
52
+ Helper.assert_required_key(histogram, :bucket_name) || \
53
+ Helper.fail_config(HISTOGRAM_BUCKET_NAME_MISSING)
54
+ assert_bucket_exists(histogram['bucket_name']) || Helper.fail_config(HISTOGRAM_INVALID_BUCKET)
55
+ Helper.check_invalid_keys(HISTOGRAM_KEYS, histogram) || \
56
+ log_warn(HISTOGRAM_INVALID_KEY)
57
+ end
58
+
59
+ def assert_bucket_exists(name)
60
+ @buckets.any? { |key| key.name == name }
61
+ end
62
+ end
63
+
64
+ # Validate buckets
65
+ class BucketsValidator
66
+ include Logger
67
+ BUCKET_NAME_MISSING = 'Missing required key :name for bucket'
68
+ BUCKET_BINS_MISSING = 'Missing required key :bins for bucket'
69
+ BUCKET_BINS_EMPTY = 'Bucket config bad, bins are empty'
70
+ BUCKET_INVALID_KEY = 'Invalid configuration option detected at bucket level, ignoring'
71
+ BUCKET_KEYS = [:name, :bins].freeze
72
+
73
+ def initialize(buckets)
74
+ @buckets = buckets
75
+ end
76
+
77
+ def validate
78
+ @buckets.map do |bucket|
79
+ validate_bucket(bucket)
80
+ end
81
+ end
82
+
83
+ def validate_bucket(bucket)
84
+ Helper.assert_required_key(bucket, :name) || Helper.fail_config(BUCKET_NAME_MISSING)
85
+ Helper.assert_required_key(bucket, :bins) || Helper.fail_config(BUCKET_BINS_MISSING)
86
+ Helper.assert_array_of_type(bucket, :bins, Integer) || Helper.fail_config(BUCKET_BINS_EMPTY)
87
+ Helper.check_invalid_keys(BUCKET_KEYS, bucket) || \
88
+ log_warn(BUCKET_INVALID_KEY)
89
+ end
90
+ end
91
+
92
+ # Validate gauges
93
+ class GaugesValidator
94
+ include Logger
95
+ GAUGE_LABEL_MISSING = 'Missing required key :label for gauge'
96
+ GAUGE_INCREMENT_MISSING = 'Missing required key :increment for gauge'
97
+ GAUGE_DECREMENT_MISSING = 'Missing required key :decrement for gauge'
98
+ GAUGE_INVALID_KEY = 'Invalid configuration option detected at gauge level, ignoring'
99
+ GAUGE_KEYS = [:label, :increment, :decrement].freeze
100
+
101
+ def initialize(gauges)
102
+ @gauges = gauges
103
+ end
104
+
105
+ def validate
106
+ @gauges.map do |gauge|
107
+ validate_gauge(gauge)
108
+ end
109
+ end
110
+
111
+ def validate_gauge(gauge)
112
+ Helper.assert_required_key(gauge, :label) || Helper.fail_config(GAUGE_LABEL_MISSING)
113
+ Helper.assert_required_key(gauge, :increment) || Helper.fail_config(GAUGE_INCREMENT_MISSING)
114
+ Helper.assert_required_key(gauge, :decrement) || Helper.fail_config(GAUGE_DECREMENT_MISSING)
115
+ Helper.check_invalid_keys(GAUGE_KEYS, gauge) || \
116
+ log_warn(GAUGE_INVALID_KEY)
117
+ end
118
+ end
119
+
120
+ # Helper for operations not dependent on instance state
121
+ module Helper
122
+ def self.read_config(path)
123
+ Phobos::DeepStruct.new(
124
+ YAML.safe_load(
125
+ ERB.new(
126
+ File.read(File.expand_path(path))
127
+ ).result
128
+ )
129
+ )
130
+ end
131
+
132
+ def self.assert_required_key(metric, required)
133
+ metric.keys.any? { |key| key.to_sym == required }
134
+ end
135
+
136
+ def self.assert_array_of_type(metric, key, type)
137
+ ary = metric[key.to_s]
138
+ ary.is_a?(Array) && \
139
+ ary.all? { |value| value.is_a? type }
140
+ end
141
+
142
+ def self.fail_config(message)
143
+ raise(InvalidConfigurationError, message)
144
+ end
145
+
146
+ def self.check_invalid_keys(keys, metric)
147
+ metric.keys.all? { |key| keys.include?(key.to_sym) }
148
+ end
149
+ end
150
+
151
+ # Config validates and parses configuration yml
152
+ class ConfigParser
153
+ include Logger
154
+ attr_reader :config
155
+
156
+ ROOT_MISSING_COLLECTORS = 'No histograms, gauges nor counters are configured. ' \
157
+ 'Metrics will not be recorded'
158
+ ROOT_INVALID_KEY = 'Invalid configuration option detected at root level, ignoring'
159
+ ROOT_KEYS = [:metrics_prefix, :counters, :histograms, :buckets, :gauges].freeze
160
+
161
+ def initialize(path)
162
+ @config = Helper.read_config(path)
163
+ validate_config
164
+ @config.counters = [] unless @config.counters
165
+ @config.histograms = [] unless @config.histograms
166
+ @config.gauges = [] unless @config.gauges
167
+ @config.freeze
168
+ end
169
+
170
+ def validate_config
171
+ validate_root
172
+ validate_counters
173
+ validate_histograms
174
+ validate_buckets
175
+ validate_gauges
176
+ end
177
+
178
+ def validate_root
179
+ assert_required_root_keys
180
+ Helper.check_invalid_keys(ROOT_KEYS, @config.to_h) || \
181
+ log_warn(ROOT_INVALID_KEY)
182
+ end
183
+
184
+ def validate_counters
185
+ CountersValidator.new(@config.to_h[:counters] || []).validate
186
+ end
187
+
188
+ def validate_histograms
189
+ HistogramsValidator.new(@config.to_h[:histograms] || [], @config.buckets).validate
190
+ end
191
+
192
+ def validate_buckets
193
+ BucketsValidator.new(@config.to_h[:buckets] || []).validate
194
+ end
195
+
196
+ def validate_gauges
197
+ GaugesValidator.new(@config.to_h[:gauges] || []).validate
198
+ end
199
+
200
+ def assert_required_root_keys
201
+ @config.counters || @config.histograms || @config.gauges || \
202
+ log_warn(ROOT_MISSING_COLLECTORS)
203
+ end
204
+ end
205
+ end