From a475a5c6fca7b5454db83f864d32864ac9a691c1 Mon Sep 17 00:00:00 2001 From: Holger Just Date: Wed, 20 Dec 2017 23:13:34 +0100 Subject: [PATCH] Use the ClassRegistry to register filters This removes the requirement that filters need to be defined in the Rackstash::Filters namespace in order to be usable with a short name. Filters can be registered with an arbitrary name. As before, raw blocks / procs can be defined as ad-hoc filters. --- lib/rackstash.rb | 9 ++ lib/rackstash/filter.rb | 102 +++++++++++------------ lib/rackstash/filter/clear_color.rb | 4 + lib/rackstash/filter/default_fields.rb | 4 + lib/rackstash/filter/default_tags.rb | 4 + lib/rackstash/filter/drop_if.rb | 4 + lib/rackstash/filter/rename.rb | 4 + lib/rackstash/filter/replace.rb | 4 + lib/rackstash/filter/truncate_message.rb | 4 + lib/rackstash/filter/update.rb | 4 + spec/rackstash/filter_chain_spec.rb | 20 +++-- spec/rackstash/filter_spec.rb | 63 +++++++------- 12 files changed, 134 insertions(+), 92 deletions(-) diff --git a/lib/rackstash.rb b/lib/rackstash.rb index 29f50e8..4901556 100644 --- a/lib/rackstash.rb +++ b/lib/rackstash.rb @@ -172,3 +172,12 @@ require 'rackstash/adapter/file' require 'rackstash/adapter/logger' require 'rackstash/adapter/io' require 'rackstash/adapter/null' + +require 'rackstash/filter/clear_color' +require 'rackstash/filter/default_fields' +require 'rackstash/filter/default_tags' +require 'rackstash/filter/drop_if' +require 'rackstash/filter/rename' +require 'rackstash/filter/replace' +require 'rackstash/filter/truncate_message' +require 'rackstash/filter/update' diff --git a/lib/rackstash/filter.rb b/lib/rackstash/filter.rb index f8f515a..7098af8 100644 --- a/lib/rackstash/filter.rb +++ b/lib/rackstash/filter.rb @@ -5,13 +5,7 @@ # This software may be modified and distributed under the terms # of the MIT license. See the LICENSE.txt file for details. -require 'rackstash/filter/clear_color' -require 'rackstash/filter/default_fields' -require 'rackstash/filter/default_tags' -require 'rackstash/filter/drop_if' -require 'rackstash/filter/rename' -require 'rackstash/filter/replace' -require 'rackstash/filter/truncate_message' +require 'rackstash/class_registry' module Rackstash # Filters are part of a {Flow} where they can alter the log event before it is @@ -29,57 +23,55 @@ module Rackstash # A filter can be any object responding to `call`, e.g. a Proc or a concrete # class inside this module. module Filter - # Create a new filter instance from the specified class and the given - # arguments. The class can be given as an actual class or as the name of a - # filter in which case we are resolving it to a class defined inside the - # {Rackstash::Filter} namespace. - # - # @param klass [Class, Symbol, String, #call] a description of the class - # from which we are creating a new filter object. When giving a `Class`, - # we are using it as is. When giving a `String` or `Symbol`, we are - # determining the associated class from the {Rackstash::Filter} module - # and create an instance of that. When giving an object which responds to - # `call` already, we return it unchanged, ignoring any additional passed - # `args`. - # @param args [Array] an optional list of arguments which is passed to the - # initializer for the new filter object. - # @raise [TypeError] if we can not create a new Filter object from `class` - # @raise [NameError] if we could not find a filter class for the specified - # class name - # @return [Object] a new filter object - def self.build(klass, *args, &block) - case klass - when Class - klass.new(*args, &block) - when Symbol, String - filter_class_name = klass.to_s - .sub(/^[a-z\d]*/) { $&.capitalize } - .gsub(/(?:_)([a-z\d]*)/) { $1.capitalize } - .to_sym - filter_class = const_get(filter_class_name, false) - filter_class.new(*args, &block) - when ->(filter) { filter.respond_to?(:call) } - klass - else - raise TypeError, "Can not build filter for #{klass.inspect}" + class << self + # @param filter_class [Class] a class from which a new filter can be + # created. Filter objects must respond to `call` and accept an event + # hash. + # @param filter_names [Array] one or more names for the + # registered `filter_class`. Using these names, the user can create a + # new filter object from the registered class in {.build}. + # @raise [TypeError] if objects of type were passed + # @return [Class] the passed `filter_class` + def register(filter_class, *filter_names) + filter_names.flatten.each do |name| + registry[name] = filter_class + end + filter_class end - end - # @return [Hash Class>] a Hash with names of filters and their - # respective classes which can be used with {Filter.build} to create a - # new filter object - def self.known - constants.each_with_object({}) do |const, known| - filter_class = const_get(const, false) - next unless filter_class.is_a?(Class) + # @return [ClassRegistry] the registry object which allows to register and + # retrieve available filter classes + def registry + @registry ||= Rackstash::ClassRegistry.new('filter'.freeze) + end - filter_class_name = const.to_s - .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') - .gsub(/([a-z\d])([A-Z])/, '\1_\2') - .downcase - .to_sym - - known[filter_class_name] = filter_class + # Create a new filter instance from the specified class and the given + # arguments. The class can be given as an actual class or as the name of a + # filter in which case we are resolving it to a class registered to the + # {.registry}. + # + # @param filter_spec [Class, Symbol, String, #call] a description of the + # class from which we are creating a new filter object. When giving a + # `Class`, we are using it as is to create a new filter object with the + # supplied `args` and `block`. When giving a `String` or `Symbol`, we + # first use the filter registry to find the matching class. With that, + # we then create a filter object as before. When giving an object which + # responds to `call` already (e.g. a `Proc`, we return it unchanged, + # ignoring any additional passed `args`. + # @param args [Array] an optional list of arguments which is passed to the + # initializer for the new filter object. + # @raise [TypeError] if we can not create a new filter object from the + # given `filter_spec`, usually because it is an unsupported type + # @raise [KeyError] if we could not find a filter class in the registry + # for the specified class name + # @return [Object] a new filter object + def build(filter_spec, *args, &block) + case filter_spec + when ->(filter) { filter.respond_to?(:call) } + filter_spec + else + registry[filter_spec].new(*args, &block) + end end end end diff --git a/lib/rackstash/filter/clear_color.rb b/lib/rackstash/filter/clear_color.rb index 3127c5d..7e90357 100644 --- a/lib/rackstash/filter/clear_color.rb +++ b/lib/rackstash/filter/clear_color.rb @@ -5,6 +5,8 @@ # This software may be modified and distributed under the terms # of the MIT license. See the LICENSE.txt file for details. +require 'rackstash/filter' + module Rackstash module Filter # Remove all ANSI color codes from the `"message"` field of the given event @@ -40,5 +42,7 @@ module Rackstash event end end + + register ClearColor, :clear_color end end diff --git a/lib/rackstash/filter/default_fields.rb b/lib/rackstash/filter/default_fields.rb index 735c288..8c757dd 100644 --- a/lib/rackstash/filter/default_fields.rb +++ b/lib/rackstash/filter/default_fields.rb @@ -5,6 +5,8 @@ # This software may be modified and distributed under the terms # of the MIT license. See the LICENSE.txt file for details. +require 'rackstash/filter' + module Rackstash module Filter # The {DefaultFields} filter allows to define fields which should be added @@ -56,5 +58,7 @@ module Rackstash event.merge!(fields, &resolver) end end + + register DefaultFields, :default_fields end end diff --git a/lib/rackstash/filter/default_tags.rb b/lib/rackstash/filter/default_tags.rb index 71b010b..c95cf09 100644 --- a/lib/rackstash/filter/default_tags.rb +++ b/lib/rackstash/filter/default_tags.rb @@ -5,6 +5,8 @@ # This software may be modified and distributed under the terms # of the MIT license. See the LICENSE.txt file for details. +require 'rackstash/filter' + module Rackstash module Filter # The {DefaultTags} filter allows to define tags which should be added @@ -50,5 +52,7 @@ module Rackstash event end end + + register DefaultTags, :default_tags end end diff --git a/lib/rackstash/filter/drop_if.rb b/lib/rackstash/filter/drop_if.rb index b1fb873..9b4e4bf 100644 --- a/lib/rackstash/filter/drop_if.rb +++ b/lib/rackstash/filter/drop_if.rb @@ -5,6 +5,8 @@ # This software may be modified and distributed under the terms # of the MIT license. See the LICENSE.txt file for details. +require 'rackstash/filter' + module Rackstash module Filter # Skip the further processing of the event if the provided condition is @@ -50,5 +52,7 @@ module Rackstash event end end + + register DropIf, :drop_if end end diff --git a/lib/rackstash/filter/rename.rb b/lib/rackstash/filter/rename.rb index 98364d7..2156065 100644 --- a/lib/rackstash/filter/rename.rb +++ b/lib/rackstash/filter/rename.rb @@ -5,6 +5,8 @@ # This software may be modified and distributed under the terms # of the MIT license. See the LICENSE.txt file for details. +require 'rackstash/filter' + module Rackstash module Filter # Rename one or more fields in the given event. @@ -38,5 +40,7 @@ module Rackstash event end end + + register Rename, :rename end end diff --git a/lib/rackstash/filter/replace.rb b/lib/rackstash/filter/replace.rb index 88c95ba..8b5d31b 100644 --- a/lib/rackstash/filter/replace.rb +++ b/lib/rackstash/filter/replace.rb @@ -5,6 +5,8 @@ # This software may be modified and distributed under the terms # of the MIT license. See the LICENSE.txt file for details. +require 'rackstash/filter' + module Rackstash module Filter # Replace fields in the given event with new values. A new value can be @@ -51,5 +53,7 @@ module Rackstash event end end + + register Replace, :replace end end diff --git a/lib/rackstash/filter/truncate_message.rb b/lib/rackstash/filter/truncate_message.rb index 1d25063..f0e74d4 100644 --- a/lib/rackstash/filter/truncate_message.rb +++ b/lib/rackstash/filter/truncate_message.rb @@ -5,6 +5,8 @@ # This software may be modified and distributed under the terms # of the MIT license. See the LICENSE.txt file for details. +require 'rackstash/filter' + module Rackstash module Filter # The Truncate filter can be used to restrict the size of the emitted @@ -150,5 +152,7 @@ module Rackstash messages.inject(0) { |sum, msg| sum + msg.size } end end + + register TruncateMessage, :truncate_message end end diff --git a/lib/rackstash/filter/update.rb b/lib/rackstash/filter/update.rb index bde1c46..abbcf7c 100644 --- a/lib/rackstash/filter/update.rb +++ b/lib/rackstash/filter/update.rb @@ -5,6 +5,8 @@ # This software may be modified and distributed under the terms # of the MIT license. See the LICENSE.txt file for details. +require 'rackstash/filter' + module Rackstash module Filter # Update fields in the given event with new values. A new value can be @@ -53,5 +55,7 @@ module Rackstash event end end + + register Update, :update end end diff --git a/spec/rackstash/filter_chain_spec.rb b/spec/rackstash/filter_chain_spec.rb index 6043ade..58e088c 100644 --- a/spec/rackstash/filter_chain_spec.rb +++ b/spec/rackstash/filter_chain_spec.rb @@ -142,8 +142,9 @@ describe Rackstash::FilterChain do expect { filter_chain.append(false) }.to raise_error TypeError expect { filter_chain.append(42) }.to raise_error TypeError - expect { filter_chain.append(:foo) }.to raise_error NameError - expect { filter_chain.append('Foo') }.to raise_error NameError + # Registered filter was not found + expect { filter_chain.append(:foo) }.to raise_error KeyError + expect { filter_chain.append('Foo') }.to raise_error KeyError expect { filter_chain.append }.to raise_error ArgumentError end @@ -341,8 +342,9 @@ describe Rackstash::FilterChain do expect { filter_chain.insert_before(1, false) }.to raise_error TypeError expect { filter_chain.insert_before(1, 42) }.to raise_error TypeError - expect { filter_chain.insert_before(1, :foo) }.to raise_error NameError - expect { filter_chain.insert_before(1, 'Foo') }.to raise_error NameError + # Registered filter was not found + expect { filter_chain.insert_before(1, :foo) }.to raise_error KeyError + expect { filter_chain.insert_before(1, 'Foo') }.to raise_error KeyError expect { filter_chain.insert_before(1) }.to raise_error ArgumentError end @@ -398,8 +400,9 @@ describe Rackstash::FilterChain do expect { filter_chain.insert_after(1, false) }.to raise_error TypeError expect { filter_chain.insert_after(1, 42) }.to raise_error TypeError - expect { filter_chain.insert_after(1, :foo) }.to raise_error NameError - expect { filter_chain.insert_after(1, 'Foo') }.to raise_error NameError + # Registered filter was not found + expect { filter_chain.insert_after(1, :foo) }.to raise_error KeyError + expect { filter_chain.insert_after(1, 'Foo') }.to raise_error KeyError expect { filter_chain.insert_after(1) }.to raise_error ArgumentError end @@ -455,8 +458,9 @@ describe Rackstash::FilterChain do expect { filter_chain.unshift(false) }.to raise_error TypeError expect { filter_chain.unshift(42) }.to raise_error TypeError - expect { filter_chain.unshift(:foo) }.to raise_error NameError - expect { filter_chain.unshift('Foo') }.to raise_error NameError + # Registered filter was not found + expect { filter_chain.unshift(:foo) }.to raise_error KeyError + expect { filter_chain.unshift('Foo') }.to raise_error KeyError expect { filter_chain.unshift }.to raise_error ArgumentError end diff --git a/spec/rackstash/filter_spec.rb b/spec/rackstash/filter_spec.rb index 9a93f2f..de53c1b 100644 --- a/spec/rackstash/filter_spec.rb +++ b/spec/rackstash/filter_spec.rb @@ -11,17 +11,23 @@ require 'securerandom' require 'rackstash/filter' describe Rackstash::Filter do - let(:filter_class) { Class.new } - let(:random) { SecureRandom.hex(6) } - let(:filter_class_name) { :"FilterClass#{random}" } + let(:registry) { Rackstash::ClassRegistry.new('filter') } - around(:each) do |example| - described_class.const_set(filter_class_name, filter_class) - example.run - described_class.send(:remove_const, filter_class_name) - end + let(:filter_class) { + Class.new do + def call(event) + 'filtered' + end + end + } + let(:filter_name) { :"filter_class_#{SecureRandom.hex(6)}" } describe '.build' do + before do + allow(described_class).to receive(:registry).and_return(registry) + described_class.register(filter_class, filter_name) + end + it 'builds a filter from a class' do args = ['arg1', foo: 'bar'] expect(filter_class).to receive(:new).with(*args) @@ -33,45 +39,44 @@ describe Rackstash::Filter do args = ['arg1', foo: 'bar'] expect(filter_class).to receive(:new).with(*args) - described_class.build(:"filter_class#{random}", *args) + described_class.build(filter_name.to_sym, *args) end it 'builds a filter from a String' do args = ['arg1', foo: 'bar'] expect(filter_class).to receive(:new).with(*args) - described_class.build("filter_class#{random}", *args) + described_class.build(filter_name.to_s, *args) end it 'returns an existing filter' do filter = -> {} + expect(described_class.build(filter)).to equal filter expect(described_class.build(filter, :ignored, 42)).to equal filter end - it 'raises a TypeError with different arguments' do - expect { described_class.build(123) }.to raise_error(TypeError) - expect { described_class.build(nil) }.to raise_error(TypeError) - expect { described_class.build(true) }.to raise_error(TypeError) + it 'raises a TypeError with invalid spec types' do + expect { described_class.build(123) } + .to raise_error(TypeError, '123 can not be used to describe filters') + expect { described_class.build(nil) } + .to raise_error(TypeError, 'nil can not be used to describe filters') + expect { described_class.build(true) } + .to raise_error(TypeError, 'true can not be used to describe filters') + end - expect { described_class.build('MissingFilter') }.to raise_error(NameError) - expect { described_class.build(:missing_filter) }.to raise_error(NameError) + it 'raises a KeyError for undefined filters' do + expect { described_class.build('MissingFilter') } + .to raise_error(KeyError, 'No filter was registered for "MissingFilter"') + expect { described_class.build(:missing_filter) } + .to raise_error(KeyError, 'No filter was registered for :missing_filter') end end - describe '.known' do - it 'returns a Hash with known filters' do - expect(described_class.known).not_to be_empty - - expect(described_class.known.keys).to all( - be_a(Symbol) - .and match(/\A[a-z0-9_]+\z/) - ) - expect(described_class.known.values).to all be_a(Class) - end - - it 'includes Filter classes' do - expect(described_class.known[:"filter_class#{random}"]).to equal filter_class + describe 'registry' do + it 'returns the filter registry' do + expect(described_class.registry).to be_instance_of Rackstash::ClassRegistry + expect(described_class.registry.object_type).to eql 'filter' end end end