diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dcbf4d..0141f1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 0.9.0 - 2021-05-07 + +### Added + +- Ability to set global metric tags only for a specific group [#19](https://github.com/yabeda-rb/yabeda/pull/19) by [@liaden] + ## 0.8.0 - 2020-08-21 ### Added @@ -99,3 +105,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. [@Envek]: https://github.com/Envek "Andrey Novikov" [@dsalahutdinov]: https://github.com/dsalahutdinov "Dmitry Salahutdinov" [@asusikov]: https://github.com/asusikov "Alexander Susikov" +[@liaden]: https://github.com/liaden "Joel Johnson" diff --git a/README.md b/README.md index 86e5ed4..93eacfe 100644 --- a/README.md +++ b/README.md @@ -79,10 +79,14 @@ And then execute: end ``` - 5. _Optionally_ setup default tags that will be added to all metrics + 5. _Optionally_ setup default tags for all appropriate metrics ```ruby Yabeda.configure do + # matches all metrics in all groups default_tag :rails_environment, 'production' + + # matches all metrics in the :your_app group + default_tag :tag_name, 'override', group: :your_app end # You can redefine them for limited amount of time @@ -91,8 +95,19 @@ And then execute: end ``` - 6. See the docs for the adapter you're using - 7. Enjoy! + **Note**: any usage of `with_tags` **must** have all those tags defined on all metrics that are generated in the block. + + 6. _Optionally_ override default tags using precedence: + + The tag precedence from high to low is: + + * Manually specified tags + * Thread local tags (specified by `Yabeda.with_tags`) + * Group specific tags + * Global tags + + 7. See the docs for the adapter you're using + 8. Enjoy! ## Available monitoring system adapters diff --git a/lib/yabeda.rb b/lib/yabeda.rb index 5e5de57..2d5fc94 100644 --- a/lib/yabeda.rb +++ b/lib/yabeda.rb @@ -20,7 +20,9 @@ def metrics # @return [Hash] All registered metrics def groups - @groups ||= Concurrent::Hash.new + @groups ||= Concurrent::Hash.new.tap do |hash| + hash[nil] = Yabeda::GlobalGroup.new(nil) + end end # @return [Hash] All loaded adapters @@ -33,7 +35,7 @@ def collectors @collectors ||= Concurrent::Array.new end - # @return [Hash] All added default tags + # @return [Hash] All added global default tags def default_tags @default_tags ||= Concurrent::Hash.new end @@ -86,14 +88,18 @@ def configure! # Forget all the configuration. # For testing purposes as it doesn't rollback changes in adapters. # @api private + # rubocop: disable Metrics/AbcSize def reset! default_tags.clear adapters.clear - groups.clear - metrics.clear + groups.each_key { |group| singleton_class.send(:remove_method, group) if group && respond_to?(group) } + @groups = nil + metrics.each_key { |metric| singleton_class.send(:remove_method, metric) if respond_to?(metric) } + @metrics = nil collectors.clear configurators.clear instance_variable_set(:@configured_by, nil) end + # rubocop: enable Metrics/AbcSize end end diff --git a/lib/yabeda/counter.rb b/lib/yabeda/counter.rb index 53a775a..e70b177 100644 --- a/lib/yabeda/counter.rb +++ b/lib/yabeda/counter.rb @@ -4,7 +4,7 @@ module Yabeda # Growing-only counter class Counter < Metric def increment(tags, by: 1) - all_tags = ::Yabeda::Tags.build(tags) + all_tags = ::Yabeda::Tags.build(tags, group) values[all_tags] += by ::Yabeda.adapters.each do |_, adapter| adapter.perform_counter_increment!(self, all_tags, by) diff --git a/lib/yabeda/dsl/class_methods.rb b/lib/yabeda/dsl/class_methods.rb index d304548..dd54e7a 100644 --- a/lib/yabeda/dsl/class_methods.rb +++ b/lib/yabeda/dsl/class_methods.rb @@ -5,6 +5,7 @@ require "yabeda/gauge" require "yabeda/histogram" require "yabeda/group" +require "yabeda/global_group" require "yabeda/dsl/metric_builder" module Yabeda @@ -30,6 +31,7 @@ def collect(&block) # (like NewRelic) it is treated individually and has a special meaning. def group(group_name) @group = group_name + Yabeda.groups[@group] ||= Yabeda::Group.new(@group) return unless block_given? yield @@ -58,24 +60,30 @@ def histogram(*args, **kwargs, &block) # # @param name [Symbol] Name of default tag # @param value [String] Value of default tag - def default_tag(name, value) - ::Yabeda.default_tags[name] = value + def default_tag(name, value, group: @group) + if group + Yabeda.groups[group] ||= Yabeda::Group.new(group) + Yabeda.groups[group].default_tag(name, value) + else + Yabeda.default_tags[name] = value + end end # Redefine default tags for a limited amount of time # @param tags Hash{Symbol=>#to_s} def with_tags(**tags) - Thread.current[:yabeda_temporary_tags] = tags + previous_temp_tags = temporary_tags + Thread.current[:yabeda_temporary_tags] = Thread.current[:yabeda_temporary_tags].merge(tags) yield ensure - Thread.current[:yabeda_temporary_tags] = {} + Thread.current[:yabeda_temporary_tags] = previous_temp_tags end # Get tags set by +with_tags+ # @api private # @return Hash def temporary_tags - Thread.current[:yabeda_temporary_tags] || {} + Thread.current[:yabeda_temporary_tags] ||= {} end private @@ -95,9 +103,10 @@ def register_group_for(metric) if group.nil? group = Group.new(metric.group) ::Yabeda.groups[metric.group] = group - ::Yabeda.define_singleton_method(metric.group) { group } end + ::Yabeda.define_singleton_method(metric.group) { group } unless ::Yabeda.respond_to?(metric.group) + group.register_metric(metric) end end diff --git a/lib/yabeda/gauge.rb b/lib/yabeda/gauge.rb index fbf9e90..3eadbe0 100644 --- a/lib/yabeda/gauge.rb +++ b/lib/yabeda/gauge.rb @@ -4,7 +4,7 @@ module Yabeda # Arbitrary value, can be changed in both sides class Gauge < Metric def set(tags, value) - all_tags = ::Yabeda::Tags.build(tags) + all_tags = ::Yabeda::Tags.build(tags, group) values[all_tags] = value ::Yabeda.adapters.each do |_, adapter| adapter.perform_gauge_set!(self, all_tags, value) diff --git a/lib/yabeda/global_group.rb b/lib/yabeda/global_group.rb new file mode 100644 index 0000000..bdd016e --- /dev/null +++ b/lib/yabeda/global_group.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require "forwardable" +require_relative "./group" + +module Yabeda + # Represents implicit global group + class GlobalGroup < Group + extend Forwardable + + def_delegators ::Yabeda, :default_tags, :default_tag + end +end diff --git a/lib/yabeda/group.rb b/lib/yabeda/group.rb index 4697b5c..5d1a47f 100644 --- a/lib/yabeda/group.rb +++ b/lib/yabeda/group.rb @@ -9,6 +9,16 @@ class Group param :name + def default_tags + @default_tags ||= Concurrent::Hash.new + ::Yabeda.default_tags.merge(@default_tags) + end + + def default_tag(key, value) + @default_tags ||= Concurrent::Hash.new + @default_tags[key] = value + 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 f6914e5..bbd5748 100644 --- a/lib/yabeda/histogram.rb +++ b/lib/yabeda/histogram.rb @@ -7,7 +7,7 @@ class Histogram < Metric option :buckets def measure(tags, value) - all_tags = ::Yabeda::Tags.build(tags) + all_tags = ::Yabeda::Tags.build(tags, group) values[all_tags] = value ::Yabeda.adapters.each do |_, adapter| adapter.perform_histogram_measure!(self, all_tags, value) diff --git a/lib/yabeda/metric.rb b/lib/yabeda/metric.rb index f092589..855f9f7 100644 --- a/lib/yabeda/metric.rb +++ b/lib/yabeda/metric.rb @@ -17,15 +17,17 @@ class Metric # Returns the value for the given label set def get(labels = {}) - values[::Yabeda::Tags.build(labels)] + values[::Yabeda::Tags.build(labels, group)] end def values @values ||= Concurrent::Hash.new end + # Returns allowed tags for metric (with account for global and group-level +default_tags+) + # @return Array def tags - (Yabeda.default_tags.keys + Array(super)).uniq + (Yabeda.groups[group].default_tags.keys + Array(super)).uniq end end end diff --git a/lib/yabeda/tags.rb b/lib/yabeda/tags.rb index b83d4a5..1a0787b 100644 --- a/lib/yabeda/tags.rb +++ b/lib/yabeda/tags.rb @@ -3,8 +3,12 @@ module Yabeda # Class to merge tags class Tags - def self.build(tags) - ::Yabeda.default_tags.merge(Yabeda.temporary_tags).merge(tags) + def self.build(tags, group_name = nil) + Yabeda.default_tags.dup.tap do |result| + result.merge!(Yabeda.groups[group_name].default_tags) if group_name + result.merge!(Yabeda.temporary_tags) + result.merge!(tags) + end end end end diff --git a/lib/yabeda/version.rb b/lib/yabeda/version.rb index 53bdbbb..b9e924a 100644 --- a/lib/yabeda/version.rb +++ b/lib/yabeda/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Yabeda - VERSION = "0.8.0" + VERSION = "0.9.0" end diff --git a/spec/yabeda/counter_spec.rb b/spec/yabeda/counter_spec.rb index 1ece627..633bb0a 100644 --- a/spec/yabeda/counter_spec.rb +++ b/spec/yabeda/counter_spec.rb @@ -14,7 +14,7 @@ counter :test_counter end Yabeda.configure! - allow(Yabeda::Tags).to receive(:build).with(tags).and_return(built_tags) + allow(Yabeda::Tags).to receive(:build).with(tags, anything).and_return(built_tags) ::Yabeda.register_adapter(:test_adapter, adapter) end diff --git a/spec/yabeda/dsl/class_methods_spec.rb b/spec/yabeda/dsl/class_methods_spec.rb index 791ada8..a974af5 100644 --- a/spec/yabeda/dsl/class_methods_spec.rb +++ b/spec/yabeda/dsl/class_methods_spec.rb @@ -1,24 +1,6 @@ # frozen_string_literal: true RSpec.describe Yabeda::DSL::ClassMethods do - after do - if Yabeda.instance_variable_defined?(:@groups) - Yabeda.instance_variable_get(:@groups).each_key do |group| - Yabeda.singleton_class.send(:remove_method, group) - end - Yabeda.remove_instance_variable(:@groups) - end - - if Yabeda.instance_variable_defined?(:@metrics) - Yabeda.instance_variable_get(:@metrics).each_key do |metric| - Yabeda.singleton_class.send(:remove_method, metric) - end - Yabeda.remove_instance_variable(:@metrics) - end - - ::Yabeda.default_tags.clear - end - describe ".group" do context "without block" do before do @@ -125,5 +107,40 @@ it { is_expected.to eq(environment: "test") } end + + context "with a specified group that does not exist" do + before do + Yabeda.configure { default_tag :environment, "test", group: :missing_group } + Yabeda.configure! + end + + it "creates the group" do + expect(Yabeda.groups[:missing_group]).to be_a(Yabeda::Group) + end + + it "defines the default tag" do + expect(Yabeda.groups[:missing_group].default_tags).to eq(environment: "test") + end + end + + context "when specified group is defined after default_tag" do + before do + Yabeda.configure { default_tag :environment, "test", group: :missing_group } + Yabeda.configure do + group :missing_group + default_tag :key, "value" + gauge :test_gauge, comment: "..." + end + Yabeda.configure! + end + + it "defines all the tags" do + expect(Yabeda.groups[:missing_group].default_tags).to eq(environment: "test", key: "value") + end + + it "test_gauge has all the tags defined" do + expect(Yabeda.missing_group.test_gauge.tags).to eq(%i[environment key]) + end + end end end diff --git a/spec/yabeda/gauge_spec.rb b/spec/yabeda/gauge_spec.rb index ed64892..d4a0f94 100644 --- a/spec/yabeda/gauge_spec.rb +++ b/spec/yabeda/gauge_spec.rb @@ -14,7 +14,7 @@ gauge :test_gauge end Yabeda.configure! - allow(Yabeda::Tags).to receive(:build).with(tags).and_return(built_tags) + allow(Yabeda::Tags).to receive(:build).with(tags, anything).and_return(built_tags) ::Yabeda.register_adapter(:test_adapter, adapter) end diff --git a/spec/yabeda/group_spec.rb b/spec/yabeda/group_spec.rb new file mode 100644 index 0000000..b63ba97 --- /dev/null +++ b/spec/yabeda/group_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +RSpec.describe Yabeda::Group do + let(:name) { nil } + + let(:group) { described_class.new(name) } + + before do + Yabeda.groups[name] = group + end + + after { Yabeda.reset! } + + describe "default tags" do + context "when on the top level group" do + it "is an empty by default" do + expect(group.default_tags).to be_empty + end + end + + context "when within a named group" do + let(:name) { :group1 } + + it "includes top level default_tags" do + Yabeda.default_tag :tag, "default" + expect(group.default_tags).to eq(tag: "default") + end + + it "overrides top level default_tags" do + Yabeda.default_tag :tag, "default" + group.default_tag :tag, "overridden" + expect(Yabeda.groups[nil].default_tags).to eq(tag: "default") + expect(group.default_tags).to eq(tag: "overridden") + end + end + end +end diff --git a/spec/yabeda/histogram_spec.rb b/spec/yabeda/histogram_spec.rb index c34a5a1..3f71b6c 100644 --- a/spec/yabeda/histogram_spec.rb +++ b/spec/yabeda/histogram_spec.rb @@ -14,7 +14,7 @@ histogram :test_histogram, buckets: [1, 10, 100] end Yabeda.configure! - allow(Yabeda::Tags).to receive(:build).with(tags).and_return(built_tags) + allow(Yabeda::Tags).to receive(:build).with(tags, anything).and_return(built_tags) ::Yabeda.register_adapter(:test_adapter, adapter) end diff --git a/spec/yabeda/tags_spec.rb b/spec/yabeda/tags_spec.rb index 50f1046..da77f7c 100644 --- a/spec/yabeda/tags_spec.rb +++ b/spec/yabeda/tags_spec.rb @@ -2,9 +2,10 @@ RSpec.describe Yabeda::Tags do describe ".build" do - subject(:result) { described_class.build(tags) } + subject(:result) { described_class.build(tags, group) } let(:tags) { { controller: "foo" } } + let(:group) { nil } context "when default tags are not set" do it { is_expected.to eq(controller: "foo") } @@ -56,6 +57,34 @@ end expect(result).to eq controller: "foo", action: "whatever", format: "html", id: "100500" end + + it "permits nesting with_tags" do + Yabeda.with_tags(action: "index") do + Yabeda.with_tags(format: "json") do + expect(result).to include(format: "json", action: "index") + end + end + end + + it "restores previous with_tags after nesting" do + Yabeda.with_tags(action: "index") do + Yabeda.with_tags(format: "json") {} + expect(result).to include(format: "html", action: "index") + end + end + end + + context "when group tags are set" do + let(:group) { :foo } + + before do + Yabeda.configure do + default_tag :bar, "baz", group: :foo + end + Yabeda.configure! + end + + it { is_expected.to eq(controller: "foo", bar: "baz") } end end end