diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fad61c..a44b201 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added +- Ability to limit some metrics to specific adapters. [#37](https://github.com/yabeda-rb/yabeda/pull/37) by [@Keallar] and [@Envek] + + ```ruby + Yabeda.configure do + group :cloud do + adapter :newrelic, :datadog + + counter :foo + end + + counter :bar, adapter: :prometheus + end + ``` + - Multiple expectations in RSpec matchers: ```ruby @@ -155,3 +169,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. [@dsalahutdinov]: https://github.com/dsalahutdinov "Dmitry Salahutdinov" [@asusikov]: https://github.com/asusikov "Alexander Susikov" [@liaden]: https://github.com/liaden "Joel Johnson" +[@Keallar]: https://github.com/Keallar "Eugene Lysanskiy" diff --git a/README.md b/README.md index 48af79f..64b5e33 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,32 @@ expect { whatever }.to increment_yabeda_counter(:my_counter).with( ) ``` +## Advanced usage + +### Limiting metrics and groups to specific adapters + +You can limit, which metrics and groups should be available for specific adapter: + +```ruby +Yabeda.configure do + group :internal do + adapter :prometheus + + counter :foo + gauge :bar + end + + group :cloud do + adapter :newrelic + + counter :baz + end + + counter :qux, adapter: :prometheus +end +``` + + ## Roadmap (aka TODO or Help wanted) - Ability to change metric settings for individual adapters @@ -233,16 +259,6 @@ expect { whatever }.to increment_yabeda_counter(:my_counter).with( end ``` - - Ability to route some metrics only for given adapter: - - ```rb - adapter :prometheus do - include_group :sidekiq - end - ``` - - - ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. diff --git a/lib/yabeda.rb b/lib/yabeda.rb index a7a1e56..9843d38 100644 --- a/lib/yabeda.rb +++ b/lib/yabeda.rb @@ -28,7 +28,7 @@ def groups end end - # @return [Hash] All loaded adapters + # @return [Hash] All loaded adapters def adapters @adapters ||= Concurrent::Hash.new end @@ -68,6 +68,8 @@ def register_adapter(name, instance) adapters[name] = instance # NOTE: Pretty sure there is race condition metrics.each_value do |metric| + next unless metric.adapters.key?(name) + instance.register!(metric) end end @@ -99,8 +101,8 @@ def configure! # Register metrics in adapters after evaluating all configuration blocks # to ensure that all global settings (like default tags) will be applied. - adapters.each_value do |adapter| - metrics.each_value do |metric| + metrics.each_value do |metric| + metric.adapters.each_value do |adapter| adapter.register!(metric) end end diff --git a/lib/yabeda/counter.rb b/lib/yabeda/counter.rb index 9f257d3..c9b52a1 100644 --- a/lib/yabeda/counter.rb +++ b/lib/yabeda/counter.rb @@ -6,7 +6,7 @@ class Counter < Metric def increment(tags, by: 1) all_tags = ::Yabeda::Tags.build(tags, group) values[all_tags] += by - ::Yabeda.adapters.each_value do |adapter| + adapters.each_value do |adapter| adapter.perform_counter_increment!(self, all_tags, by) end values[all_tags] diff --git a/lib/yabeda/dsl/class_methods.rb b/lib/yabeda/dsl/class_methods.rb index dc00ded..a79789e 100644 --- a/lib/yabeda/dsl/class_methods.rb +++ b/lib/yabeda/dsl/class_methods.rb @@ -92,6 +92,16 @@ def temporary_tags Thread.current[:yabeda_temporary_tags] ||= {} end + # Limit all group metrics to specific adapters only + # + # @param adapter_names [Array] Names of adapters to use + def adapter(*adapter_names, group: @group) + raise ConfigurationError, "Adapter limitation can't be defined outside of group" unless group + + Yabeda.groups[group] ||= Yabeda::Group.new(group) + Yabeda.groups[group].adapter(*adapter_names) + end + private def register_metric(metric) @@ -99,7 +109,7 @@ def register_metric(metric) ::Yabeda.define_singleton_method(name) { metric } ::Yabeda.metrics[name] = metric register_group_for(metric) if metric.group - ::Yabeda.adapters.each_value { |adapter| adapter.register!(metric) } if ::Yabeda.configured? + metric.adapters.each_value { |adapter| adapter.register!(metric) } if ::Yabeda.configured? metric end diff --git a/lib/yabeda/gauge.rb b/lib/yabeda/gauge.rb index 9b75a49..5f76c7b 100644 --- a/lib/yabeda/gauge.rb +++ b/lib/yabeda/gauge.rb @@ -6,7 +6,7 @@ class Gauge < Metric def set(tags, value) all_tags = ::Yabeda::Tags.build(tags, group) values[all_tags] = value - ::Yabeda.adapters.each_value do |adapter| + adapters.each_value do |adapter| adapter.perform_gauge_set!(self, all_tags, value) end value diff --git a/lib/yabeda/group.rb b/lib/yabeda/group.rb index 5d1a47f..787b664 100644 --- a/lib/yabeda/group.rb +++ b/lib/yabeda/group.rb @@ -19,6 +19,13 @@ def default_tag(key, value) @default_tags[key] = value end + def adapter(*adapter_names) + return @adapter if adapter_names.empty? + + @adapter ||= Concurrent::Array.new + @adapter.push(*adapter_names) + end + def register_metric(metric) define_singleton_method(metric.name) { metric } end diff --git a/lib/yabeda/histogram.rb b/lib/yabeda/histogram.rb index 035c424..bf7cda7 100644 --- a/lib/yabeda/histogram.rb +++ b/lib/yabeda/histogram.rb @@ -20,7 +20,7 @@ def measure(tags, value = nil) all_tags = ::Yabeda::Tags.build(tags, group) values[all_tags] = value - ::Yabeda.adapters.each_value do |adapter| + adapters.each_value do |adapter| adapter.perform_histogram_measure!(self, all_tags, value) end value diff --git a/lib/yabeda/metric.rb b/lib/yabeda/metric.rb index c999b22..a8c4fff 100644 --- a/lib/yabeda/metric.rb +++ b/lib/yabeda/metric.rb @@ -14,6 +14,9 @@ class Metric option :per, optional: true, comment: "Per which unit is measured `unit`. E.g. `call` as in seconds per call" option :group, optional: true, comment: "Category name for grouping metrics" option :aggregation, optional: true, comment: "How adapters should aggregate values from different processes" + # rubocop:disable Layout/LineLength + option :adapter, optional: true, comment: "Monitoring system adapter to register metric in and report metric values to (other adapters won't be used)" + # rubocop:enable Layout/LineLength # Returns the value for the given label set def get(labels = {}) @@ -25,7 +28,7 @@ def values end # Returns allowed tags for metric (with account for global and group-level +default_tags+) - # @return Array + # @return [Array] def tags (Yabeda.groups[group].default_tags.keys + Array(super)).uniq end @@ -33,5 +36,31 @@ def tags def inspect "#<#{self.class.name}: #{[@group, @name].compact.join('.')}>" end + + # Returns the metric adapters + # @return [Hash] + def adapters + return ::Yabeda.adapters unless adapter + + @adapters ||= begin + adapter_names = Array(adapter) + unknown_adapters = adapter_names - ::Yabeda.adapters.keys + + if unknown_adapters.any? + raise ConfigurationError, + "invalid adapter option #{adapter.inspect} in metric #{inspect}" + end + + ::Yabeda.adapters.slice(*adapter_names) + end + end + + # Redefined option reader to get group-level adapter if not set on metric level + # @api private + def adapter + return ::Yabeda.groups[group]&.adapter if @adapter == Dry::Initializer::UNDEFINED + + super + end end end diff --git a/lib/yabeda/summary.rb b/lib/yabeda/summary.rb index b118ecc..b84d1d1 100644 --- a/lib/yabeda/summary.rb +++ b/lib/yabeda/summary.rb @@ -18,7 +18,7 @@ def observe(tags, value = nil) all_tags = ::Yabeda::Tags.build(tags, group) values[all_tags] = value - ::Yabeda.adapters.each_value do |adapter| + adapters.each_value do |adapter| adapter.perform_summary_observe!(self, all_tags, value) end value diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5b7cf8d..377d86d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,6 +2,7 @@ require "bundler/setup" require "yabeda" +require "yabeda/base_adapter" require "pry" RSpec.configure do |config| diff --git a/spec/yabeda/counter_spec.rb b/spec/yabeda/counter_spec.rb index bff7b79..1b404b7 100644 --- a/spec/yabeda/counter_spec.rb +++ b/spec/yabeda/counter_spec.rb @@ -24,4 +24,26 @@ increment_counter expect(adapter).to have_received(:perform_counter_increment!).with(counter, built_tags, metric_value) end + + context "with adapter option" do + let(:counter) { Yabeda.counter_with_adapter } + let(:another_adapter) { instance_double(Yabeda::BaseAdapter, perform_counter_increment!: true, register!: true) } + + before do + Yabeda.register_adapter(:another_adapter, another_adapter) + Yabeda.configure do + counter :counter_with_adapter, adapter: :test_adapter + end + Yabeda.configure! unless Yabeda.already_configured? + end + + it "execute perform_counter_increment! method of adapter with name :test_adapter" do + increment_counter + + aggregate_failures do + expect(adapter).to have_received(:perform_counter_increment!).with(counter, built_tags, metric_value) + expect(another_adapter).not_to have_received(:perform_counter_increment!) + end + end + end end diff --git a/spec/yabeda/dsl/class_methods_spec.rb b/spec/yabeda/dsl/class_methods_spec.rb index 3b2fe9c..7b8159f 100644 --- a/spec/yabeda/dsl/class_methods_spec.rb +++ b/spec/yabeda/dsl/class_methods_spec.rb @@ -156,4 +156,59 @@ end end end + + describe ".configure" do + subject(:configure) { Yabeda.configure(&block) } + + let(:block) { proc { histogram :test_histogram, buckets: [42] } } + + before do + Yabeda.register_adapter(:another_adapter, Yabeda::TestAdapter.instance) + Yabeda.configure! unless Yabeda.configured? + end + + it "register metric" do + configure + + expect(Yabeda.test_histogram).to be_a(Yabeda::Histogram) + end + + context "when got metric with adapter option" do + let(:block) { proc { histogram :invalid_test, buckets: [42], adapter: :another_adapter } } + + it { expect { configure }.not_to raise_error } + + context "when option is invalid" do + let(:block) { proc { histogram :invalid_test, buckets: [42], adapter: :invalid } } + + it { expect { configure }.to raise_error(Yabeda::ConfigurationError, /invalid adapter option/) } + end + end + end + + describe ".adapter" do + context "when group is not defined" do + it "raises an error" do + expect do + Yabeda.configure { adapter :test } + Yabeda.configure! unless Yabeda.already_configured? + end.to raise_error(Yabeda::ConfigurationError, /can't be defined outside of group/) + end + end + + context "with a specified group that does not exist" do + before do + Yabeda.configure { adapter :test, group: :adapter_group } + Yabeda.configure! unless Yabeda.already_configured? + end + + it "creates the group" do + expect(Yabeda.groups[:adapter_group]).to be_a(Yabeda::Group) + end + + it "defines the default tag" do + expect(Yabeda.groups[:adapter_group].adapter).to eq(%i[test]) + end + end + end end diff --git a/spec/yabeda/gauge_spec.rb b/spec/yabeda/gauge_spec.rb index 2c4ab79..56f696a 100644 --- a/spec/yabeda/gauge_spec.rb +++ b/spec/yabeda/gauge_spec.rb @@ -74,4 +74,26 @@ end end end + + context "with adapter option" do + let(:gauge) { Yabeda.gauge_with_adapter } + let(:another_adapter) { instance_double(Yabeda::BaseAdapter, perform_gauge_set!: true, register!: true) } + + before do + Yabeda.register_adapter(:another_adapter, another_adapter) + Yabeda.configure do + gauge :gauge_with_adapter, adapter: :test_adapter + end + Yabeda.configure! unless Yabeda.already_configured? + end + + it "execute perform_counter_increment! method of adapter with name :test_adapter" do + set_gauge + + aggregate_failures do + expect(adapter).to have_received(:perform_gauge_set!).with(gauge, built_tags, metric_value) + expect(another_adapter).not_to have_received(:perform_gauge_set!) + end + end + end end diff --git a/spec/yabeda/histogram_spec.rb b/spec/yabeda/histogram_spec.rb index 13b14b8..8900a48 100644 --- a/spec/yabeda/histogram_spec.rb +++ b/spec/yabeda/histogram_spec.rb @@ -56,4 +56,26 @@ expect { measure_histogram }.to raise_error(ArgumentError) end end + + context "with adapter option" do + let(:histogram) { Yabeda.histogram_with_adapter } + let(:another_adapter) { instance_double(Yabeda::BaseAdapter, perform_histogram_measure!: true, register!: true) } + + before do + Yabeda.register_adapter(:another_adapter, another_adapter) + Yabeda.configure do + histogram :histogram_with_adapter, adapter: :test_adapter, buckets: [1, 10, 100] + end + Yabeda.configure! unless Yabeda.already_configured? + end + + it "execute perform_counter_increment! method of adapter with name :test_adapter" do + measure_histogram + + aggregate_failures do + expect(adapter).to have_received(:perform_histogram_measure!).with(histogram, built_tags, metric_value) + expect(another_adapter).not_to have_received(:perform_histogram_measure!) + end + end + end end diff --git a/spec/yabeda/metric_spec.rb b/spec/yabeda/metric_spec.rb index 1599acd..652dfde 100644 --- a/spec/yabeda/metric_spec.rb +++ b/spec/yabeda/metric_spec.rb @@ -61,4 +61,63 @@ it { is_expected.to match_array(%i[foo bar baz]) } end end + + describe "#adapters" do + subject(:metric_adapters) { metric.adapters } + + let(:adapter_name) { :test_adapter } + let(:adapter) { instance_double(Yabeda::BaseAdapter, register!: true) } + let(:another_adapter_name) { :another_test_adapter } + let(:another_adapter) { instance_double(Yabeda::BaseAdapter, register!: true) } + + before do + Yabeda.register_adapter(adapter_name, adapter) + Yabeda.register_adapter(another_adapter_name, another_adapter) + end + + it "returns default Yabeda adapters by default" do + expect(metric_adapters.object_id).to eq(Yabeda.adapters.object_id) + end + + context "when metric has option adapter" do + let(:options) { { tags: %i[foo bar], adapter: :test_adapter } } + + it "returns only defined in option adapter" do + aggregate_failures do + expect(metric_adapters).to eq({ + adapter_name => adapter, + }) + expect(metric_adapters.size).to eq(1) + end + end + + context "when adapter option is invalid" do + let(:options) { { tags: %i[foo bar], adapter: :invalid } } + + it "raises error" do + expect { metric_adapters }.to raise_error(Yabeda::ConfigurationError, /invalid adapter option/) + end + end + end + + context "when metric has no adapter option but group does" do + let(:options) { { group: :adapter_group } } + + before do + Yabeda.configure do + group(:adapter_group) { adapter :test_adapter } + end + Yabeda.configure! unless Yabeda.already_configured? + end + + it "returns only adapter defined in group" do + aggregate_failures do + expect(metric_adapters).to eq({ + adapter_name => adapter, + }) + expect(metric_adapters.size).to eq(1) + end + end + end + end end diff --git a/spec/yabeda/summary_spec.rb b/spec/yabeda/summary_spec.rb index 0a27dae..9cc2f81 100644 --- a/spec/yabeda/summary_spec.rb +++ b/spec/yabeda/summary_spec.rb @@ -54,4 +54,26 @@ expect { observe_summary }.to raise_error(ArgumentError) end end + + context "with adapter option" do + let(:summary) { Yabeda.summary_with_adapter } + let(:another_adapter) { instance_double(Yabeda::BaseAdapter, perform_summary_observe!: true, register!: true) } + + before do + Yabeda.register_adapter(:another_adapter, another_adapter) + Yabeda.configure do + summary :summary_with_adapter, adapter: :test_adapter + end + Yabeda.configure! unless Yabeda.already_configured? + end + + it "execute perform_counter_increment! method of adapter with name :test_adapter" do + observe_summary + + aggregate_failures do + expect(adapter).to have_received(:perform_summary_observe!).with(summary, built_tags, metric_value) + expect(another_adapter).not_to have_received(:perform_summary_observe!) + end + end + end end diff --git a/spec/yabeda_spec.rb b/spec/yabeda_spec.rb index 2a438b1..9464ab7 100644 --- a/spec/yabeda_spec.rb +++ b/spec/yabeda_spec.rb @@ -23,6 +23,28 @@ it { expect { configure! }.to raise_error(Yabeda::AlreadyConfiguredError) } end + + context "when set valid adapter option in metric" do + let(:adapter) { instance_double(Yabeda::BaseAdapter, register!: true, debug!: true) } + + before do + described_class.configure { counter(:test_counter, adapter: :test_adapter) } + described_class.register_adapter(:test_adapter, adapter) + end + + it { expect { configure! }.to change(described_class, :configured?).to(true) } + end + + context "when set invalid adapter option in metric" do + let(:adapter) { instance_double(Yabeda::BaseAdapter, register!: true, debug!: true) } + + before do + described_class.configure { counter(:test_counter, adapter: :invalid) } + described_class.register_adapter(:test_adapter, adapter) + end + + it { expect { configure! }.to raise_error(Yabeda::ConfigurationError, /invalid adapter option/) } + end end describe ".debug!" do @@ -95,4 +117,22 @@ end end end + + describe ".register_adapter" do + subject(:register_adapter) { described_class.register_adapter(name, adapter) } + + let(:name) { :test_adapter } + let(:adapter) { instance_double(Yabeda::BaseAdapter, register!: true) } + + before do + described_class.configure { histogram :test, buckets: [42] } + described_class.configure! unless described_class.configured? + end + + it "register metric for adapter" do + register_adapter + + expect(adapter).to have_received(:register!).with(described_class.test) + end + end end