From e6b34c93cea1717f1ee3ddca2e0ad1ff11649f1a Mon Sep 17 00:00:00 2001 From: Denis Talakevich Date: Wed, 6 Dec 2023 11:39:16 +0200 Subject: [PATCH] add gauge with expire --- CHANGELOG.md | 1 + README.md | 87 +++++++- .../ext/metric/gauge_with_expire.rb | 116 ++++++++++ lib/prometheus_exporter/ext/rspec/matchers.rb | 4 + .../ext/rspec/metric_matcher.rb | 5 + .../ext/rspec/send_metrics_matcher.rb | 4 +- .../ext/server/base_collector_methods.rb | 1 + .../ext/server/expired_stats_collector.rb | 2 +- .../ext/server/stats_collector.rb | 8 + .../server/expired_stats_collector_spec.rb | 206 ++++++++++-------- .../ext/server/stats_collector_spec.rb | 155 +++++++++++-- spec/spec_helper.rb | 6 +- 12 files changed, 463 insertions(+), 132 deletions(-) create mode 100644 lib/prometheus_exporter/ext/metric/gauge_with_expire.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 8259051..da372ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ## [Unreleased] +- add gauge_with_expire ## [0.2.0] - 2023-12-06 - remove gauge_with_time, add expired_stats_collector, add tests diff --git a/README.md b/README.md index 5fa51e9..a09c205 100644 --- a/README.md +++ b/README.md @@ -68,15 +68,41 @@ module Prometheus include ::PrometheusExporter::Ext::Server::StatsCollector self.type = 'my' - # The :gauge_with_time will allow you to write timestamp to gauge metric when value were observer. - # It will replace data with same labels and recalculate timestamp. - register_metric :last_duration_seconds, :gauge_with_time, 'duration of last operation execution' - register_metric :task_duration_seconds_sum, :counter, 'sum of operation execution durations' - register_metric :task_duration_seconds_count, :counter, 'sum of operation execution runs' + # The `register_gauge_with_expire` will remove or zero expired metric. + # when no :strategy option passed, default is `:removing`, available options are `:removing, :zeroing`. + # when no :ttl option passed, default is 60, any numeric greater than 0 can be used. + register_gauge_with_expire :last_duration_seconds, 'duration of last operation execution', ttl: 300 + + register_counter :task_duration_seconds_sum, 'sum of operation execution durations' + register_counter :task_duration_seconds_count, 'sum of operation execution runs' end end ``` +as alternative you can use `ExpiredStatsCollector` if you want all metric data to be removed after expiration +```ruby +require 'prometheus_exporter/ext' +require 'prometheus_exporter/ext/server/stats_collector' + +module Prometheus + class MyCollector < ::PrometheusExporter::Server::TypeCollector + include ::PrometheusExporter::Ext::Server::ExpiredStatsCollector + self.type = 'my' + self.ttl = 300 # default 60 + + # Optionally you can expire old_metric when specific new metric is collected. + # If this block returns true then old_metric will be removed. + unique_metric_by do |new_metric, old_metric| + new_metric['labels'] == old_metric['labels'] + end + + register_gauge :last_duration_seconds, 'duration of last operation execution' + register_counter :task_duration_seconds_sum, 'sum of operation execution durations' + register_counter :task_duration_seconds_count, 'sum of operation execution runs' + end +end +```` + ### When metrics should be send periodically with given frequency create instrumentation ```ruby @@ -126,15 +152,32 @@ module Prometheus class MyCollector < ::PrometheusExporter::Server::TypeCollector include ::PrometheusExporter::Ext::Server::StatsCollector self.type = 'my' - - # The :gauge_with_time will allow you to write timestamp to gauge metric when value were observer. - # It will replace data with same labels and recalculate timestamp. - register_metric :last_processed_duration, :gauge_with_time, 'duration of last processed record' + + # Default ttl 60, default strategy `:removing`. + register_gauge_with_expire :last_processed_duration, 'duration of last processed record' register_metric :processed_count, :gauge_with_time, 'count of processed records' end end ``` +as alternative you can use `ExpiredStatsCollector` if you want all metric data to be removed after expiration +```ruby +require 'prometheus_exporter/ext' +require 'prometheus_exporter/ext/server/stats_collector' + +module Prometheus + class MyCollector < ::PrometheusExporter::Server::TypeCollector + include ::PrometheusExporter::Ext::Server::ExpiredStatsCollector + self.type = 'my' + # By default ttl is 60 + # By default deletes old metrics only when it's expired + + register_gauge :last_processed_duration, 'duration of last processed record' + register_metric :processed_count, :gauge_with_time, 'count of processed records' + end +end +```` + ### You also can easily test your instrumentations and collectors using new matchers instrumentation test @@ -168,7 +211,7 @@ collector test RSpec.describe Prometheus::MyCollector do describe '#collect' do subject do - collector.collect(metric.deep_stringify_keys) + collector.metrics end let(:collector) { described_class.new } @@ -178,19 +221,39 @@ RSpec.describe Prometheus::MyCollector do metric_labels: {}, labels: { operation_name: 'test' }, last_duration_seconds: 1.2, - duration_seconds_sum: 3,4, + duration_seconds_sum: 3.4, duration_seconds_count: 1 } end + + let(:collect_data) do + collector.collect(metric.deep_stringify_keys) + end it 'observes prometheus metrics' do subject expect(collector.metrics).to contain_exactly( - a_gauge_with_time_metric('my_last_duration_seconds').with([1.2, ms_since_epoch], metric[:labels]), + a_gauge_with_expire_metric('my_last_duration_seconds').with(1.2, metric[:labels]), a_counter_metric('my_duration_seconds_sum').with(3.4, metric[:labels]), a_counter_metric('my_duration_seconds_count').with(1, metric[:labels]) ) end + + context 'when collected data is expired' do + let(:collect_data) do + super() + sleep 60.1 # when gauge_with_expire ttl is 60 + end + + it 'observes empty prometheus metrics' do + subject + expect(collector.metrics).to contain_exactly( + a_gauge_with_expire_metric('my_last_duration_seconds').empty, + a_counter_metric('my_duration_seconds_sum').with(3.4, metric[:labels]), + a_counter_metric('my_duration_seconds_count').with(1, metric[:labels]) + ) + end + end end end ``` diff --git a/lib/prometheus_exporter/ext/metric/gauge_with_expire.rb b/lib/prometheus_exporter/ext/metric/gauge_with_expire.rb new file mode 100644 index 0000000..efa92a5 --- /dev/null +++ b/lib/prometheus_exporter/ext/metric/gauge_with_expire.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require 'prometheus_exporter/metric' + +module PrometheusExporter::Ext::Metric + class GaugeWithExpire < PrometheusExporter::Metric::Gauge + NULLIFY_STRATEGY_UPDATE = ->(labels) { expiration_times[labels] = now_time + ttl }.freeze + NULLIFY_STRATEGY_EXPIRE = ->(labels) { data.delete(labels) }.freeze + ZEROING_STRATEGY_UPDATE = ->(labels) do + if data[labels].zero? + expiration_times.delete(labels) + else + expiration_times[labels] = now_time + ttl + end + end.freeze + ZEROING_STRATEGY_EXPIRE = ->(labels) { data[labels] = 0 }.freeze + + class << self + # @return [Hash] + # key - strategy name + # value [Hash] with keys: :on_update, :on_expire + # :on_update [Proc] yieldparam labels [Hash] - updates expiration_times after data was updated (instance exec) + # :on_expire [Proc] yieldparam labels [Hash] - updates data after expiration_times was expired (instance exec) + def strategies + { + removing: { on_update: NULLIFY_STRATEGY_UPDATE, on_expire: NULLIFY_STRATEGY_EXPIRE }, + zeroing: { on_update: ZEROING_STRATEGY_UPDATE, on_expire: ZEROING_STRATEGY_EXPIRE } + } + end + + def default_ttl + 60 + end + end + + attr_reader :ttl, :expiration_times + + def initialize(name, help, opts = {}) + super(name, help) + @ttl = opts[:ttl] || self.class.default_ttl + raise ArgumentError, ':ttl must be numeric' unless ttl.is_a?(Numeric) + raise ArgumentError, ":ttl must be greater than zero: #{ttl.inspect}" unless ttl.positive? + + @strategy = self.class.strategies.fetch(opts[:strategy] || :removing) do + raise ArgumentError, "Unknown strategy: #{opts[:strategy].inspect}" + end + end + + def reset! + @expiration_times = {} + super + end + + def metric_text + expire + super + end + + def remove(labels) + result = super + remove_expired_at(labels) + result + end + + def observe(value, labels = {}) + result = super + value.nil? ? remove_expired_at(labels) : update_expired_at(labels) + result + end + + def increment(labels = {}, value = 1) + result = super + update_expired_at(labels) + result + end + + def decrement(labels = {}, value = 1) + result = super + update_expired_at(labels) + result + end + + def expire + now = now_time + expiration_times.each do |labels, expired_at| + if expired_at < now + remove_data_when_expired(labels) + remove_expired_at(labels) + end + end + end + + def to_h + expire + super + end + + private + + def remove_data_when_expired(labels) + instance_exec(labels, &@strategy[:on_expire]) + end + + def update_expired_at(labels) + instance_exec(labels, &@strategy[:on_update]) + end + + def remove_expired_at(labels) + expiration_times.delete(labels) + end + + def now_time + ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + end + end +end diff --git a/lib/prometheus_exporter/ext/rspec/matchers.rb b/lib/prometheus_exporter/ext/rspec/matchers.rb index b5d1341..7eb147c 100644 --- a/lib/prometheus_exporter/ext/rspec/matchers.rb +++ b/lib/prometheus_exporter/ext/rspec/matchers.rb @@ -19,6 +19,10 @@ def a_gauge_metric(name) a_prometheus_metric(PrometheusExporter::Metric::Gauge, name) end + def a_gauge_with_expire_metric(name) + a_prometheus_metric(PrometheusExporter::Ext::Metric::GaugeWithExpire, name) + end + def a_counter_metric(name) a_prometheus_metric(PrometheusExporter::Metric::Counter, name) end diff --git a/lib/prometheus_exporter/ext/rspec/metric_matcher.rb b/lib/prometheus_exporter/ext/rspec/metric_matcher.rb index e05bd7d..369c8fa 100644 --- a/lib/prometheus_exporter/ext/rspec/metric_matcher.rb +++ b/lib/prometheus_exporter/ext/rspec/metric_matcher.rb @@ -40,6 +40,11 @@ def with(value, labels) self end + def empty + @metric_payload = {} + self + end + def description_of(object) RSpec::Support::ObjectFormatter.new(nil).format(object) end diff --git a/lib/prometheus_exporter/ext/rspec/send_metrics_matcher.rb b/lib/prometheus_exporter/ext/rspec/send_metrics_matcher.rb index a84f200..63a8877 100644 --- a/lib/prometheus_exporter/ext/rspec/send_metrics_matcher.rb +++ b/lib/prometheus_exporter/ext/rspec/send_metrics_matcher.rb @@ -33,8 +33,8 @@ def matches?(actual_proc) if expected expected_value = @ordered ? expected : match_array(expected) values_match?(expected_value, actual) - elsif @times - values_match?(@times, actual.size) + elsif @qty + values_match?(@qty, actual.size) else actual.size >= 1 end diff --git a/lib/prometheus_exporter/ext/server/base_collector_methods.rb b/lib/prometheus_exporter/ext/server/base_collector_methods.rb index 9e54e7d..533748c 100644 --- a/lib/prometheus_exporter/ext/server/base_collector_methods.rb +++ b/lib/prometheus_exporter/ext/server/base_collector_methods.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'prometheus_exporter/metric' +require_relative '../metric/gauge_with_expire' module PrometheusExporter::Ext::Server module BaseCollectorMethods diff --git a/lib/prometheus_exporter/ext/server/expired_stats_collector.rb b/lib/prometheus_exporter/ext/server/expired_stats_collector.rb index bcd59ea..6b53883 100644 --- a/lib/prometheus_exporter/ext/server/expired_stats_collector.rb +++ b/lib/prometheus_exporter/ext/server/expired_stats_collector.rb @@ -21,7 +21,7 @@ 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. + # @yieldparam old_metric [Hash] existing metric data. # @yieldreturn [Boolean] if true existing metric will be replaced with new one. def unique_metric_by(&block) @filter = block diff --git a/lib/prometheus_exporter/ext/server/stats_collector.rb b/lib/prometheus_exporter/ext/server/stats_collector.rb index 30438f7..1856012 100644 --- a/lib/prometheus_exporter/ext/server/stats_collector.rb +++ b/lib/prometheus_exporter/ext/server/stats_collector.rb @@ -5,6 +5,14 @@ module PrometheusExporter::Ext::Server module StatsCollector class << self + # Registers PrometheusExporter::Metric::GaugeWithExpire observer. + # @param name [Symbol] metric name. + # @param help [String] metric description. + # @param opts [Hash] additional options, supports `ttl` and `strategy` keys. + def register_gauge_with_expire(name, help, opts = {}) + register_metric(name, help, PrometheusExporter::Ext::Metric::GaugeWithExpire, opts) + end + private def included(klass) diff --git a/spec/prometheus_exporter/ext/server/expired_stats_collector_spec.rb b/spec/prometheus_exporter/ext/server/expired_stats_collector_spec.rb index 8e43fc7..f6260b0 100644 --- a/spec/prometheus_exporter/ext/server/expired_stats_collector_spec.rb +++ b/spec/prometheus_exporter/ext/server/expired_stats_collector_spec.rb @@ -1,125 +1,149 @@ # 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) - ) + describe '#metrics' do + subject do + collect_data + collector.metrics end - end - context 'with filled custom_labels' do + let(:collector) { TestExpiredCollector.new } 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 + g_metric: 1.23, + gwt_metric: 4.56, + c_metric: 7.89 } end - - before do - collector.collect(deep_stringify_keys(prev_metric)) + let(:expected_labels) { metric[:labels] } + let(:collect_data) do + collector.collect(deep_stringify_keys(metric)) end it 'observes prometheus metrics' do - subject - expect(collector.metrics).to contain_exactly( + expect(subject).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 + a_counter_metric('test_c_metric').with(7.89, expected_labels) ) 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 - } + context 'without data' do + let(:collect_data) { nil } + + it 'observes empty prometheus metrics' do + expect(subject).to contain_exactly( + a_gauge_metric('test_g_metric').empty, + a_counter_metric('test_c_metric').empty + ) + end end - let(:prev_expected_labels) { prev_metric[:labels] } - before do - collector.collect(prev_stringified_metric) + context 'with empty custom_labels' do + let(:metric) do + super().merge custom_labels: {} + end + + it 'observes prometheus metrics' do + expect(subject).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 - 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) - ) + 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 + expect(subject).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 previous metrics are expired' do - before do - sleep_seconds = collector.class.ttl + 0.1 - sleep(sleep_seconds) + 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 + + let(:collect_data) do + collector.collect(deep_stringify_keys(prev_metric)) + sleep(sleep_after_prev_metric) + super() end + let(:sleep_after_prev_metric) { 0.5 } it 'observes prometheus metrics' do - subject - expect(collector.metrics).to contain_exactly( + expect(subject).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 'when previous metrics are expired' do + let(:sleep_after_prev_metric) { collector.class.ttl + 0.1 } + + it 'observes prometheus metrics' do + expect(subject).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 + + context 'when collector has not expired previous metrics with different labels' do + 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] } + + let(:collect_data) do + collector.collect(deep_stringify_keys(prev_metric)) + sleep(sleep_after_prev_metric) + super() + end + let(:sleep_after_prev_metric) { 0.5 } + + it 'observes prometheus metrics' do + expect(subject).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 + let(:sleep_after_prev_metric) { collector.class.ttl + 0.1 } + + it 'observes prometheus metrics' do + expect(subject).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 end diff --git a/spec/prometheus_exporter/ext/server/stats_collector_spec.rb b/spec/prometheus_exporter/ext/server/stats_collector_spec.rb index 73444a9..dc5d27e 100644 --- a/spec/prometheus_exporter/ext/server/stats_collector_spec.rb +++ b/spec/prometheus_exporter/ext/server/stats_collector_spec.rb @@ -2,38 +2,58 @@ RSpec.describe PrometheusExporter::Ext::Server::StatsCollector do subject do - collector.collect(stringified_metric) + collect_data + collector.metrics end let(:collector) { TestStatsCollector.new } - let(:stringified_metric) { JSON.parse JSON.generate(metric) } let(:metric) do { type: 'test', labels: { qwe: 'asd' }, g_metric: 1.23, + gwen_metric: 1.24, + gwez_metric: 1.25, c_metric: 7.89 } end let(:expected_labels) { metric[:labels] } + let(:collect_data) do + collector.collect(deep_stringify_keys(metric)) + end it 'observes prometheus metrics' do - subject - expect(collector.metrics).to contain_exactly( + expect(subject).to contain_exactly( a_gauge_metric('test_g_metric').with(1.23, expected_labels), + a_gauge_with_expire_metric('test_gwen_metric').with(1.24, expected_labels), + a_gauge_with_expire_metric('test_gwez_metric').with(1.25, expected_labels), a_counter_metric('test_c_metric').with(7.89, expected_labels) ) end + context 'without data' do + let(:collect_data) { nil } + + it 'observes empty prometheus metrics' do + expect(subject).to contain_exactly( + a_gauge_metric('test_g_metric').empty, + a_gauge_with_expire_metric('test_gwen_metric').empty, + a_gauge_with_expire_metric('test_gwez_metric').empty, + a_counter_metric('test_c_metric').empty + ) + end + 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( + expect(subject).to contain_exactly( a_gauge_metric('test_g_metric').with(1.23, expected_labels), + a_gauge_with_expire_metric('test_gwen_metric').with(1.24, expected_labels), + a_gauge_with_expire_metric('test_gwez_metric').with(1.25, expected_labels), a_counter_metric('test_c_metric').with(7.89, expected_labels) ) end @@ -46,66 +66,153 @@ let(:expected_labels) { metric[:labels].merge metric[:custom_labels] } it 'observes prometheus metrics' do - subject - expect(collector.metrics).to contain_exactly( + expect(subject).to contain_exactly( a_gauge_metric('test_g_metric').with(1.23, expected_labels), + a_gauge_with_expire_metric('test_gwen_metric').with(1.24, expected_labels), + a_gauge_with_expire_metric('test_gwez_metric').with(1.25, 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_stringified_metric) { JSON.parse JSON.generate(prev_metric) } let(:prev_metric) do { type: 'test', labels: { qwe: 'asd' }, g_metric: 10, - gwt_metric: 20, - c_metric: 30 + gwen_metric: 11, + gwez_metric: 12, + c_metric: 20 } end - before do - collector.collect(prev_stringified_metric) + let(:collect_data) do + collector.collect(deep_stringify_keys(prev_metric)) + sleep(sleep_after_prev_metric) + super() end + let(:sleep_after_prev_metric) { 0.5 } it 'observes prometheus metrics' do - subject - expect(collector.metrics).to contain_exactly( + expect(subject).to contain_exactly( a_gauge_metric('test_g_metric').with(1.23, expected_labels), - a_counter_metric('test_c_metric').with(37.89, expected_labels) + a_gauge_with_expire_metric('test_gwen_metric').with(1.24, expected_labels), + a_gauge_with_expire_metric('test_gwez_metric').with(1.25, expected_labels), + a_counter_metric('test_c_metric').with(27.89, expected_labels) # 20 + 7.89 ) end + + context 'when test_gwen_metric prev metric is expired' do + # test_gwen_metric ttl is 2 + # test_gwez_metric ttl is 3 + let(:sleep_after_prev_metric) { 2.1 } + + it 'observes prometheus metrics' do + expect(subject).to contain_exactly( + a_gauge_metric('test_g_metric').with(1.23, expected_labels), + a_gauge_with_expire_metric('test_gwen_metric').with(1.24, expected_labels), + a_gauge_with_expire_metric('test_gwez_metric').with(1.25, expected_labels), + a_counter_metric('test_c_metric').with(27.89, expected_labels) # 20 + 7.89 + ) + end + end + + context 'when test_gwen_metric and test_gwez_metric prev metrics are expired' do + # test_gwen_metric ttl is 2 + # test_gwez_metric ttl is 3 + let(:sleep_after_prev_metric) { 3.1 } + + it 'observes prometheus metrics' do + expect(subject).to contain_exactly( + a_gauge_metric('test_g_metric').with(1.23, expected_labels), + a_gauge_with_expire_metric('test_gwen_metric').with(1.24, expected_labels), + a_gauge_with_expire_metric('test_gwez_metric').with(1.25, expected_labels), + a_counter_metric('test_c_metric').with(27.89, expected_labels) # 20 + 7.89 + ) + end + 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 + gwen_metric: 11, + gwez_metric: 12, + c_metric: 20 } end let(:prev_expected_labels) { prev_metric[:labels] } - before do - collector.collect(prev_stringified_metric) + let(:collect_data) do + collector.collect(deep_stringify_keys(prev_metric)) + sleep(sleep_after_prev_metric) + super() end + let(:sleep_after_prev_metric) { 0.5 } it 'observes prometheus metrics' do - subject - expect(collector.metrics).to contain_exactly( + expect(subject).to contain_exactly( a_gauge_metric('test_g_metric') .with(10, prev_expected_labels) .with(1.23, expected_labels), + a_gauge_with_expire_metric('test_gwen_metric') + .with(11, prev_expected_labels) + .with(1.24, expected_labels), + a_gauge_with_expire_metric('test_gwez_metric') + .with(12, prev_expected_labels) + .with(1.25, expected_labels), a_counter_metric('test_c_metric') - .with(30, prev_expected_labels) + .with(20, prev_expected_labels) .with(7.89, expected_labels) ) end + + context 'when test_gwen_metric prev metric is expired' do + # test_gwen_metric ttl is 2 + # test_gwez_metric ttl is 3 + let(:sleep_after_prev_metric) { 2.1 } + + it 'observes prometheus metrics' do + expect(subject).to contain_exactly( + a_gauge_metric('test_g_metric') + .with(10, prev_expected_labels) + .with(1.23, expected_labels), + a_gauge_with_expire_metric('test_gwen_metric') + .with(1.24, expected_labels), # prev metric deleted + a_gauge_with_expire_metric('test_gwez_metric') + .with(12, prev_expected_labels) + .with(1.25, expected_labels), + a_counter_metric('test_c_metric') + .with(20, prev_expected_labels) + .with(7.89, expected_labels) + ) + end + end + + context 'when test_gwen_metric and test_gwez_metric prev metrics are expired' do + # test_gwen_metric ttl is 2 + # test_gwez_metric ttl is 3 + let(:sleep_after_prev_metric) { 3.1 } + + it 'observes prometheus metrics' do + expect(subject).to contain_exactly( + a_gauge_metric('test_g_metric') + .with(10, prev_expected_labels) + .with(1.23, expected_labels), + a_gauge_with_expire_metric('test_gwen_metric') + .with(1.24, expected_labels), # prev metric deleted + a_gauge_with_expire_metric('test_gwez_metric') + .with(0, prev_expected_labels) # prev metric zeroed + .with(1.25, expected_labels), + a_counter_metric('test_c_metric') + .with(20, prev_expected_labels) + .with(7.89, expected_labels) + ) + end + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6c9e026..351e5fa 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -40,6 +40,8 @@ class TestStatsCollector < PrometheusExporter::Server::TypeCollector self.type = 'test' register_gauge :g_metric, 'test gauge metric' + register_gauge_with_expire :gwen_metric, 'test gauge with expire nullify metric', ttl: 2 + register_gauge_with_expire :gwez_metric, 'test gauge with expire zeroing metric', ttl: 3, strategy: :zeroing register_counter :c_metric, 'test counter metric' end @@ -48,8 +50,8 @@ class TestExpiredCollector < PrometheusExporter::Server::TypeCollector self.type = 'test' self.ttl = 2 - unique_metric_by do |new_metric, metric| - metric['labels'] == new_metric['labels'] + unique_metric_by do |new_metric, old_metric| + old_metric['labels'] == new_metric['labels'] end register_gauge :g_metric, 'test gauge metric'