Skip to content

Commit

Permalink
api: support for ECS schema major version numbers
Browse files Browse the repository at this point in the history
  • Loading branch information
yaauie committed Jul 2, 2020
1 parent 57f84cf commit 463dbf1
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 31 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# 1.0.0

- Support Mixin for ensuring a plugin has an `ecs_compatibility?` method that is configurable from a boolean `ecs_compatibility` option, using the implementation from Logstash core if available.
- Support Mixin for ensuring a plugin has an `ecs_compatibility` method that is configurable from an `ecs_compatibility` option that accepts the literal `disabled` or an integer representing a major ECS version, using the implementation from Logstash core if available.
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
This gem provides an API-compatible implementation of ECS-compatiblity mode,
allowing plugins to be explicitly configured with `ecs_compatibility` in a way
that respects pipeline- and process-level settings where they are available.
It can be added as a dependency of any plugin that wishes to implement an
ECS-compatibility mode while still supporting older Logstash versions.
It can be added as a dependency of any plugin that wishes to implement one or
more ECS-compatibility modes while still supporting older Logstash versions.

## Usage

Expand All @@ -33,17 +33,26 @@ ECS-compatibility mode while still supporting older Logstash versions.
end
~~~

3. Use the `ecs_compatibility?` method, which will reflect the user's desired
ECS-Compatibility mode after the plugin has been sent `#config_init`; your
plugin does not need to know whether the user specified it in their plugin
config or its value was provided by Logstash.
3. Use the `ecs_compatibility` method, which will reflect the user's desired
ECS-Compatibility mode (either `disabled` or an integer major version of ECS)
after the plugin has been sent `#config_init`; your plugin does not need to
know whether the user specified it in their plugin config or its value was
provided by Logstash.
Care should be taken to handle _all_ possible values:
- all ECS major versions that are supported by the plugin
- ECS Compatibility being disabled
- helpful failure when an unsupported version is requested
~~~ ruby
def register
if ecs_compatibility?
case ecs_compatibility
when :disabled
# ...
else
when 1
# ...
else
fail(NotImplementedError, "ECS v#{ecs_compatibility} is not supported by this plugin.")
end
end
~~~
Expand Down
49 changes: 41 additions & 8 deletions lib/logstash/plugin_mixins/ecs_compatibility_support.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ module LogStash
module PluginMixins
##
# This `ECSCompatibilitySupport` can be included in any `LogStash::Plugin`,
# and will ensure that the plugin provides a boolean `ecs_compatibility`
# option.
# and will ensure that the plugin provides an `ecs_compatibility` option that
# accepts the literal `disabled` or an integer representing a major version
# of ECS.
#
# When included into a Logstash plugin that already has the option (e.g.,
# when run on a Logstash release that includes this option on all plugins),
Expand All @@ -25,7 +26,7 @@ def self.included(base)

# If our base does not include an `ecs_compatibility` config option,
# include the legacy adapter to ensure it gets defined.
base.send(:include, LegacyAdapter) unless base.method_defined?(:ecs_compatibility?)
base.send(:include, LegacyAdapter) unless base.method_defined?(:ecs_compatibility)
end

##
Expand All @@ -39,23 +40,55 @@ def self.extended(base)
end

##
# Implements `ecs_compatibility?` method backed by a boolean `ecs_compatibility`
# config option that defaults to `false`.
# Implements `ecs_compatibility` method backed by an `ecs_compatibility`
# config option accepting the literal `disabled` or an integer representing
# a major version of ECS.
#
# @api internal
module LegacyAdapter
def self.included(base)
base.config(:ecs_compatibility, :validate => :boolean, :default => false)
base.extend(ArgumentValidator)
base.config(:ecs_compatibility, :validate => :ecs_compatibility_argument, :default => :disabled)
end

##
# Designed for use by plugins in a `case` statement, this method returns the current
# ECS compatibility mode as configured at plugin initialization, or raises an exception
# if the mode has not yet been initialized.
#
# Plugin implementations using this method MUST provide code-paths for:
# - the major version(s) they explicitly support,
# - ECS Compatibility being disabled, AND
# - unknown versions (e.g., an else clause that raises an exception)
#
# @api public
# @return [Boolean]
def ecs_compatibility?
# @return [:disabled, Integer]
def ecs_compatibility
fail('uninitialized') if @ecs_compatibility.nil?

