1
0
mirror of https://github.com/meineerde/rackstash.git synced 2025-10-17 14:01:01 +00:00

Add abstract adapter and adapter registry

An ada pert wraps a log device (e.g. a file, an underlying logger, ...)
and provides an uniform interface to write the encoded log event to its
final target.

By using a registry, we can create the required adapter instance for a
provided log device automatically.
This commit is contained in:
Holger Just 2017-06-05 22:57:55 +02:00
parent f4e85f7013
commit 05f0faeedc
4 changed files with 547 additions and 0 deletions

145
lib/rackstash/adapters.rb Normal file
View File

@ -0,0 +1,145 @@
# Copyright 2017 Holger Just
#
# This software may be modified and distributed under the terms
# of the MIT license. See the LICENSE.txt file for details.
require 'uri'
module Rackstash
module Adapters
class << self
# Register a concrete adapter class which can be instanciated with a
# certain log device (e.g. a file name, an IO object, a URL specifying a
# log target, ...). With the provided `matchers`, a class can describe
# which kind of log devices are suitable to be used with it:
#
# * `String` with only lower-case characters - When passing a string with
# only lower-case characters, we register it as the scheme of a URL.
# When retrieving an adapter for a URL in {[]}, we check if the URL's
# scheme matches this string.
# * `String` with other characters - When passing a string that doesn't
# look like a URL scheme, we assume it to represent a class name. When
# retrieving a matching adapter for a device, we check if the name of
# the device's class matches this string. This can be used to register
# an adapter for a device which might not be loaded (yet), e.g. from an
# external gem. If possible, you should register the adapter for the
# actual class instead of its name.
# * `Symbol` - When passing a symbol, we check if the resolved device
# responds to an equally named method.
# * An object responding to the `===` method - When retrieving an adapter
# for a device, we are comparing the matcher object to the device. This
# is the same comparison as done in a `case ... when` statement in Ruby.
# Usually, the matcher object is either a class or module (in which case
# we check if the device object inherits from the matcher) or a proc,
# accepting an object as its first parameter. When checking this
# matcher, the proc gets called with the device as its parameter. If the
# proc returns a truethy value, we use it to build the adapter instance.
#
# @param adapter_class [Class] a concrete adapter class
# @param matchers [Array<String, Symbol, #===>] a list of specifications
# for log devices the `adapter_class` can forward logs to.
# @raise [TypeError] if the passed adapter_class is not a class
# inheriting from {Adapters::Adapter}
# @return [Class] the `adapter_class`
def register(adapter_class, *matchers)
unless adapter_class.is_a?(Class) && adapter_class < Adapters::Adapter
raise TypeError, 'adapter_class must be a class and inherit from ' +
'Rackstash::Adapters::Adapter'
end
matchers.flatten.each do |matcher|
case matcher
when String
matcher = matcher.to_s
if matcher =~ /\A[a-z0-9]+\z/
# If the matcher is a lower-case string, we assume it is a URL
# scheme.
adapter_schemes[matcher.downcase] = adapter_class
else
# If it starts with a upper-case characters, we assume it is a
# class name.
# Since we use `compare_by_identity` for types, we need to ensure
# that we can override existing class names.
adapter_types.delete_if { |key, _value| key == matcher }
adapter_types[matcher] = adapter_class
end
when Symbol, ->(o) { o.respond_to?(:===) }
adapter_types[matcher] = adapter_class
else
# Should not be reached by "normal" objects since `Object` already
# responds to `===` (which is the same as `==` by default)
raise TypeError, "unknown matcher: #{matcher.inspect}"
end
end
adapter_class
end
# Try to build an adapter instance from the passed `device`. If the
# `device` is already an {Adapter}, it is returned unchanged. If not, we
# attempt to identify a suitable adapter class from the {register}ed
# classes and return a new adapter instance.
#
# if no suitable adapter can be found, we raise an `ArgumentError`.
#
# @param device [Adapters::Adapter, Object] a log device which should be
# wrapped in an {Adapter}. If it is already an adapter, the `device` is
# returned unchanged.
# @raise [ArgumentError] if no suitable adapter could be found for the
# provided `device`
# @return [Adapters::Adapter] the resolved adapter instance
def [](device)
return device if device.is_a?(Adapters::Adapter)
adapter = adapter_by_uri(device)
adapter ||= adapter_by_type(device)
unless adapter
raise ArgumentError, "No log adapter found for #{device.inspect}"
end
adapter
end
private
def adapter_by_uri(uri)
uri = URI(uri) rescue return
scheme = uri.scheme || uri.opaque
return unless scheme
adapter_class = adapter_schemes.fetch(scheme.to_s.downcase) { return }
if adapter_class.respond_to?(:from_uri)
adapter_class.from_uri(uri)
else
adapter_class.new(uri)
end
end
def adapter_by_type(device)
adapter_types.each do |type, adapter_class|
suitable =
if type.is_a?(Symbol)
device.respond_to?(type)
elsif type.is_a?(String)
device.class.ancestors.any? { |klass| type == klass.name }
else
type === device
end
return adapter_class.new(device) if suitable
end
nil
end
def adapter_schemes
@adapter_schemes ||= {}
end
def adapter_types
@adapter_types ||= {}.compare_by_identity
end
end
end
end

