diff --git a/lib/prometheus_exporter/ext/instrumentation/base_stats.rb b/lib/prometheus_exporter/ext/instrumentation/base_stats.rb index 33ed052..2b75c62 100644 --- a/lib/prometheus_exporter/ext/instrumentation/base_stats.rb +++ b/lib/prometheus_exporter/ext/instrumentation/base_stats.rb @@ -9,7 +9,7 @@ class << self end def initialize(client: PrometheusExporter::Client.default, metric_labels: {}) - @metric_labels = metric_labels + @metric_labels = metric_labels.transform_keys(&:to_sym) @client = client end @@ -25,16 +25,19 @@ def collect # @param data [Array,Hash] def collect_data(data) - metric = build_metric(data) - @client.send_json(metric) + data_list = data.is_a?(Array) ? data : [data] + metrics = data_list.map { |data_item| build_metric(data_item) } + metrics.map { |metric| @client.send_json(metric) } end # @param data [Hash] # @return [Hash] def build_metric(data) - metric = data.dup + metric = data.transform_keys(&:to_sym) metric[:type] = type - metric[:labels] = (metric[:labels] || {}).merge(@metric_labels) + metric[:labels] ||= {} + metric[:labels].transform_keys!(&:to_sym) + metric[:labels].merge!(@metric_labels) metric end end diff --git a/lib/prometheus_exporter/ext/instrumentation/periodic_stats.rb b/lib/prometheus_exporter/ext/instrumentation/periodic_stats.rb index ee4c15f..cba09a2 100644 --- a/lib/prometheus_exporter/ext/instrumentation/periodic_stats.rb +++ b/lib/prometheus_exporter/ext/instrumentation/periodic_stats.rb @@ -21,7 +21,6 @@ def start(frequency: 30, client: PrometheusExporter::Client.default, **) rescue StandardError => e client.logger.error("#{klass} Prometheus Exporter Failed To Collect Stats") client.logger.error("#{e.class} #{e.backtrace&.join("\n")}") - @on_exception&.call(e) if defined?(@on_exception) ensure sleep frequency end @@ -44,12 +43,6 @@ def stop end @thread = nil end - - # Adds handler that will be called when exception is raised in the thread. - # @yieldparam exception [Exception] - def on_exception(&block) - @on_exception = block - end end end end diff --git a/lib/prometheus_exporter/ext/metric/gauge_with_time.rb b/lib/prometheus_exporter/ext/metric/gauge_with_time.rb deleted file mode 100644 index 4c931bb..0000000 --- a/lib/prometheus_exporter/ext/metric/gauge_with_time.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require 'prometheus_exporter/metric' -require 'date' - -module PrometheusExporter::Ext::Metric - class GaugeWithTime < PrometheusExporter::Metric::Gauge - attr_reader :timestamps - - def reset! - @data = {} - @timestamps = {} - end - - def metric_text - @data.map do |labels, value| - "#{prefix(@name)}#{labels_text(labels)} #{value} #{timestamps[labels]}" - end.join("\n") - end - - def remove(labels) - result = super - remove_timestamp(labels) - result - end - - def observe(value, labels = {}) - result = super - value.nil? ? remove_timestamp(labels) : update_timestamp(labels) - result - end - - def increment(labels = {}, value = 1) - result = super - update_timestamp(labels) - result - end - - def decrement(labels = {}, value = 1) - result = super - update_timestamp(labels) - result - end - - def to_h - super.to_h { |labels, value| [labels, [value, timestamps[labels]]] } - end - - private - - def update_timestamp(labels) - timestamps[labels] = DateTime.now.strftime('%Q').to_i - end - - def remove_timestamp(labels) - timestamps.delete(labels) - end - end -end diff --git a/lib/prometheus_exporter/ext/metric/type_registry.rb b/lib/prometheus_exporter/ext/metric/type_registry.rb deleted file mode 100644 index aaa03d5..0000000 --- a/lib/prometheus_exporter/ext/metric/type_registry.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'prometheus_exporter/metric' -require_relative 'gauge_with_time' - -module PrometheusExporter::Ext::Metric - module TypeRegistry - class << self - attr_accessor :types - end - - self.types = {} - - module_function - - def register(type, klass) - types[type.to_s] = klass - end - - def find_metric_class(type) - klass = types[type.to_s] - raise ArgumentError, "Unknown metric type #{type}" unless klass - - klass - end - - register :gauge_with_time, PrometheusExporter::Ext::Metric::GaugeWithTime - register :counter, PrometheusExporter::Metric::Counter - register :gauge, PrometheusExporter::Metric::Gauge - register :summary, PrometheusExporter::Metric::Summary - register :histogram, PrometheusExporter::Metric::Histogram - end -end diff --git a/lib/prometheus_exporter/ext/rspec/matchers.rb b/lib/prometheus_exporter/ext/rspec/matchers.rb index 0e0adaa..b5d1341 100644 --- a/lib/prometheus_exporter/ext/rspec/matchers.rb +++ b/lib/prometheus_exporter/ext/rspec/matchers.rb @@ -6,7 +6,8 @@ module PrometheusExporter::Ext::RSpec module Matchers - def send_metrics(expected = nil) + def send_metrics(*expected) + expected = nil if expected.empty? PrometheusExporter::Ext::RSpec::SendMetricsMatcher.new(expected) end @@ -14,22 +15,10 @@ def a_prometheus_metric(klass, name) MetricMatcher.new(klass, name) end - # Matches approximate milliseconds since epoch - # @param date_time [DateTime] default DateTime.now - # @param delta [Integer] default 1000 ms - # @return [RSpec::Matchers::BuiltIn::BeWithin] - def ms_since_epoch(date_time: DateTime.now, delta: 1_000) - be_within(delta).of(date_time.strftime('%Q').to_i) - end - def a_gauge_metric(name) a_prometheus_metric(PrometheusExporter::Metric::Gauge, name) end - def a_gauge_with_time_metric(name) - a_prometheus_metric(PrometheusExporter::Ext::Metric::GaugeWithTime, name) - end - def a_counter_metric(name) a_prometheus_metric(PrometheusExporter::Metric::Counter, name) end diff --git a/lib/prometheus_exporter/ext/rspec/send_metrics_matcher.rb b/lib/prometheus_exporter/ext/rspec/send_metrics_matcher.rb index e84348a..a84f200 100644 --- a/lib/prometheus_exporter/ext/rspec/send_metrics_matcher.rb +++ b/lib/prometheus_exporter/ext/rspec/send_metrics_matcher.rb @@ -78,9 +78,7 @@ def description_of(object) private def deep_stringify_keys(hash) - hash.transform_keys(&:to_s).transform_values do |value| - value.is_a?(Hash) ? deep_stringify_keys(value) : value - end + JSON.parse JSON.generate(hash) end end end diff --git a/lib/prometheus_exporter/ext/server/base_collector_methods.rb b/lib/prometheus_exporter/ext/server/base_collector_methods.rb new file mode 100644 index 0000000..9e54e7d --- /dev/null +++ b/lib/prometheus_exporter/ext/server/base_collector_methods.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'prometheus_exporter/metric' + +module PrometheusExporter::Ext::Server + module BaseCollectorMethods + class << self + private + + def included(klass) + super + klass.singleton_class.attr_accessor :type, :registered_metrics + klass.registered_metrics = {} + klass.extend ClassMethods + end + end + + module ClassMethods + # Registers metric observer. + # @param name [Symbol] metric name. + # @param help [String] metric description. + # @param metric_class [Class] observer class. + # @param args [Array] additional arguments for observer class. + # rubocop:disable Metrics/ParameterLists + def register_metric(name, help, metric_class, *args) + # rubocop:enable Metrics/ParameterLists + name = name.to_s + raise ArgumentError, "metric #{name} is already registered" if registered_metrics.key?(name) + + registered_metrics[name] = { help:, metric_class:, args: } + end + + # Registers PrometheusExporter::Metric::Counter observer. + # @param name [Symbol] metric name. + # @param help [String] metric description. + def register_counter(name, help) + register_metric(name, help, PrometheusExporter::Metric::Counter) + end + + # Registers PrometheusExporter::Metric::Gauge observer. + # @param name [Symbol] metric name. + # @param help [String] metric description. + def register_gauge(name, help) + register_metric(name, help, PrometheusExporter::Metric::Gauge) + end + + # Registers PrometheusExporter::Metric::Summary observer. + # @param name [Symbol] metric name. + # @param help [String] metric description. + # @param opts [Hash] additional options, supports `quantiles` key. + def register_summary(name, help, opts = {}) + register_metric(name, help, PrometheusExporter::Metric::Summary, opts) + end + + # Registers PrometheusExporter::Metric::Histogram observer. + # @param name [Symbol] metric name. + # @param help [String] metric description. + # @param opts [Hash] additional options, supports `buckets` key. + def register_histogram(name, help, opts = {}) + register_metric(name, help, PrometheusExporter::Metric::Histogram, opts) + end + end + + # @return [String] + def type + self.class.type + end + + private + + # Adds metrics to observers with matched name. + # @param observers [Hash] returned by #build_observers. + # @param obj [Hash] metric data. + def fill_observers(observers, obj) + observers.each do |name, observer| + value = obj[name] + observer.observe(value, obj['labels']) if value + end + end + + # Generally metrics sent via PrometheusExporter::Ext::Instrumentation::BaseStats populate labels to `labels` key. + # But PrometheusExporter::Client populate it's own labels to `custom_labels` key. + # Here we merge them into single `labels` key. + # @param obj [Hash] + # @return [Hash] + def normalize_labels(obj) + obj['labels'] ||= {} + custom_labels = obj.delete('custom_labels') + obj['labels'].merge!(custom_labels) if custom_labels + obj + end + + # @return [Hash] key is metric name, value is observer. + def build_observers + observers = {} + self.class.registered_metrics.each do |name, metric| + observers[name] = metric[:metric_class].new("#{type}_#{name}", metric[:help], *metric[:args]) + end + observers + end + end +end diff --git a/lib/prometheus_exporter/ext/server/expired_stats_collector.rb b/lib/prometheus_exporter/ext/server/expired_stats_collector.rb new file mode 100644 index 0000000..bcd59ea --- /dev/null +++ b/lib/prometheus_exporter/ext/server/expired_stats_collector.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require_relative 'base_collector_methods' +require 'prometheus_exporter/server/metrics_container' + +module PrometheusExporter::Ext::Server + module ExpiredStatsCollector + class << self + private + + def included(klass) + super + klass.include BaseCollectorMethods + klass.singleton_class.attr_accessor :filter, :ttl + klass.ttl = 60 + klass.extend ClassMethods + end + end + + module ClassMethods + # Defines a rule how old metric will be replaced with new one. + # @yield compare new metric with existing one. + # @yieldparam new_metric [Hash] new metric data. + # @yieldparam metric [Hash] existing metric data. + # @yieldreturn [Boolean] if true existing metric will be replaced with new one. + def unique_metric_by(&block) + @filter = block + end + end + + def initialize + super + @data = PrometheusExporter::Server::MetricsContainer.new( + ttl: self.class.ttl, + filter: self.class.filter + ) + end + + # Returns all metrics collected by this collector. + # @return [Array] + def metrics + observers = build_observers + @data.each do |obj| + fill_observers(observers, obj) + end + + observers.values + end + + # Collects metric data received from client. + # @param obj [Hash] metric data. + def collect(obj) + normalize_labels(obj) + @data << obj + end + end +end diff --git a/lib/prometheus_exporter/ext/server/stats_collector.rb b/lib/prometheus_exporter/ext/server/stats_collector.rb index a114be1..30438f7 100644 --- a/lib/prometheus_exporter/ext/server/stats_collector.rb +++ b/lib/prometheus_exporter/ext/server/stats_collector.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative '../metric/type_registry' +require_relative 'base_collector_methods' module PrometheusExporter::Ext::Server module StatsCollector @@ -9,67 +9,26 @@ class << self def included(klass) super - klass.singleton_class.attr_accessor :type, :registered_metrics - klass.registered_metrics = {} - klass.extend ClassMethods - klass.include InstanceMethods + klass.include BaseCollectorMethods end end - module ClassMethods - # rubocop:disable Metrics/ParameterLists - def register_metric(name, type, help, *args) - # rubocop:enable Metrics/ParameterLists - name = name.to_s - raise ArgumentError, "metric #{name} is already registered" if registered_metrics.key?(name) - - metric_class = PrometheusExporter::Ext::Metric::TypeRegistry.find_metric_class(type) - registered_metrics[name] = { help:, metric_class:, args: } - end + def initialize + super + @observers = build_observers end - module InstanceMethods - def initialize - super - build_observers - end - - def type - self.class.type - end - - def metrics - @observers.values - end - - def collect(obj) - labels = build_labels(obj) - fill_observers(obj, labels) - end - - private - - def fill_observers(obj, labels) - @observers.each do |name, observer| - value = obj[name] - observer.observe(value, labels) if value - end - end - - def build_labels(obj) - labels = {} - labels.merge!(obj['labels']) if obj['labels'] - labels.merge!(obj['custom_labels']) if obj['custom_labels'] - - labels - end + # Returns all metrics collected by this collector. + # @return [Array] + def metrics + @observers.values + end - def build_observers - @observers = self.class.registered_metrics.to_h do |name, metric| - observer = metric[:metric_class].new("#{type}_#{name}", metric[:help], *metric[:args]) - [name, observer] - end - end + # Collects metric data received from client. + # @param obj [Hash] metric data. + def collect(obj) + obj = normalize_labels(obj) + fill_observers(@observers, obj) end end end diff --git a/lib/prometheus_exporter/ext/version.rb b/lib/prometheus_exporter/ext/version.rb index 7efbf43..d75bcba 100644 --- a/lib/prometheus_exporter/ext/version.rb +++ b/lib/prometheus_exporter/ext/version.rb @@ -2,6 +2,6 @@ module PrometheusExporter module Ext - VERSION = '0.1.0' + VERSION = '0.2.0' end end diff --git a/spec/prometheus_exporter/ext/instrumentation/base_stats_spec.rb b/spec/prometheus_exporter/ext/instrumentation/base_stats_spec.rb index 18f0bf9..5c463f5 100644 --- a/spec/prometheus_exporter/ext/instrumentation/base_stats_spec.rb +++ b/spec/prometheus_exporter/ext/instrumentation/base_stats_spec.rb @@ -23,20 +23,18 @@ it 'sends correct metrics' do expect { subject }.to send_metrics( - [ - { - type: 'test', - foo: 123, - bar: 456, - labels: { qwe: 'asd' } - }, - { - type: 'test', - foo: 124, - bar: 457, - labels: { qwe: 'zxc' } - } - ] + { + type: 'test', + foo: 123, + bar: 456, + labels: { qwe: 'asd' } + }, + { + type: 'test', + foo: 124, + bar: 457, + labels: { qwe: 'zxc' } + } ) end @@ -50,20 +48,18 @@ it 'sends correct metrics' do expect { subject }.to send_metrics( - [ - { - type: 'test', - foo: 123, - bar: 456, - labels: {} - }, - { - type: 'test', - foo: 124, - bar: 457, - labels: {} - } - ] + { + type: 'test', + foo: 123, + bar: 456, + labels: {} + }, + { + type: 'test', + foo: 124, + bar: 457, + labels: {} + } ) end end @@ -75,20 +71,18 @@ it 'sends correct metrics' do expect { subject }.to send_metrics( - [ - { - type: 'test', - foo: 123, - bar: 456, - labels: { qwe: 'asd' } - }, - { - type: 'test', - foo: 124, - bar: 457, - labels: { qwe: 'zxc' } - } - ] + { + type: 'test', + foo: 123, + bar: 456, + labels: { qwe: 'asd' } + }, + { + type: 'test', + foo: 124, + bar: 457, + labels: { qwe: 'zxc' } + } ) end end @@ -99,20 +93,18 @@ it 'sends correct metrics' do expect { subject }.to send_metrics( - [ - { - type: 'test', - foo: 123, - bar: 456, - labels: { qwe: 'asd', host: 'example.com' } - }, - { - type: 'test', - foo: 124, - bar: 457, - labels: { qwe: 'zxc', host: 'example.com' } - } - ] + { + type: 'test', + foo: 123, + bar: 456, + labels: { qwe: 'asd', host: 'example.com' } + }, + { + type: 'test', + foo: 124, + bar: 457, + labels: { qwe: 'zxc', host: 'example.com' } + } ) end end diff --git a/spec/prometheus_exporter/ext/instrumentation/periodic_stats_spec.rb b/spec/prometheus_exporter/ext/instrumentation/periodic_stats_spec.rb new file mode 100644 index 0000000..296e3ab --- /dev/null +++ b/spec/prometheus_exporter/ext/instrumentation/periodic_stats_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +RSpec.describe PrometheusExporter::Ext::Instrumentation::PeriodicStats do + describe '.start' do + subject do + PeriodicTestInstrumentation.start(frequency:) + sleep(wait_seconds) + end + + let(:frequency) { 2 } + let(:wait_seconds) { 0.1 } # can't be 0 because thread requires few milliseconds to start + + after do + PeriodicTestInstrumentation.stop + PeriodicTestInstrumentation.test_counter = nil + end + + it 'sends 1 metric' do + expect { subject }.to send_metrics( + { + keepalive: 1, + type: 'test', + labels: { foo: 'bar' } + } + ) + end + + context 'when wait for frequency seconds more' do + let(:wait_seconds) { super() + frequency } # 2.1 + + it 'sends 2 metrics' do + expect { subject }.to send_metrics( + { + keepalive: 1, + type: 'test', + labels: { foo: 'bar' } + }, + { + keepalive: 2, + type: 'test', + labels: { foo: 'bar' } + } + ).ordered + end + end + + context 'when wait for frequency+1 seconds more' do + let(:wait_seconds) { super() + frequency + 1 } # 3.1 + + it 'sends 2 metrics' do + expect { subject }.to send_metrics( + { + keepalive: 1, + type: 'test', + labels: { foo: 'bar' } + }, + { + keepalive: 2, + type: 'test', + labels: { foo: 'bar' } + } + ).ordered + end + end + end +end diff --git a/spec/prometheus_exporter/ext/metric/gauge_with_time_spec.rb b/spec/prometheus_exporter/ext/metric/gauge_with_time_spec.rb deleted file mode 100644 index ac12690..0000000 --- a/spec/prometheus_exporter/ext/metric/gauge_with_time_spec.rb +++ /dev/null @@ -1,98 +0,0 @@ -# frozen_string_literal: true - -require 'prometheus_exporter/ext/metric/gauge_with_time' - -RSpec.describe PrometheusExporter::Ext::Metric::GaugeWithTime do - describe '#to_prometheus_text' do - subject { metric.to_prometheus_text } - - let(:metric) { described_class.new('metric_name', 'help_msg') } - - it 'returns correct text' do - expect(subject).to eq("# HELP metric_name help_msg\n# TYPE metric_name gauge\n\n") - end - - context 'with metric_text' do - let(:metric_text) { "metric_text 1\nmetric_text 2" } - - before { allow(metric).to receive(:metric_text).once.and_return(metric_text) } - - it 'returns correct text' do - title = "# HELP metric_name help_msg\n# TYPE metric_name gauge" - expect(subject).to eq("#{title}\n#{metric_text}\n") - end - end - end - - describe '#metric_text' do - subject { metric.metric_text.split("\n") } - - let(:metric) { described_class.new('metric_name', 'help_msg') } - - it 'returns correct text' do - expect(subject).to eq([]) - end - - it 'has correct to_h' do - expect(metric.to_h).to eq({}) - end - - context 'when metric has data with stubbed timestamps' do - let!(:date_time_old) { DateTime.now - 25 } - let!(:date_time_new) { DateTime.now } - - before do - allow(DateTime).to receive(:now).once.and_return(date_time_old) - metric.observe(1, 'foo' => 'bar') - - allow(DateTime).to receive(:now).once.and_return(date_time_new) - metric.observe(2, 'baz' => 'boo') - end - - it 'returns correct text' do - expect(subject).to contain_exactly( - %(metric_name{foo="bar"} 1 #{date_time_old.strftime('%Q').to_i}), - %(metric_name{baz="boo"} 2 #{date_time_new.strftime('%Q').to_i}) - ) - end - - it 'has correct to_h' do - expect(metric.to_h).to match( - { - { 'foo' => 'bar' } => [1, date_time_old.strftime('%Q').to_i], - { 'baz' => 'boo' } => [2, date_time_new.strftime('%Q').to_i] - } - ) - end - - it 'matches a_gauge_with_time_metric' do - expect(metric).to a_gauge_with_time_metric('metric_name') - .with([1, date_time_old.strftime('%Q').to_i], foo: 'bar') - .with([2, date_time_new.strftime('%Q').to_i], baz: 'boo') - end - end - - context 'when metric has data without stubbed timestamps' do - before do - metric.observe(1, 'foo' => 'bar') - sleep 0.2 - metric.observe(2, baz: 'boo') - end - - it 'has correct to_h' do - expect(metric.to_h).to match( - { - { 'foo' => 'bar' } => [1, ms_since_epoch], - { baz: 'boo' } => [2, ms_since_epoch] - } - ) - end - - it 'matches a_gauge_with_time_metric' do - expect(metric).to a_gauge_with_time_metric('metric_name') - .with([1, ms_since_epoch], foo: 'bar') - .with([2, ms_since_epoch], baz: 'boo') - end - end - end -end diff --git a/spec/prometheus_exporter/ext/server/expired_stats_collector_spec.rb b/spec/prometheus_exporter/ext/server/expired_stats_collector_spec.rb new file mode 100644 index 0000000..8e43fc7 --- /dev/null +++ b/spec/prometheus_exporter/ext/server/expired_stats_collector_spec.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +RSpec.describe PrometheusExporter::Ext::Server::ExpiredStatsCollector do + subject do + collector.collect(deep_stringify_keys(metric)) + end + + let(:collector) { TestExpiredCollector.new } + let(:metric) do + { + type: 'test', + labels: { qwe: 'asd' }, + g_metric: 1.23, + gwt_metric: 4.56, + c_metric: 7.89 + } + end + let(:expected_labels) { metric[:labels] } + + it 'observes prometheus metrics' do + subject + expect(collector.metrics).to contain_exactly( + a_gauge_metric('test_g_metric').with(1.23, expected_labels), + a_counter_metric('test_c_metric').with(7.89, expected_labels) + ) + end + + context 'with empty custom_labels' do + let(:metric) do + super().merge custom_labels: {} + end + + it 'observes prometheus metrics' do + subject + expect(collector.metrics).to contain_exactly( + a_gauge_metric('test_g_metric').with(1.23, expected_labels), + a_counter_metric('test_c_metric').with(7.89, expected_labels) + ) + end + end + + context 'with filled custom_labels' do + let(:metric) do + super().merge custom_labels: { host: 'example.com' } + end + let(:expected_labels) { metric[:labels].merge metric[:custom_labels] } + + it 'observes prometheus metrics' do + subject + expect(collector.metrics).to contain_exactly( + a_gauge_metric('test_g_metric').with(1.23, expected_labels), + a_counter_metric('test_c_metric').with(7.89, expected_labels) + ) + end + end + + context 'when collector has previous metrics with same labels' do + let(:prev_metric) do + { + type: 'test', + labels: { qwe: 'asd' }, + g_metric: 10, + gwt_metric: 20, + c_metric: 30 + } + end + + before do + collector.collect(deep_stringify_keys(prev_metric)) + end + + it 'observes prometheus metrics' do + subject + expect(collector.metrics).to contain_exactly( + a_gauge_metric('test_g_metric').with(1.23, expected_labels), + a_counter_metric('test_c_metric').with(7.89, expected_labels) # was replaced, not incremented + ) + end + end + + context 'when collector has previous metrics with different labels' do + let(:prev_stringified_metric) { JSON.parse JSON.generate(prev_metric) } + let(:prev_metric) do + { + type: 'test', + labels: { qwe: 'asd2' }, + g_metric: 10, + gwt_metric: 20, + c_metric: 30 + } + end + let(:prev_expected_labels) { prev_metric[:labels] } + + before do + collector.collect(prev_stringified_metric) + end + + it 'observes prometheus metrics' do + subject + expect(collector.metrics).to contain_exactly( + a_gauge_metric('test_g_metric') + .with(10, prev_expected_labels) + .with(1.23, expected_labels), + a_counter_metric('test_c_metric') + .with(30, prev_expected_labels) + .with(7.89, expected_labels) + ) + end + + context 'when previous metrics are expired' do + before do + sleep_seconds = collector.class.ttl + 0.1 + sleep(sleep_seconds) + end + + it 'observes prometheus metrics' do + subject + expect(collector.metrics).to contain_exactly( + a_gauge_metric('test_g_metric').with(1.23, expected_labels), + a_counter_metric('test_c_metric').with(7.89, expected_labels) + ) + end + end + end +end diff --git a/spec/prometheus_exporter/ext/server/stats_collector_spec.rb b/spec/prometheus_exporter/ext/server/stats_collector_spec.rb index d82482a..73444a9 100644 --- a/spec/prometheus_exporter/ext/server/stats_collector_spec.rb +++ b/spec/prometheus_exporter/ext/server/stats_collector_spec.rb @@ -5,14 +5,13 @@ collector.collect(stringified_metric) end - let(:collector) { TestCollector.new } + let(:collector) { TestStatsCollector.new } let(:stringified_metric) { JSON.parse JSON.generate(metric) } let(:metric) do { type: 'test', labels: { qwe: 'asd' }, g_metric: 1.23, - gwt_metric: 4.56, c_metric: 7.89 } end @@ -22,7 +21,6 @@ subject expect(collector.metrics).to contain_exactly( a_gauge_metric('test_g_metric').with(1.23, expected_labels), - a_gauge_with_time_metric('test_gwt_metric').with([4.56, ms_since_epoch], expected_labels), a_counter_metric('test_c_metric').with(7.89, expected_labels) ) end @@ -36,9 +34,6 @@ subject expect(collector.metrics).to contain_exactly( a_gauge_metric('test_g_metric').with(1.23, expected_labels), - a_gauge_with_time_metric('test_gwt_metric').with([4.56, ms_since_epoch], - expected_labels - ), a_counter_metric('test_c_metric').with(7.89, expected_labels) ) end @@ -54,7 +49,6 @@ subject expect(collector.metrics).to contain_exactly( a_gauge_metric('test_g_metric').with(1.23, expected_labels), - a_gauge_with_time_metric('test_gwt_metric').with([4.56, ms_since_epoch], expected_labels), a_counter_metric('test_c_metric').with(7.89, expected_labels) ) end @@ -80,9 +74,6 @@ subject expect(collector.metrics).to contain_exactly( a_gauge_metric('test_g_metric').with(1.23, expected_labels), - a_gauge_with_time_metric('test_gwt_metric').with([4.56, ms_since_epoch], - expected_labels - ), a_counter_metric('test_c_metric').with(37.89, expected_labels) ) end @@ -111,9 +102,6 @@ a_gauge_metric('test_g_metric') .with(10, prev_expected_labels) .with(1.23, expected_labels), - a_gauge_with_time_metric('test_gwt_metric') - .with([20, ms_since_epoch], prev_expected_labels) - .with([4.56, ms_since_epoch], expected_labels), a_counter_metric('test_c_metric') .with(30, prev_expected_labels) .with(7.89, expected_labels) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ffe23ff..6c9e026 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,7 +5,10 @@ require 'prometheus_exporter/ext' require 'prometheus_exporter/ext/rspec' require 'prometheus_exporter/ext/instrumentation/base_stats' +require 'prometheus_exporter/ext/instrumentation/periodic_stats' require 'prometheus_exporter/ext/server/stats_collector' +require 'prometheus_exporter/ext/server/expired_stats_collector' +require_relative 'support/rspec_test_helpers' RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = nil @@ -19,13 +22,38 @@ def collect(data_list) end end -class TestCollector < PrometheusExporter::Server::TypeCollector +class PeriodicTestInstrumentation < PrometheusExporter::Ext::Instrumentation::PeriodicStats + self.type = 'test' + class << self + attr_accessor :test_counter + end + + def collect + self.class.test_counter ||= 0 + self.class.test_counter += 1 + collect_data(keepalive: self.class.test_counter, labels: { foo: 'bar' }) + end +end + +class TestStatsCollector < PrometheusExporter::Server::TypeCollector include PrometheusExporter::Ext::Server::StatsCollector self.type = 'test' - register_metric :g_metric, :gauge, 'test gauge metric' - register_metric :gwt_metric, :gauge_with_time, 'test gauge with time metric' - register_metric :c_metric, :counter, 'test counter metric' + register_gauge :g_metric, 'test gauge metric' + register_counter :c_metric, 'test counter metric' +end + +class TestExpiredCollector < PrometheusExporter::Server::TypeCollector + include PrometheusExporter::Ext::Server::ExpiredStatsCollector + self.type = 'test' + self.ttl = 2 + + unique_metric_by do |new_metric, metric| + metric['labels'] == new_metric['labels'] + end + + register_gauge :g_metric, 'test gauge metric' + register_counter :c_metric, 'test counter metric' end RSpec.configure do |config| @@ -38,4 +66,6 @@ class TestCollector < PrometheusExporter::Server::TypeCollector config.expect_with :rspec do |c| c.syntax = :expect end + + config.include RSpecTestHelpers end diff --git a/spec/support/rspec_test_helpers.rb b/spec/support/rspec_test_helpers.rb new file mode 100644 index 0000000..78ccb3a --- /dev/null +++ b/spec/support/rspec_test_helpers.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module RSpecTestHelpers + # @param data [Hash] + # @return [Hash] + def deep_stringify_keys(data) + JSON.parse JSON.generate(data) + end +end