# NOTE: The @ecs_compatibility instance variable is an implementation detail of
# this `LegacyAdapter` and plugins MUST NOT rely in its presence or value.
@ecs_compatibility
end

module ArgumentValidator
INTEGER_PATTERN = %r(\A[1-9][0-9]*\Z).freeze
private_constant :INTEGER_PATTERN

def validate_value(value, validator)
return super unless validator == :ecs_compatibility_argument

value = deep_replace(value)
value = hash_or_array(value)

if value.size == 1
return true, :disabled if value.first.to_s == 'disabled'
return true, value.first.to_i if value.first.to_s =~ INTEGER_PATTERN
end

return false, "Expected an integer major-version number or the literal `disabled`, got #{value.inspect}"
end
end
end
end
end
Expand Down
41 changes: 27 additions & 14 deletions spec/logstash/plugin_mixins/ecs_compatibility_support_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
LogStash::Outputs::Base
].each do |base_class|
context "that inherits from `#{base_class}`" do
native_support_for_ecs_compatibility = base_class.method_defined?(:ecs_compatibility?)
native_support_for_ecs_compatibility = base_class.method_defined?(:ecs_compatibility)

let(:plugin_base_class) { base_class }

Expand All @@ -50,8 +50,8 @@
expect(plugin_class.get_config).to include('ecs_compatibility')
end

it 'defines an `ecs_compatibility?` method' do
expect(plugin_class.method_defined?(:ecs_compatibility?)).to be true
it 'defines an `ecs_compatibility` method' do
expect(plugin_class.method_defined?(:ecs_compatibility)).to be true
end

# depending on which version of Logstash is running, we either expect
Expand All @@ -72,39 +72,52 @@
context 'if base class were to include ecs_compatibility config' do
let(:plugin_base_class) do
Class.new(super()) do
config :ecs_compatibility, :validate => :boolean, :default => false
def ecs_compatibility?
config :ecs_compatibility
def ecs_compatibility
end
end
end
before(:each) do
expect(plugin_base_class.method_defined?(:ecs_compatibility?)).to be true
expect(plugin_base_class.method_defined?(:ecs_compatibility)).to be true
end
its(:ancestors) { is_expected.to_not include(ecs_compatibility_support::LegacyAdapter) }
end
end

# The four plugin base classes override their own `#initialize` to also
# send `#config_init`, so we can count on the options being normalized
# and populated out to the relevant ivars.
# and available.
context 'when initialized' do
let(:plugin_options) { Hash.new }
subject(:instance) { plugin_class.new(plugin_options) }

context 'with `ecs_compatibility => true`' do
let(:plugin_options) { super().merge('ecs_compatibility' => 'true') }
its(:ecs_compatibility?) { should be true }
context 'with `ecs_compatibility => 1`' do
let(:plugin_options) { super().merge('ecs_compatibility' => '1') }
its(:ecs_compatibility) { should equal 1 }
end

context 'with `ecs_compatibility => false`' do
let(:plugin_options) { super().merge('ecs_compatibility' => 'false') }
its(:ecs_compatibility?) { should be false }
context 'with `ecs_compatibility => disabled`' do
let(:plugin_options) { super().merge('ecs_compatibility' => 'disabled') }
its(:ecs_compatibility) { should equal :disabled }
end

context 'with an invalid value for `ecs_compatibility`' do
before { allow(plugin_class).to receive(:logger).and_return(logger_stub) }
let(:logger_stub) { double('Logger').as_null_object }

let(:plugin_options) { super().merge('ecs_compatibility' => 'bananas') }
it 'fails to initialize and emits a helpful log message' do
# we cannot rely on internal details of the error that is emitted such as its exact message,
# but we can expect the given value to be included in a message logged at ERROR-level.
expect { plugin_class.new(plugin_options) }.to raise_error(LogStash::ConfigurationError)
expect(logger_stub).to have_received(:error).with(/bananas/)
end
end

# we only specify default behaviour in cases where native support is _NOT_ provided.
unless native_support_for_ecs_compatibility
context 'without an `ecs_compatibility` directive' do
its(:ecs_compatibility?) { should be false }
its(:ecs_compatibility) { should equal :disabled }
end
end
end
Expand Down

0 comments on commit 463dbf1

Please sign in to comment.