View File

@ -0,0 +1,99 @@
# Copyright 2017 Holger Just
#
# This software may be modified and distributed under the terms
# of the MIT license. See the LICENSE.txt file for details.
require 'rackstash/adapters'
require 'rackstash/encoders/json'
module Rackstash
module Adapters
# The Adapter wraps a raw external log device like a file, an IO object like
# `STDOUT`, the system's syslog or even the connection to a TCP server with
# a common interface. At the end of a {Flow}, it is responsible to finally
# store the filtered and encoded log event.
#
# Each concrete adapter can register itself so that it can be used to wrap
# any compatible log device with {Rackstash::Adapters.[]}.
#
# @abstract Subclasses need to override at least {#write_single} to
# implement a concrete log adapter.
class Adapter
# Register the current class as an adapter for the provided matchers.
#
# This is a convenience method intended to be used by sub-classes of this
# abstract parent class to register themselves as adapters.
#
# @param matchers [Array<String, Symbol, #===>] a list of specifications
# for log devices the current adapter can forward logs to.
# @return [self]
# @see Adapter.register
def self.register_for(*matchers)
Rackstash::Adapters.register(self, *matchers)
end
# Create a new adapter instance.
#
# Usually, this method is overwritten by child classes to accept a
# suitable log device which will be used to write log lines to. When
# registering the adapter class, {Rackstash::Adapters.[]} will call
# {initialize} with a single argument: the log device.
def initialize(*)
end
# Close the underlying log device if supported by it.
#
# This method needs to be overwritten in child classes. By default, this
# method does nothing.
#
# @return [void]
def close
end
# Close and re-open the underlying log device if supported by it.
#
# This method needs to be overwritten in child classes. By default, this
# method does nothing.
#
# @return [void]
def reopen
end
# Write a log line to the log device. This method is called by the flow
# with a formatted log event.
#
# @param log [Object] the encoded log event
# @return [nil]
def write(log)
write_single(log)
nil
end
# Write a single log line to the log device.
#
# This method needs to be overwritten in child classes to write the
# encoded log event to the adapter's device. By default, this method
# raises a `NotImplementedError`.
#
# @param log [Object] the encoded log event
# @return [void]
# @raise NotImplementedError
def write_single(log)
raise NotImplementedError, 'write needs to be implemented in a sub class'
end
private
# Helper method to ensure that a log line passed to {#write} is a string
# that ends in a newline character by mutating the object is required.
#
# @param line [#to_s] a log line
# @return [String] a string with a trailing newline character (`"\n"`)
def normalize_line(line)
line = line.to_s
line << "\n".freeze unless line.end_with?("\n".freeze)
line
end
end
end
end

View File

@ -0,0 +1,45 @@
# Copyright 2017 Holger Just
#
# This software may be modified and distributed under the terms
# of the MIT license. See the LICENSE.txt file for details.
require 'spec_helper'
require 'rackstash/adapters/adapter'
describe Rackstash::Adapters::Adapter do
let(:adapter) { Rackstash::Adapters::Adapter.new }
describe '#initialize' do
it 'accepts any arguments' do
Rackstash::Adapters::Adapter.new
Rackstash::Adapters::Adapter.new(:foo)
Rackstash::Adapters::Adapter.new(123, [:foo])
end
end
describe '#close' do
it 'does nothing' do
expect(adapter.close).to be nil
end
end
describe '#reopen' do
it 'does nothing' do
expect(adapter.reopen).to be nil
end
end
describe '#write' do
it 'calls write_single' do
expect(adapter).to receive(:write_single).with('a log line')
adapter.write('a log line')
end
end
describe '#write_single' do
it 'is not implemented in the abstract base class' do
expect { adapter.write('something') }.to raise_error(NotImplementedError)
end
end
end

