From b58fd299debb711489b6434ed66cd1f046f365fd Mon Sep 17 00:00:00 2001 From: Denis Talakevich Date: Wed, 29 Nov 2023 12:53:08 +0200 Subject: [PATCH] add tests for gauge_with_time, add rspec matchers --- .../ext/metric/gauge_with_time.rb | 5 + lib/prometheus_exporter/ext/rspec.rb | 7 ++ lib/prometheus_exporter/ext/rspec/matchers.rb | 40 ++++++++ .../ext/rspec/metric_matcher.rb | 45 +++++++++ .../ext/metric/gauge_with_time_spec.rb | 98 +++++++++++++++++++ spec/prometheus_exporter/ext_spec.rb | 4 - spec/spec_helper.rb | 3 + 7 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 lib/prometheus_exporter/ext/rspec.rb create mode 100644 lib/prometheus_exporter/ext/rspec/matchers.rb create mode 100644 lib/prometheus_exporter/ext/rspec/metric_matcher.rb create mode 100644 spec/prometheus_exporter/ext/metric/gauge_with_time_spec.rb diff --git a/lib/prometheus_exporter/ext/metric/gauge_with_time.rb b/lib/prometheus_exporter/ext/metric/gauge_with_time.rb index 47a9ea2..4c931bb 100644 --- a/lib/prometheus_exporter/ext/metric/gauge_with_time.rb +++ b/lib/prometheus_exporter/ext/metric/gauge_with_time.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'prometheus_exporter/metric' +require 'date' module PrometheusExporter::Ext::Metric class GaugeWithTime < PrometheusExporter::Metric::Gauge @@ -41,6 +42,10 @@ def decrement(labels = {}, value = 1) result end + def to_h + super.to_h { |labels, value| [labels, [value, timestamps[labels]]] } + end + private def update_timestamp(labels) diff --git a/lib/prometheus_exporter/ext/rspec.rb b/lib/prometheus_exporter/ext/rspec.rb new file mode 100644 index 0000000..4e659d4 --- /dev/null +++ b/lib/prometheus_exporter/ext/rspec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require_relative 'rspec/matchers' + +RSpec.configure do |config| + config.include PrometheusExporter::Ext::RSpec::Matchers +end diff --git a/lib/prometheus_exporter/ext/rspec/matchers.rb b/lib/prometheus_exporter/ext/rspec/matchers.rb new file mode 100644 index 0000000..12a27b9 --- /dev/null +++ b/lib/prometheus_exporter/ext/rspec/matchers.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'date' +require_relative 'metric_matcher' + +module PrometheusExporter::Ext::RSpec + module Matchers + 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 + + def a_histogram_metric(name) + a_prometheus_metric(PrometheusExporter::Metric::Histogram, name) + end + + def a_summary_metric(name) + a_prometheus_metric(PrometheusExporter::Metric::Summary, name) + end + end +end diff --git a/lib/prometheus_exporter/ext/rspec/metric_matcher.rb b/lib/prometheus_exporter/ext/rspec/metric_matcher.rb new file mode 100644 index 0000000..2532263 --- /dev/null +++ b/lib/prometheus_exporter/ext/rspec/metric_matcher.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module PrometheusExporter::Ext::RSpec + class MetricMatcher + include RSpec::Matchers::DSL::DefaultImplementations + include RSpec::Matchers + include RSpec::Matchers::Composable + + attr_reader :metric_class, :metric_name, :metric_payload, :actual + + def initialize(metric_class, metric_name) + @metric_class = metric_class + @metric_name = metric_name.to_s + @metric_payload = nil + end + + def name + 'be a prometheus metric' + end + + def expected + "#{metric_class}(name=#{metric_name}, to_h=#{description_of(metric_payload)})" + end + + def matches?(actual) + @actual = actual + + return false unless values_match?(metric_class, actual.class) + return false unless values_match?(metric_name, actual.name.to_s) + return false if !metric_payload.nil? && !values_match?(metric_payload, actual.to_h) + + true + end + + def with(value, labels) + @metric_payload ||= {} + metric_payload[labels.transform_keys(&:to_s)] = value + self + end + + def description_of(object) + RSpec::Support::ObjectFormatter.new(nil).format(object) + 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 new file mode 100644 index 0000000..ca6a4ed --- /dev/null +++ b/spec/prometheus_exporter/ext/metric/gauge_with_time_spec.rb @@ -0,0 +1,98 @@ +# 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_spec.rb b/spec/prometheus_exporter/ext_spec.rb index 83b86c8..174fd8c 100644 --- a/spec/prometheus_exporter/ext_spec.rb +++ b/spec/prometheus_exporter/ext_spec.rb @@ -4,8 +4,4 @@ it 'has a version number' do expect(PrometheusExporter::Ext::VERSION).not_to be_nil end - - it 'does something useful' do - expect(false).to be(true) - end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e87b960..39b1c55 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'prometheus_exporter/ext' +require 'prometheus_exporter/ext/rspec/matchers' RSpec.configure do |config| # Enable flags like --only-failures and --next-failure @@ -12,4 +13,6 @@ config.expect_with :rspec do |c| c.syntax = :expect end + + config.include PrometheusExporter::Ext::RSpec::Matchers end