yabeda 0.10.1 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5aed70338bdfe24c4505a3cbe51cb4853c7c0e0c1842ef1cee93480ef517c02
4
- data.tar.gz: 95dc76b40980f227abe24c7bf70339d47d81eaa91e6bdfa354c8b6e6b93c8170
3
+ metadata.gz: 52ac87de3f6f2edd66e88d967b2e2e7231a87a27189e9d0a129cbafc6041813f
4
+ data.tar.gz: 2096a9ea457d7aa10efdcb54bc29a6a6e520e15974bba1efafc53df2921b5116
5
5
  SHA512:
6
- metadata.gz: 77e865d851f2e9f2a168426983df6693c9e655e0349107aafd25594eea12f9f83c14da519abbdfc5826f8c0e208d68b2a0616735771ba5ec82bde7acb3e71085
7
- data.tar.gz: aa7596a3de80b063845d552136c882cbdf46e772a1f53998058ba894dc8428290fdbae391ef241f2264dd5e76bd41be8cfab0b8afad654152b50d3b47fd0ab3b
6
+ metadata.gz: 3a75f40eb17ab3d35feef5449bfe07e369487c61609eca03da0f7eec17d1d7e32ad5b51b4a5079d3247b994df8e9730d681d501b7fa7b71d17a9b7d1ede06993
7
+ data.tar.gz: bcf1c14781cb17a3621ed4f03a3431b2399ee9e4ec80bfa044455903d2d4e9ab25563b94a01207435b05e1898e681dda91c6f36d437e574c33060f0f7c1014ef
@@ -0,0 +1,32 @@
1
+ name: Lint Ruby
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ paths:
8
+ - "gemfiles/*"
9
+ - "Gemfile"
10
+ - "**/*.rb"
11
+ - "**/*.gemspec"
12
+ - ".github/workflows/lint.yml"
13
+ pull_request:
14
+ paths:
15
+ - "gemfiles/*"
16
+ - "Gemfile"
17
+ - "**/*.rb"
18
+ - "**/*.gemspec"
19
+ - ".github/workflows/lint.yml"
20
+
21
+ jobs:
22
+ rubocop:
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@v3
26
+ - uses: ruby/setup-ruby@v1
27
+ with:
28
+ ruby-version: 3.2
29
+ bundler-cache: true
30
+ - name: Lint Ruby code with RuboCop
31
+ run: |
32
+ bundle exec rubocop
@@ -16,12 +16,10 @@ jobs:
16
16
  fail-fast: false
17
17
  matrix:
18
18
  include:
19
+ - ruby: "3.2"
20
+ - ruby: "3.1"
19
21
  - ruby: "3.0"
20
22
  - ruby: "2.7"
21
- - ruby: "2.6"
22
- - ruby: "2.5"
23
- - ruby: "2.4"
24
- - ruby: "2.3"
25
23
  container:
26
24
  image: ruby:${{ matrix.ruby }}
27
25
  env:
@@ -35,14 +33,10 @@ jobs:
35
33
  restore-keys: |
36
34
  bundle-${{ matrix.ruby }}-${{ hashFiles('**/*.gemspec') }}-${{ hashFiles('**/Gemfile') }}
37
35
  bundle-${{ matrix.ruby }}-
38
- - name: Upgrade Bundler to 2.0 (for older Rubies)
39
- run: gem install bundler -v '~> 2.0'
40
36
  - name: Bundle install
41
37
  run: |
42
38
  bundle config path vendor/bundle
43
39
  bundle install
44
40
  bundle update
45
- - name: Run Rubocop
46
- run: bundle exec rubocop
47
41
  - name: Run RSpec
48
42
  run: bundle exec rspec
data/.rubocop.yml CHANGED
@@ -10,6 +10,9 @@ Metrics/BlockLength:
10
10
  - "Gemfile"
11
11
  - "spec/**/*"
12
12
 
13
+ Metrics/AbcSize:
14
+ Max: 25
15
+
13
16
  Layout/LineLength:
14
17
  Max: 120
15
18
 
@@ -23,6 +26,15 @@ RSpec/LetSetup:
23
26
  RSpec/MultipleExpectations:
24
27
  Enabled: false
25
28
 
29
+ RSpec/DescribeClass:
30
+ Enabled: false
31
+
32
+ RSpec/NestedGroups:
33
+ Max: 4
34
+
35
+ RSpec/MultipleMemoizedHelpers:
36
+ Enabled: false
37
+
26
38
  Bundler/OrderedGems:
27
39
  Enabled: false
28
40
 
@@ -52,3 +64,6 @@ Style/HashTransformKeys:
52
64
 
53
65
  Style/HashTransformValues:
54
66
  Enabled: true
