Skip to content

Commit

Permalink
0.12.0: Summary metric type
Browse files Browse the repository at this point in the history
  • Loading branch information
Envek committed Jul 28, 2023
1 parent 5456049 commit 4f0f687
Show file tree
Hide file tree
Showing 12 changed files with 359 additions and 5 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## Unreleased

## 0.12.0 - 2023-07-28

### Added

- Summary metric type (mostly for Prometheus adapter).

## 0.11.0 - 2021-09-25

### Added
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ And then execute:
comment "How long whistles are being active"
unit :seconds
end
summary :bells_ringing_duration, unit: :seconds, comment: "How long bells are ringing"
end
end
```
Expand Down Expand Up @@ -181,7 +182,7 @@ Add the following to your `rails_helper.rb` (or `spec_helper.rb`):
require "yabeda/rspec"
```

Now you can use `increment_yabeda_counter`, `update_yabeda_gauge`, and `measure_yabeda_histogram` matchers:
Now you can use `increment_yabeda_counter`, `update_yabeda_gauge`, `measure_yabeda_histogram`, and `observe_yabeda_summary` matchers:

```ruby
it "increments counters" do
Expand All @@ -201,7 +202,7 @@ end

Note that tags you specified doesn't need to be exact, but can be a subset of tags used on metric update. In this example updates with following sets of tags `{ method: "command", command: "subscribe", status: "SUCCESS" }` and `{ method: "command", command: "subscribe", status: "FAILURE" }` will make test example to pass.
And check for values with `by` for counters, `to` for gauges, and `with` for gauges and histograms (and you [can use other matchers here](https://relishapp.com/rspec/rspec-expectations/v/3-10/docs/composing-matchers)):
And check for values with `by` for counters, `to` for gauges, and `with` for histograms and summaries (and you [can use other matchers here](https://relishapp.com/rspec/rspec-expectations/v/3-10/docs/composing-matchers)):
```ruby
expect { subject }.to \
Expand Down
9 changes: 9 additions & 0 deletions lib/yabeda/base_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ def register!(metric)
when Counter then register_counter!(metric)
when Gauge then register_gauge!(metric)
when Histogram then register_histogram!(metric)
when Summary then register_summary!(metric)
else raise "#{metric.class} is unknown metric type"
end
end
Expand Down Expand Up @@ -36,6 +37,14 @@ def perform_histogram_measure!(_metric, _tags, _value)
raise NotImplementedError, "#{self.class} doesn't support measuring histograms"
end

def register_summary!(_metric)
raise NotImplementedError, "#{self.class} doesn't support summaries as metric type!"
end

def perform_summary_observe!(_metric, _tags, _value)
raise NotImplementedError, "#{self.class} doesn't support observing summaries"
end

# Hook to enable debug mode in adapters when it is enabled in Yabeda itself
def debug!; end
end
Expand Down
7 changes: 7 additions & 0 deletions lib/yabeda/dsl/class_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require "yabeda/counter"
require "yabeda/gauge"
require "yabeda/histogram"
require "yabeda/summary"
require "yabeda/group"
require "yabeda/global_group"
require "yabeda/dsl/metric_builder"
Expand Down Expand Up @@ -55,6 +56,12 @@ def histogram(*args, **kwargs, &block)
register_metric(metric)
end

# Register a summary
def summary(*args, **kwargs, &block)
metric = MetricBuilder.new(Summary).build(args, kwargs, @group, &block)
register_metric(metric)
end

# Add default tag for all metric
#
# @param name [Symbol] Name of default tag
Expand Down
1 change: 1 addition & 0 deletions lib/yabeda/rspec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module RSpec
require_relative "rspec/increment_yabeda_counter"
require_relative "rspec/update_yabeda_gauge"
require_relative "rspec/measure_yabeda_histogram"
require_relative "rspec/observe_yabeda_summary"

RSpec.configure do |config|
config.before(:suite) do
Expand Down
80 changes: 80 additions & 0 deletions lib/yabeda/rspec/observe_yabeda_summary.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# frozen_string_literal: true

require_relative "base_matcher"

module Yabeda
module RSpec
# Checks whether Yabeda summary was observed during test run or not
# @param metric [Yabeda::Summary,String,Symbol] metric instance or name
# @return [Yabeda::RSpec::ObserveYabedaSummary]
def observe_yabeda_summary(metric)
ObserveYabedaSummary.new(metric)
end

# Custom matcher class with implementation for +observe_yabeda_summary+
class ObserveYabedaSummary < BaseMatcher
def with(value)
@expected_value = value
self
end

attr_reader :expected_value

def initialize(*)
super
return if metric.is_a? Yabeda::Summary

raise ArgumentError, "Pass summary instance/name to `observe_yabeda_summary`. Got #{metric.inspect} instead"
end