View File

@ -0,0 +1,258 @@
# Copyright 2017 Holger Just
#
# This software may be modified and distributed under the terms
# of the MIT license. See the LICENSE.txt file for details.
require 'spec_helper'
require 'rackstash/adapters'
describe Rackstash::Adapters do
around(:each) do |example|
types = Rackstash::Adapters.send(:adapter_types)
schemes = Rackstash::Adapters.send(:adapter_schemes)
Rackstash::Adapters.instance_variable_set(:@adapter_types, nil)
Rackstash::Adapters.instance_variable_set(:@adapter_schemes, nil)
example.run
Rackstash::Adapters.instance_variable_set(:@adapter_types, types)
Rackstash::Adapters.instance_variable_set(:@adapter_schemes, schemes)
end
let(:adapter) {
Class.new(Rackstash::Adapters::Adapter) do
def self.from_uri(*args)
new(*args)
end
end
}
describe '#register' do
it 'can register a class' do
expect {
Rackstash::Adapters.register adapter, Class.new
Rackstash::Adapters.register adapter, String
Rackstash::Adapters.register adapter, Numeric
Rackstash::Adapters.register adapter, Integer
}.to change { Rackstash::Adapters.send(:adapter_types).size }
.from(0).to(4)
expect(Rackstash::Adapters.send(:adapter_schemes).size).to eql 0
end
it 'can register a class name (upper-case String)' do
expect {
Rackstash::Adapters.register adapter, '❤'
Rackstash::Adapters.register adapter, ''
Rackstash::Adapters.register adapter, 'Hello::World'
}.to change { Rackstash::Adapters.send(:adapter_types).size }
.from(0).to(3)
# Registering 'Hello::World' a second time overwrites the first one
expect {
Rackstash::Adapters.register(adapter, 'Hello::World')
}.not_to change { Rackstash::Adapters.send(:adapter_types).size }
expect(Rackstash::Adapters.send(:adapter_schemes).size).to eql 0
end
it 'can register a method name (symbol)' do
expect {
Rackstash::Adapters.register adapter, :foo
}.to change { Rackstash::Adapters.send(:adapter_types).size }
.from(0).to(1)
expect(Rackstash::Adapters.send(:adapter_schemes).size).to eql 0
end
it 'can register a proc' do
expect {
Rackstash::Adapters.register adapter, ->(o) { o.respond_to?(:write) }
Rackstash::Adapters.register adapter, -> {}
}.to change { Rackstash::Adapters.send(:adapter_types).size }
.from(0).to(2)
expect(Rackstash::Adapters.send(:adapter_schemes).size).to eql 0
end
it 'can register a scheme (lower-case String)' do
expect {
Rackstash::Adapters.register adapter, 'customscheme'
}.to change { Rackstash::Adapters.send(:adapter_schemes).size }
.from(0).to(1)
expect(Rackstash::Adapters.send(:adapter_types).size).to eql 0
end
it 'rejects invalid adapter classes' do
expect { Rackstash::Adapters.register nil, :foo }
.to raise_error(TypeError)
expect { Rackstash::Adapters.register Class.new, :foo }
.to raise_error(TypeError)
end
end
describe '#[]' do
context 'with a registered class' do
let(:device_class) { Class.new }
before do
Rackstash::Adapters.register adapter, device_class
end
it 'creates an adapter if the class was found' do
device = device_class.new
expect(device_class).to receive(:===).with(device).and_call_original
expect(adapter).to receive(:new).with(device).and_call_original
expect(Rackstash::Adapters[device]).to be_an Rackstash::Adapters::Adapter
end
it 'creates an adapter if any parent class was found' do
inherited_device = Class.new(device_class).new
expect(device_class).to receive(:===).with(inherited_device).and_call_original
expect(adapter).to receive(:new).with(inherited_device).and_call_original
expect(Rackstash::Adapters[inherited_device]).to be_an Rackstash::Adapters::Adapter
end
it 'raises if no class was found' do
expect(adapter).to_not receive(:new)
expect { Rackstash::Adapters['foo'] }.to raise_error(ArgumentError)
end
end
context 'with a registered class name' do
before do
class SpecDevice; end
class InheritedSpecDevice < SpecDevice; end
Rackstash::Adapters.register adapter, 'SpecDevice'
end
after do
Object.send :remove_const, :InheritedSpecDevice
Object.send :remove_const, :SpecDevice
end
it 'creates an adapter if the class was found' do
device = SpecDevice.new
expect(adapter).to receive(:new).with(device).and_call_original
expect(Rackstash::Adapters[device]).to be_an Rackstash::Adapters::Adapter
end
it 'creates an adapter if any parent class was found' do
inherited_device = InheritedSpecDevice.new
expect(adapter).to receive(:new).with(inherited_device).and_call_original
expect(Rackstash::Adapters[inherited_device]).to be_an Rackstash::Adapters::Adapter
end
it 'raises if no class was found' do
expect(adapter).to_not receive(:new)
expect { Rackstash::Adapters['foo'] }.to raise_error(ArgumentError)
end
end
context 'with a registered symbol' do
before do
Rackstash::Adapters.register adapter, :foo
end
it 'creates an adapter if it responds to the registered method' do
device = Struct.new(:foo).new('foo')
expect(adapter).to receive(:new).with(device).and_call_original
expect(Rackstash::Adapters[device]).to be_an Rackstash::Adapters::Adapter
end
it 'raises if it does not respond to the registered method' do
device = Struct.new(:bar).new('bar')
expect(adapter).to_not receive(:new)
expect { Rackstash::Adapters[device] }.to raise_error(ArgumentError)
end
end
context 'with a registered proc' do
let(:device) { Object.new }
it 'creates an adapter if the proc returns true' do
checker = proc { true }
Rackstash::Adapters.register adapter, checker
expect(checker).to receive(:===).with(device).and_call_original
expect(adapter).to receive(:new).with(device).and_call_original
expect(Rackstash::Adapters[device]).to be_an Rackstash::Adapters::Adapter
end
it 'does not create an adapter if the proc returns false' do
checker = proc { false }
Rackstash::Adapters.register adapter, checker
expect(checker).to receive(:===).with(device).and_call_original
expect(adapter).to_not receive(:new)
expect { Rackstash::Adapters[device] }.to raise_error(ArgumentError)
end
end
context 'with a registered scheme' do
before do
Rackstash::Adapters.register adapter, 'dummy'
end
it 'creates an adapter from the scheme' do
raw_uri = 'dummy://example.com'
expect(adapter).to receive(:from_uri).with(URI(raw_uri)).and_call_original
expect(Rackstash::Adapters[raw_uri]).to be_an Rackstash::Adapters::Adapter
end
it 'creates an adapter from a URI' do
uri = URI('dummy://example.com')
expect(adapter).to receive(:from_uri).with(uri).and_call_original
expect(Rackstash::Adapters[uri]).to be_an Rackstash::Adapters::Adapter
end
it 'raises if no scheme was found' do
expect(adapter).to_not receive(:new)
expect(adapter).to_not receive(:from_uri)
expect { Rackstash::Adapters['unknown://example.com'] }
.to raise_error(ArgumentError)
expect { Rackstash::Adapters[URI('unknown://example.com')] }
.to raise_error(ArgumentError)
end
context 'and a registered class' do
before do
Rackstash::Adapters.register adapter, Object
end
it 'falls though on invalid URI' do
invalid_uri = '::'
expect(adapter).to_not receive(:from_uri)
# from the fallback
expect(adapter).to receive(:new).with(invalid_uri).and_call_original
expect(Rackstash::Adapters[invalid_uri]).to be_an Rackstash::Adapters::Adapter
end
it 'falls though if no scheme was found' do
unknown_uri = 'unknown://example.com'
expect(adapter).to_not receive(:from_uri)
expect(adapter).to receive(:new).with(unknown_uri).and_call_original
expect(Rackstash::Adapters[unknown_uri]).to be_an Rackstash::Adapters::Adapter
end
end
end
context 'with an existing adapter object' do
it 'just returns the object' do
adapter_instance = adapter.new
Rackstash::Adapters.register adapter, Object
expect(adapter).to_not receive(:new)
expect(Rackstash::Adapters[adapter_instance]).to equal adapter_instance
end
end
end
end