67
+
68
+ Style/Documentation:
69
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.12.0 - 2023-07-28
11
+
12
+ ### Added
13
+
14
+ - Summary metric type (mostly for Prometheus adapter).
15
+
16
+ ## 0.11.0 - 2021-09-25
17
+
18
+ ### Added
19
+
20
+ - RSpec matchers `increment_yabeda_counter`, `update_yabeda_gauge`, and `measure_yabeda_histogram` for convenient testing. [#25](https://github.com/yabeda-rb/yabeda/pull/25) by [@Envek][]
21
+ - Automatic setup of RSpec on `require "yabeda/rspec"`
22
+ - Special test adapter that collects metric changes in memory
23
+
10
24
  ## 0.10.1 - 2021-08-30
11
25
 
12
26
  ### Fixed
data/Gemfile CHANGED
@@ -8,9 +8,14 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
8
8
  gemspec
9
9
 
10
10
  group :development, :test do
11
+ gem "rake", "~> 12.0"
12
+ gem "rspec", "~> 3.0"
13
+ gem "yard"
14
+ gem "yard-dry-initializer"
15
+
11
16
  gem "pry"
12
17
  gem "pry-byebug", platform: :mri
13
18
 
14
- gem "rubocop", "~> 0.80.0"
19
+ gem "rubocop", "~> 1.0"
15
20
  gem "rubocop-rspec"
16
21
  end
data/README.md CHANGED
@@ -1,8 +1,7 @@
1
1
  # ![Yabeda](./yabeda-logo.png)
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/yabeda.svg)](https://rubygems.org/gems/yabeda)
4
-
5
- **This software is Work in Progress: features will appear and disappear, API will be changed, your feedback is always welcome!**
4
+ [![Tests status](https://github.com/yabeda-rb/yabeda/actions/workflows/test.yml/badge.svg)](https://github.com/yabeda-rb/yabeda/actions/workflows/test.yml)
6
5
 
7
6
  Extendable solution for easy setup of monitoring in your Ruby apps.
8
7
 
@@ -18,6 +17,11 @@ Most of the time you don't need to add this gem to your Gemfile directly (unless
18
17
 
19
18
  ```ruby
20
19
  gem 'yabeda'
20
+
21
+ # Add some plugins to quickly start collecting some essential metrics:
22
+ # gem 'yabeda-rails'
23
+ # gem 'yabeda-sidekiq'
24
+
21
25
  # Then add monitoring system adapter, e.g.:
22
26
  # gem 'yabeda-prometheus'
23
27
  ```
@@ -39,6 +43,7 @@ And then execute:
39
43
  comment "How long whistles are being active"
40
44
  unit :seconds
41
45
  end
46
+ summary :bells_ringing_duration, unit: :seconds, comment: "How long bells are ringing"
42
47
  end
43
48
  end
44
49
  ```
@@ -124,6 +129,7 @@ And then execute:
124
129
  These are developed and maintained by other awesome folks:
125
130
 
126
131
  - [Statsd](https://github.com/asusikov/yabeda-statsd)
132
+ - [AWS CloudWatch](https://github.com/retsef/yabeda-cloudwatch)
127
133
  - _…and more! You can write your own adapter and open a pull request to add it into this list._
128
134
 
129
135
  ## Available plugins to collect metrics
@@ -131,6 +137,7 @@ These are developed and maintained by other awesome folks:
131
137
  ### Maintained by Yabeda
132
138
 
133
139
  - [yabeda-rails] — basic request metrics for [Ruby on Rails](https://rubyonrails.org/) applications.
140
+ - [yabeda-activerecord] — query performance and connection pool stats for apps using ActiveRecord to query databases.
134
141
  - [yabeda-sidekiq] — comprehensive set of metrics for monitoring [Sidekiq](https://sidekiq.org/) jobs execution and queues.
135
142
  - [yabeda-faktory] — metrics for monitoring jobs execution by Ruby workers of [Faktory](https://contribsys.com/faktory/).
136
143
  - [yabeda-graphql] — metrics to query and field-level monitoring for apps using [GraphQL-Ruby](https://graphql-ruby.org/).
@@ -146,6 +153,9 @@ These are developed and maintained by other awesome folks:
146
153
  - [yabeda-grape](https://github.com/efigence/yabeda-grape) — metrics for [Grape](https://github.com/ruby-grape/grape) framework.
147
154
  - [yabeda-gruf](https://github.com/Placewise/yabeda-gruf) — metrics for [gRPC Ruby Framework](https://github.com/bigcommerce/gruf)
148
155
  - [yabeda-gc](https://github.com/ianks/yabeda-gc) — metrics for Ruby garbage collection.
156
+ - [yabeda-activejob](https://github.com/Fullscript/yabeda-activejob) — backend-agnostic metrics for background jobs.
157
+ - [yabeda-shoryuken](https://github.com/retsef/yabeda-shoryuken) — metrics for [Shoryuken](https://github.com/ruby-shoryuken/shoryuken) jobs execution message queues.
158
+ - [yabeda-rack-ratelimit](https://github.com/basecamp/yabeda-rack-ratelimit) — metrics for [Rack::Ratelimit](https://github.com/jeremy/rack-ratelimit)
149
159
  - _…and more! You can write your own adapter and open a pull request to add it into this list._
150
160
 
151
161
  ## Configuration
@@ -162,6 +172,44 @@ Config key | Type | Default | Description |
162
172
 
163
173
  These are only enabled in debug mode. To enable it either set `debug` config key to `true` (e.g. by specifying `YABEDA_DEBUG=true` in your environment variables or executing `Yabeda.debug!` in your code).
164
174
 
175
+ ## Testing
176
+
177
+ ### RSpec
178
+
179
+ Add the following to your `rails_helper.rb` (or `spec_helper.rb`):
180
+
181
+ ```ruby
182
+ require "yabeda/rspec"
183
+ ```
184
+
185
+ Now you can use `increment_yabeda_counter`, `update_yabeda_gauge`, `measure_yabeda_histogram`, and `observe_yabeda_summary` matchers:
186
+
187
+ ```ruby
188
+ it "increments counters" do
189
+ expect { subject }.to increment_yabeda_counter(Yabeda.myapp.foo_count).by(3)
190
+ end
191
+ ```
192
+
193
+ You can scope metrics by used tags with `with_tags`:
194
+
195
+ ```ruby
196
+ it "updates gauges" do
197
+ expect { subject }.to \
198
+ update_yabeda_gauge("some_gauge_name").
199
+ with_tags(method: "command", command: "subscribe")
200
+ end
201
+ ```
202
+
203
+ 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.
204
+
205
+ 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)):
206
+
207
+ ```ruby
208
+ expect { subject }.to \
209
+ measure_yabeda_histogram(Yabeda.something.anything_runtime).
210
+ with(be_between(0.005, 0.05))
211
+ ```
212
+
165
213
  ## Roadmap (aka TODO or Help wanted)
166
214
 
167
215
  - Ability to change metric settings for individual adapters
@@ -229,6 +277,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/yabeda
229
277
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
230
278
 
231
279
  [yabeda-rails]: https://github.com/yabeda-rb/yabeda-rails/ "Yabeda plugin for collecting and exporting basic metrics for Rails applications"
280
+ [yabeda-activerecord]: https://github.com/yabeda-rb/yabeda-activerecord/ "Yabeda plugin to collect query performance metrics and connection pool statistics"
232
281
  [yabeda-sidekiq]: https://github.com/yabeda-rb/yabeda-sidekiq/ "Yabeda plugin for complete monitoring of Sidekiq metrics"
233
282
  [yabeda-faktory]: https://github.com/yabeda-rb/yabeda-faktory/ "Yabeda plugin for complete monitoring of Faktory Ruby Workers"
234
283
  [yabeda-graphql]: https://github.com/yabeda-rb/yabeda-graphql/ "Measure and understand how good your GraphQL-Ruby application works"
@@ -8,6 +8,7 @@ module Yabeda
8
8
  when Counter then register_counter!(metric)
9
9
  when Gauge then register_gauge!(metric)
10
10
  when Histogram then register_histogram!(metric)
11
+ when Summary then register_summary!(metric)
11
12
  else raise "#{metric.class} is unknown metric type"
12
13
  end
13
14
  end
@@ -36,6 +37,14 @@ module Yabeda
36
37
  raise NotImplementedError, "#{self.class} doesn't support measuring histograms"
37
38
  end
38
39
 
40
+ def register_summary!(_metric)
41
+ raise NotImplementedError, "#{self.class} doesn't support summaries as metric type!"
42
+ end
43
+
44
+ def perform_summary_observe!(_metric, _tags, _value)
45
+ raise NotImplementedError, "#{self.class} doesn't support observing summaries"
46
+ end
47
+
39
48
  # Hook to enable debug mode in adapters when it is enabled in Yabeda itself
40
49
  def debug!; end
41
50
  end
@@ -4,6 +4,7 @@ require "yabeda/metric"
4
4
  require "yabeda/counter"
5
5
  require "yabeda/gauge"
6
6
  require "yabeda/histogram"
7
+ require "yabeda/summary"
7
8
  require "yabeda/group"
8
9
  require "yabeda/global_group"
9
10
  require "yabeda/dsl/metric_builder"
@@ -11,7 +12,6 @@ require "yabeda/dsl/metric_builder"
11
12
  module Yabeda
12
13
  # DSL for ease of work with Yabeda
13
14
  module DSL
14
- # rubocop: disable Style/Documentation
15
15
  module ClassMethods
16
16
  # Block for grouping and simplifying configuration of related metrics
17
17
  def configure(&block)
@@ -56,6 +56,12 @@ module Yabeda
56
56
  register_metric(metric)
57
57
  end
58
58
 
59
+ # Register a summary
60
+ def summary(*args, **kwargs, &block)
61
+ metric = MetricBuilder.new(Summary).build(args, kwargs, @group, &block)
62
+ register_metric(metric)
63
+ end
64
+
59
65
  # Add default tag for all metric
60
66
  #
61
67
  # @param name [Symbol] Name of default tag
@@ -110,6 +116,5 @@ module Yabeda
110
116
  group.register_metric(metric)
111
117
  end
112
118
  end
113
- # rubocop: enable Style/Documentation
114
119
  end
115
120
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "forwardable"
4
- require_relative "./group"
4
+ require_relative "group"
5
5
 
6
6
  module Yabeda
7
7
  # Represents implicit global group
data/lib/yabeda/metric.rb CHANGED
@@ -29,5 +29,9 @@ module Yabeda
29
29
  def tags
30
30
  (Yabeda.groups[group].default_tags.keys + Array(super)).uniq
31
31
  end
32
+
33
+ def inspect
34
+ "#<#{self.class.name}: #{[@group, @name].compact.join('.')}>"
35
+ end
32
36
  end
33
37
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yabeda
4
+ module RSpec
5
+ # Notes:
6
+ # +expected+ is always a metric instance
7
+ # +actual+ is always a block of code
8
+ # Example:
9
+ # expect { anything }.to do_whatever_with_yabeda_metric(Yabeda.something)
10
+ class BaseMatcher < ::RSpec::Matchers::BuiltIn::BaseMatcher
11
+ attr_reader :tags, :metric
12
+
13
+ # Specify a scope of labels (tags). Subset of tags can be specified.
14
+ def with_tags(tags)
15
+ @tags = tags
16
+ self
17
+ end
18
+
19
+ def initialize(expected)
20
+ super
21
+ @expected = @metric = resolve_metric(expected)
22
+ rescue KeyError
23
+ raise ArgumentError, <<~MSG
24
+ Pass metric name or metric instance to matcher (e.g. `increment_yabeda_counter(Yabeda.metric_name)` or \
25
+ increment_yabeda_counter('metric_name')). Got #{expected.inspect} instead
26
+ MSG
27
+ end
28
+
29
+ # RSpec doesn't define this method, but it is more convenient to rely on +match_when_negated+ method presence
30
+ def does_not_match?(actual)
31
+ @actual = actual
32
+ if respond_to?(:match_when_negated)
33
+ match_when_negated(expected, actual)
34
+ else
35
+ !match(expected, actual)
36
+ end
37
+ end
38
+
39
+ def supports_block_expectations?
40
+ true
41
+ end
42
+
43
+ # Pretty print metric name (expected is expected to always be a Yabeda metric instance)
44
+ def expected_formatted
45
+ "Yabeda.#{[metric.group, metric.name].compact.join('.')}"
46
+ end
47
+
48
+ private
49
+
50
+ def resolve_metric(instance_or_name)
51
+ return instance_or_name if instance_or_name.is_a? Yabeda::Metric
52
+
53
+ Yabeda.metrics.fetch(instance_or_name.to_s)
54
+ end
55
+
56
+ # Filter metric changes by tags.
57
+ # If tags specified, treat them as subset of real tags (to avoid bothering with default tags in tests)
58
+ def filter_matching_changes(changes)
59
+ return changes if tags.nil?
60
+
61
+ changes.select { |t, _v| t >= tags }
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_matcher"
4
+
5
+ module Yabeda
6
+ module RSpec
7
+ # Checks whether Yabeda counter was incremented during test run or not
8
+ # @param metric [Yabeda::Counter,String,Symbol] metric instance or name
9
+ # @return [Yabeda::RSpec::IncrementYabedaCounter]
10
+ def increment_yabeda_counter(metric)
11
+ IncrementYabedaCounter.new(metric)
12
+ end
13
+
14
+ # Custom matcher class with implementation for +increment_yabeda_counter+
15
+ class IncrementYabedaCounter < BaseMatcher
16
+ def by(increment)
17
+ @expected_increment = increment
18
+ self
19
+ end
20
+
21
+ attr_reader :expected_increment
22
+
23
+ def initialize(*)
24
+ super
25
+ return if metric.is_a? Yabeda::Counter
26
+
27
+ raise ArgumentError, "Pass counter instance/name to `increment_yabeda_counter`. Got #{metric.inspect} instead"
28
+ end
29
+
30
+ def match(metric, block)
31
+ block.call
32
+
33
+ increments = filter_matching_changes(Yabeda::TestAdapter.instance.counters.fetch(metric))
34
+
35
+ increments.values.any? do |actual_increment|
36
+ expected_increment.nil? || values_match?(expected_increment, actual_increment)
37
+ end
38
+ end
39
+
40
+ def match_when_negated(metric, block)
41
+ unless expected_increment.nil?
42
+ raise NotImplementedError, <<~MSG
43
+ `expect(Yabeda.metric_name).not_to increment_yabeda_counter` doesn't support specifying increment
44
+ with `.by` as it can lead to false positives.
45
+ MSG
46
+ end
47
+
48
+ block.call
49
+
50
+ increments = filter_matching_changes(Yabeda::TestAdapter.instance.counters.fetch(metric))
51
+
52
+ increments.none?
53
+ end
54
+
55
+ def failure_message
56
+ "expected #{expected_formatted} " \
57
+ "to be incremented #{"by #{description_of(expected_increment)} " unless expected_increment.nil?}" \
58
+ "#{"with tags #{::RSpec::Support::ObjectFormatter.format(tags)} " if tags}" \
59
+ "but #{actual_increments_message}"
60
+ end
61
+
62
+ def failure_message_when_negated
63
+ "expected #{expected_formatted} " \
64
+ "not to be incremented " \
65
+ "#{"with tags #{::RSpec::Support::ObjectFormatter.format(tags)} " if tags}" \
66
+ "but #{actual_increments_message}"
67
+ end
68
+
69
+ def actual_increments_message
70
+ counter_increments = Yabeda::TestAdapter.instance.counters.fetch(metric)
71
+ if counter_increments.empty?
72
+ "no increments of this counter have been made"
73
+ elsif tags && counter_increments.key?(tags)
74
+ "has been incremented by #{counter_increments.fetch(tags)}"
75
+ else
76
+ "following increments have been made: #{::RSpec::Support::ObjectFormatter.format(counter_increments)}"
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_matcher"
4
+
5
+ module Yabeda
6
+ module RSpec
7
+ # Checks whether Yabeda histogram was measured during test run or not
8
+ # @param metric [Yabeda::Histogram,String,Symbol] metric instance or name
9
+ # @return [Yabeda::RSpec::MeasureYabedaHistogram]
10
+ def measure_yabeda_histogram(metric)
11
+ MeasureYabedaHistogram.new(metric)
12
+ end
13
+
14
+ # Custom matcher class with implementation for +measure_yabeda_histogram+
15
+ class MeasureYabedaHistogram < BaseMatcher
16
+ def with(value)
17
+ @expected_value = value
18
+ self
19
+ end
20
+
21
+ attr_reader :expected_value
22
+
23
+ def initialize(*)
24
+ super
25
+ return if metric.is_a? Yabeda::Histogram
26
+
27
+ raise ArgumentError, "Pass histogram instance/name to `measure_yabeda_histogram`. Got #{metric.inspect} instead"
28
+ end
29
+
30
+ def match(metric, block)
31
+ block.call
32
+
33
+ measures = filter_matching_changes(Yabeda::TestAdapter.instance.histograms.fetch(metric))
34
+
35
+ measures.values.any? { |measure| expected_value.nil? || values_match?(expected_value, measure) }
36
+ end
37
+
38
+ def match_when_negated(metric, block)
39
+ unless expected_value.nil?
40
+ raise NotImplementedError, <<~MSG
41
+ `expect {}.not_to measure_yabeda_histogram` doesn't support specifying values with `.with`
42
+ as it can lead to false positives.
43
+ MSG
44
+ end
45
+
46
+ block.call
47
+
48
+ measures = filter_matching_changes(Yabeda::TestAdapter.instance.histograms.fetch(metric))
49
+
50
+ measures.none?
51
+ end
52
+
53
+ def failure_message
54
+ "expected #{expected_formatted} " \
55
+ "to be changed #{"to #{expected} " unless expected_value.nil?}" \
56
+ "#{"with tags #{::RSpec::Support::ObjectFormatter.format(tags)} " if tags}" \
57
+ "but #{actual_changes_message}"
58
+ end
59
+
60
+ def failure_message_when_negated
61
+ "expected #{expected_formatted} " \
62
+ "not to be incremented " \
63
+ "#{"with tags #{::RSpec::Support::ObjectFormatter.format(tags)} " if tags}" \
64
+ "but #{actual_changes_message}"
65
+ end
66
+
67
+ def actual_changes_message
68
+ measures = Yabeda::TestAdapter.instance.histograms.fetch(metric)
69
+ if measures.empty?
70
+ "no changes of this gauge have been made"
71
+ elsif tags && measures.key?(tags)
72
+ "has been changed to #{measures.fetch(tags)} with tags #{::RSpec::Support::ObjectFormatter.format(tags)}"
73
+ else
74
+ "following changes have been made: #{::RSpec::Support::ObjectFormatter.format(measures)}"
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_matcher"
4
+
5
+ module Yabeda
6
+ module RSpec
7
+ # Checks whether Yabeda summary was observed during test run or not
8
+ # @param metric [Yabeda::Summary,String,Symbol] metric instance or name
9
+ # @return [Yabeda::RSpec::ObserveYabedaSummary]
10
+ def observe_yabeda_summary(metric)
11
+ ObserveYabedaSummary.new(metric)
12
+ end
13
+
14
+ # Custom matcher class with implementation for +observe_yabeda_summary+
15
+ class ObserveYabedaSummary < BaseMatcher
16
+ def with(value)
17
+ @expected_value = value
18
+ self
19
+ end
20
+
21
+ attr_reader :expected_value
22
+
23
+ def initialize(*)
24
+ super
25
+ return if metric.is_a? Yabeda::Summary
26
+
27
+ raise ArgumentError, "Pass summary instance/name to `observe_yabeda_summary`. Got #{metric.inspect} instead"
28
+ end
29
+
30
+ def match(metric, block)
31
+ block.call
32
+
33
+ observations = filter_matching_changes(Yabeda::TestAdapter.instance.summaries.fetch(metric))
34
+
35
+ observations.values.any? { |observation| expected_value.nil? || values_match?(expected_value, observation) }
36
+ end
37
+
38
+ def match_when_negated(metric, block)
39
+ unless expected_value.nil?
40
+ raise NotImplementedError, <<~MSG
41
+ `expect {}.not_to observe_yabeda_summary` doesn't support specifying values with `.with`
42
+ as it can lead to false positives.
43
+ MSG
44
+ end
45
+
46
+ block.call
47
+
48
+ observations = filter_matching_changes(Yabeda::TestAdapter.instance.summaries.fetch(metric))
49
+
50
+ observations.none?
51
+ end
52
+
53
+ def failure_message
54
+ "expected #{expected_formatted} " \
55
+ "to be observed #{"with #{expected} " unless expected_value.nil?}" \
56
+ "#{"with tags #{::RSpec::Support::ObjectFormatter.format(tags)} " if tags}" \
57
+ "but #{actual_changes_message}"
58
+ end
59
+
60
+ def failure_message_when_negated
61
+ "expected #{expected_formatted} " \
62
+ "not to be observed " \
63
+ "#{"with tags #{::RSpec::Support::ObjectFormatter.format(tags)} " if tags}" \
64
+ "but #{actual_changes_message}"
65
+ end
66
+
67
+ def actual_changes_message
68
+ observations = Yabeda::TestAdapter.instance.summaries.fetch(metric)
69
+ if observations.empty?
70
+ "no observations of this summary have been made"
71
+ elsif tags && observations.key?(tags)
72
+ formatted_tags = ::RSpec::Support::ObjectFormatter.format(tags)
73
+ "has been observed with #{observations.fetch(tags)} with tags #{formatted_tags}"
74
+ else
75
+ "following observations have been made: #{::RSpec::Support::ObjectFormatter.format(observations)}"
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_matcher"
4
+
5
+ module Yabeda
6
+ module RSpec
7
+ # Checks whether Yabeda gauge was set to some value during test run or not
8
+ # @param metric [Yabeda::Gauge,String,Symbol] metric instance or name
9
+ # @return [Yabeda::RSpec::UpdateYabedaGauge]
10
+ def update_yabeda_gauge(metric)
11
+ UpdateYabedaGauge.new(metric)
12
+ end
13
+
14
+ # Custom matcher class with implementation for +update_yabeda_gauge+
15
+ class UpdateYabedaGauge < BaseMatcher
16
+ def with(value)
17
+ @expected_value = value
18
+ self
19
+ end
20
+
21
+ attr_reader :expected_value
22
+
23
+ def initialize(*)
24
+ super
25
+ return if metric.is_a? Yabeda::Gauge
26
+
27
+ raise ArgumentError, "Pass gauge instance/name to `update_yabeda_gauge`. Got #{metric.inspect} instead"
28
+ end
29
+
30
+ def match(metric, block)
31
+ block.call
32
+
33
+ updates = filter_matching_changes(Yabeda::TestAdapter.instance.gauges.fetch(metric))
34
+
35
+ updates.values.any? { |update| expected_value.nil? || values_match?(expected_value, update) }
36
+ end
37
+
38
+ def match_when_negated(metric, block)
39
+ unless expected_value.nil?
40
+ raise NotImplementedError, <<~MSG
41
+ `expect(Yabeda.metric_name).not_to update_yabeda_gauge` doesn't support specifying values with `.with`
42
+ as it can lead to false positives.
43
+ MSG
44
+ end
45
+
46
+ block.call
47
+
48
+ updates = filter_matching_changes(Yabeda::TestAdapter.instance.gauges.fetch(metric))
49
+
50
+ updates.none?
51
+ end
52
+
53
+ def failure_message
54
+ "expected #{expected_formatted} " \
55
+ "to be changed #{"to #{expected_value} " unless expected_value.nil?}" \
56
+ "#{"with tags #{::RSpec::Support::ObjectFormatter.format(tags)} " if tags}" \
57
+ "but #{actual_changes_message}"
58
+ end
59
+
60
+ def failure_message_when_negated
61
+ "expected #{expected_formatted} " \
62
+ "not to be changed " \
63
+ "#{"with tags #{::RSpec::Support::ObjectFormatter.format(tags)} " if tags}" \
64
+ "but #{actual_changes_message}"
65
+ end
66
+
67
+ def actual_changes_message
68
+ updates = Yabeda::TestAdapter.instance.gauges.fetch(metric)
69
+ if updates.empty?
70
+ "no changes of this gauge have been made"
71
+ elsif tags && updates.key?(tags)
72
+ "has been changed to #{updates.fetch(tags)} with tags #{::RSpec::Support::ObjectFormatter.format(tags)}"
73
+ else
74
+ "following changes have been made: #{::RSpec::Support::ObjectFormatter.format(updates)}"
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "testing"
4
+
5
+ module Yabeda
6
+ # RSpec integration for Yabeda: custom matchers, etc
7
+ module RSpec
8
+ end
9
+ end
10
+
11
+ require_relative "rspec/increment_yabeda_counter"
12
+ require_relative "rspec/update_yabeda_gauge"
13
+ require_relative "rspec/measure_yabeda_histogram"
14
+ require_relative "rspec/observe_yabeda_summary"
15
+
16
+ RSpec.configure do |config|
17
+ config.before(:suite) do
18
+ Yabeda.configure! unless Yabeda.already_configured?
19
+ end
20
+
21
+ config.after(:each) do
22
+ Yabeda::TestAdapter.instance.reset!
23
+ end
24
+
25
+ config.include(Yabeda::RSpec)
26
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yabeda
4
+ # Base class for complex metric for measuring time values that allow to
5
+ # calculate averages, percentiles, and so on.
6
+ class Summary < Metric
7
+ # rubocop: disable Metrics/MethodLength
8
+ def observe(tags, value = nil)
9
+ if value.nil? ^ block_given?
10
+ raise ArgumentError, "You must provide either numeric value or block for Yabeda::Summary#observe!"
11
+ end
12
+
13
+ if block_given?
14
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
15
+ yield
16
+ value = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - starting)
17
+ end
18
+
19
+ all_tags = ::Yabeda::Tags.build(tags, group)
20
+ values[all_tags] = value
21
+ ::Yabeda.adapters.each do |_, adapter|
22
+ adapter.perform_summary_observe!(self, all_tags, value)
23
+ end
24
+ value
25
+ end
26
+ # rubocop: enable Metrics/MethodLength
27
+ end
28
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+
5
+ require_relative "base_adapter"
6
+
7
+ module Yabeda
8
+ # Fake monitoring system adapter that collects latest metric values for later inspection
9
+ class TestAdapter < BaseAdapter
10
+ include Singleton
11
+
12
+ attr_reader :counters, :gauges, :histograms, :summaries
13
+
14
+ # rubocop:disable Metrics/AbcSize
15
+ def initialize
16
+ super
17
+ @counters = Hash.new { |ch, ck| ch[ck] = Hash.new { |th, tk| th[tk] = 0 } }
18
+ @gauges = Hash.new { |gh, gk| gh[gk] = Hash.new { |th, tk| th[tk] = nil } }
19
+ @histograms = Hash.new { |hh, hk| hh[hk] = Hash.new { |th, tk| th[tk] = nil } }
20
+ @summaries = Hash.new { |sh, sk| sh[sk] = Hash.new { |th, tk| th[tk] = nil } }
21
+ end
22
+ # rubocop:enable Metrics/AbcSize
23
+
24
+ # Call this method after every test example to quickly get blank state for the next test example
25
+ def reset!
26
+ [@counters, @gauges, @histograms, @summaries].each do |collection|
27
+ collection.each_value(&:clear) # Reset tag-values hash to be empty
28
+ end
29
+ end
30
+
31
+ def register_counter!(metric)
32
+ @counters[metric]
33
+ end
34
+
35
+ def register_gauge!(metric)
36
+ @gauges[metric]
37
+ end
38
+
39
+ def register_histogram!(metric)
40
+ @histograms[metric]
41
+ end
42
+
43
+ def register_summary!(metric)
44
+ @summaries[metric]
45
+ end
46
+
47
+ def perform_counter_increment!(counter, tags, increment)
48
+ @counters[counter][tags] += increment
49
+ end
50
+
51
+ def perform_gauge_set!(gauge, tags, value)
52
+ @gauges[gauge][tags] = value
53
+ end
54
+
55
+ def perform_histogram_measure!(histogram, tags, value)
56
+ @histograms[histogram][tags] = value
57
+ end
58
+
59
+ def perform_summary_observe!(summary, tags, value)
60
+ @summaries[summary][tags] = value
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Include this file to get things prepared for testing
4
+
5
+ require_relative "test_adapter"
6
+
7
+ Yabeda.register_adapter(:test, Yabeda::TestAdapter.instance)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yabeda
4
- VERSION = "0.10.1"
4
+ VERSION = "0.12.0"
5
5
  end
data/lib/yabeda.rb CHANGED
@@ -86,7 +86,7 @@ module Yabeda
86
86
 
87
87
  # Perform configuration: registration of metrics and collector blocks
88
88
  # @return [void]
89
- # rubocop: disable Metrics/MethodLength, Metrics/AbcSize
89
+ # rubocop: disable Metrics/MethodLength
90
90
  def configure!
91
91
  raise(AlreadyConfiguredError, @configured_by) if already_configured?
92
92
 
@@ -129,12 +129,11 @@ module Yabeda
129
129
 
130
130
  true
131
131
  end
132
- # rubocop: enable Metrics/MethodLength, Metrics/AbcSize
132
+ # rubocop: enable Metrics/MethodLength
133
133
 
134
134
  # Forget all the configuration.
135
135
  # For testing purposes as it doesn't rollback changes in adapters.
136
136
  # @api private
137
- # rubocop: disable Metrics/AbcSize
138
137
  def reset!
139
138
  default_tags.clear
140
139
  adapters.clear
@@ -147,6 +146,5 @@ module Yabeda
147
146
  instance_variable_set(:@configured_by, nil)
148
147
  instance_variable_set(:@debug_was_enabled_by, nil)
149
148
  end
150
- # rubocop: enable Metrics/AbcSize
151
149
  end
152
150
  end
data/yabeda.gemspec CHANGED
@@ -29,9 +29,5 @@ Gem::Specification.new do |spec|
29
29
  spec.add_dependency "concurrent-ruby"
30
30
  spec.add_dependency "dry-initializer"
31
31
 
32
- spec.add_development_dependency "bundler", "~> 2.0"
33
- spec.add_development_dependency "rake", "~> 12.0"
34
- spec.add_development_dependency "rspec", "~> 3.0"
35
- spec.add_development_dependency "yard"
36
- spec.add_development_dependency "yard-dry-initializer"
32
+ spec.required_ruby_version = ">= 2.3"
37
33
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yabeda
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.1
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrey Novikov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-08-30 00:00:00.000000000 Z
11
+ date: 2023-07-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: anyway_config
@@ -58,76 +58,6 @@ dependencies:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
60
  version: '0'
61
- - !ruby/object:Gem::Dependency
62
- name: bundler
63
- requirement: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - "~>"
66
- - !ruby/object:Gem::Version
67
- version: '2.0'
68
- type: :development
69
- prerelease: false
70
- version_requirements: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: '2.0'
75
- - !ruby/object:Gem::Dependency
76
- name: rake
77
- requirement: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - "~>"
80
- - !ruby/object:Gem::Version
81
- version: '12.0'
82
- type: :development
83
- prerelease: false
84
- version_requirements: !ruby/object:Gem::Requirement
85
- requirements:
86
- - - "~>"
87
- - !ruby/object:Gem::Version
88
- version: '12.0'
89
- - !ruby/object:Gem::Dependency
90
- name: rspec
91
- requirement: !ruby/object:Gem::Requirement
92
- requirements:
93
- - - "~>"
94
- - !ruby/object:Gem::Version
95
- version: '3.0'
96
- type: :development
97
- prerelease: false
98
- version_requirements: !ruby/object:Gem::Requirement
99
- requirements:
100
- - - "~>"
101
- - !ruby/object:Gem::Version
102
- version: '3.0'
103
- - !ruby/object:Gem::Dependency
104
- name: yard
105
- requirement: !ruby/object:Gem::Requirement
106
- requirements:
107
- - - ">="
108
- - !ruby/object:Gem::Version
109
- version: '0'
110
- type: :development
111
- prerelease: false
112
- version_requirements: !ruby/object:Gem::Requirement
113
- requirements:
114
- - - ">="
115
- - !ruby/object:Gem::Version
116
- version: '0'
117
- - !ruby/object:Gem::Dependency
118
- name: yard-dry-initializer
119
- requirement: !ruby/object:Gem::Requirement
120
- requirements:
121
- - - ">="
122
- - !ruby/object:Gem::Version
123
- version: '0'
124
- type: :development
125
- prerelease: false
126
- version_requirements: !ruby/object:Gem::Requirement
127
- requirements:
128
- - - ">="
129
- - !ruby/object:Gem::Version
130
- version: '0'
131
61
  description: 'Collect statistics about how your application is performing with ease.
132
62
  Export metrics to various monitoring systems.
133
63
 
@@ -139,6 +69,7 @@ extensions: []
139
69
  extra_rdoc_files: []
140
70
  files:
141
71
  - ".github/workflows/build-release.yml"
72
+ - ".github/workflows/lint.yml"
142
73
  - ".github/workflows/test.yml"
143
74
  - ".gitignore"
144
75
  - ".rspec"
@@ -166,7 +97,16 @@ files:
166
97
  - lib/yabeda/histogram.rb
167
98
  - lib/yabeda/metric.rb
168
99
  - lib/yabeda/railtie.rb
100
+ - lib/yabeda/rspec.rb
101
+ - lib/yabeda/rspec/base_matcher.rb
102
+ - lib/yabeda/rspec/increment_yabeda_counter.rb
103
+ - lib/yabeda/rspec/measure_yabeda_histogram.rb
104
+ - lib/yabeda/rspec/observe_yabeda_summary.rb
105
+ - lib/yabeda/rspec/update_yabeda_gauge.rb
106
+ - lib/yabeda/summary.rb
169
107
  - lib/yabeda/tags.rb
108
+ - lib/yabeda/test_adapter.rb
109
+ - lib/yabeda/testing.rb
170
110
  - lib/yabeda/version.rb
171
111
  - yabeda-logo.png
172
112
  - yabeda.gemspec
@@ -182,7 +122,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
182
122
  requirements:
183
123
  - - ">="
184
124
  - !ruby/object:Gem::Version
185
- version: '0'
125
+ version: '2.3'
186
126
  required_rubygems_version: !ruby/object:Gem::Requirement
187
127
  requirements:
188
128
  - - ">="