def match(metric, block)
block.call

observations = filter_matching_changes(Yabeda::TestAdapter.instance.summaries.fetch(metric))

observations.values.any? { |observation| expected_value.nil? || values_match?(expected_value, observation) }
end

def match_when_negated(metric, block)
unless expected_value.nil?
raise NotImplementedError, <<~MSG
`expect {}.not_to observe_yabeda_summary` doesn't support specifying values with `.with`
as it can lead to false positives.
MSG
end

block.call

observations = filter_matching_changes(Yabeda::TestAdapter.instance.summaries.fetch(metric))

observations.none?
end

def failure_message
"expected #{expected_formatted} " \
"to be observed #{"with #{expected} " unless expected_value.nil?}" \
"#{"with tags #{::RSpec::Support::ObjectFormatter.format(tags)} " if tags}" \
"but #{actual_changes_message}"
end

def failure_message_when_negated
"expected #{expected_formatted} " \
"not to be observed " \
"#{"with tags #{::RSpec::Support::ObjectFormatter.format(tags)} " if tags}" \
"but #{actual_changes_message}"
end

def actual_changes_message
observations = Yabeda::TestAdapter.instance.summaries.fetch(metric)
if observations.empty?
"no observations of this summary have been made"
elsif tags && observations.key?(tags)
formatted_tags = ::RSpec::Support::ObjectFormatter.format(tags)
"has been observed with #{observations.fetch(tags)} with tags #{formatted_tags}"
else
"following observations have been made: #{::RSpec::Support::ObjectFormatter.format(observations)}"
end
end
end
end
end
28 changes: 28 additions & 0 deletions lib/yabeda/summary.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module Yabeda
# Base class for complex metric for measuring time values that allow to
# calculate averages, percentiles, and so on.
class Summary < Metric
# rubocop: disable Metrics/MethodLength
def observe(tags, value = nil)
if value.nil? ^ block_given?
raise ArgumentError, "You must provide either numeric value or block for Yabeda::Summary#observe!"
end

if block_given?
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
yield
value = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - starting)
end

all_tags = ::Yabeda::Tags.build(tags, group)
values[all_tags] = value
::Yabeda.adapters.each do |_, adapter|
adapter.perform_summary_observe!(self, all_tags, value)
end
value
end
# rubocop: enable Metrics/MethodLength
end
end
15 changes: 13 additions & 2 deletions lib/yabeda/test_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,21 @@ module Yabeda
class TestAdapter < BaseAdapter
include Singleton

attr_reader :counters, :gauges, :histograms
attr_reader :counters, :gauges, :histograms, :summaries

# rubocop:disable Metrics/AbcSize
def initialize
super
@counters = Hash.new { |ch, ck| ch[ck] = Hash.new { |th, tk| th[tk] = 0 } }
@gauges = Hash.new { |gh, gk| gh[gk] = Hash.new { |th, tk| th[tk] = nil } }
@histograms = Hash.new { |hh, hk| hh[hk] = Hash.new { |th, tk| th[tk] = nil } }
@summaries = Hash.new { |sh, sk| sh[sk] = Hash.new { |th, tk| th[tk] = nil } }
end
# rubocop:enable Metrics/AbcSize

# Call this method after every test example to quickly get blank state for the next test example
def reset!
[@counters, @gauges, @histograms].each do |collection|
[@counters, @gauges, @histograms, @summaries].each do |collection|
collection.each_value(&:clear) # Reset tag-values hash to be empty
end
end
Expand All @@ -37,6 +40,10 @@ def register_histogram!(metric)
@histograms[metric]
end

def register_summary!(metric)
@summaries[metric]
end

def perform_counter_increment!(counter, tags, increment)
@counters[counter][tags] += increment
end
Expand All @@ -48,5 +55,9 @@ def perform_gauge_set!(gauge, tags, value)
def perform_histogram_measure!(histogram, tags, value)
@histograms[histogram][tags] = value
end

def perform_summary_observe!(summary, tags, value)
@summaries[summary][tags] = value
end
end
end
2 changes: 1 addition & 1 deletion lib/yabeda/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Yabeda
VERSION = "0.11.0"
VERSION = "0.12.0"
end
13 changes: 13 additions & 0 deletions spec/yabeda/dsl/class_methods_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,19 @@
end
end

describe ".summary" do
subject { Yabeda.test_summary }

context "when properly configured" do
before do
Yabeda.configure { summary(:test_summary) }
Yabeda.configure! unless Yabeda.already_configured?
end

it("defines method on root object") { is_expected.to be_a(Yabeda::Summary) }
end
end

describe ".default_tag" do
subject { Yabeda.default_tags }

Expand Down
Loading

0 comments on commit 4f0f687

Please sign in to comment.