phobos_prometheus 0.3.2

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