Skip to content

Commit

Permalink
Merge pull request #3 from yaauie/api-support-ecs-major-version-number
Browse files Browse the repository at this point in the history
API: support ECS major version number
  • Loading branch information
yaauie authored Jul 7, 2020
2 parents 9499d9e + ad8a05c commit f43148c
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 41 deletions.
65 changes: 65 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Contributing to Logstash

All contributions are welcome: ideas, patches, documentation, bug reports,
complaints, etc!

Programming is not a required skill, and there are many ways to help out!
It is more important to us that you are able to contribute.

That said, some basic guidelines, which you are free to ignore :)

## Want to learn?

Want to lurk about and see what others are doing with Logstash?

* The irc channel (#logstash on irc.freenode.org) is a good place for this
* The [forum](https://discuss.elastic.co/c/logstash) is also
great for learning from others.

## Got Questions?

Have a problem you want Logstash to solve for you?

* You can ask a question in the [forum](https://discuss.elastic.co/c/logstash)
* Alternately, you are welcome to join the IRC channel #logstash on
irc.freenode.org and ask for help there!

## Have an Idea or Feature Request?

* File a ticket on [GitHub](https:/elastic/logstash/issues). Please remember that GitHub is used only for issues and feature requests. If you have a general question, the [forum](https://discuss.elastic.co/c/logstash) or IRC would be the best place to ask.

## Something Not Working? Found a Bug?

If you think you found a bug, it probably is a bug.

* If it is a general Logstash or a pipeline issue, file it in [Logstash GitHub](https:/elasticsearch/logstash/issues)
* If it is specific to a plugin, please file it in the respective repository under [logstash-plugins](https:/logstash-plugins)
* or ask the [forum](https://discuss.elastic.co/c/logstash).

# Contributing Documentation and Code Changes

If you have a bugfix or new feature that you would like to contribute to
logstash, and you think it will take more than a few minutes to produce the fix
(ie; write code), it is worth discussing the change with the Logstash users and developers first! You can reach us via [GitHub](https:/elastic/logstash/issues), the [forum](https://discuss.elastic.co/c/logstash), or via IRC (#logstash on freenode irc)
Please note that Pull Requests without tests will not be merged. If you would like to contribute but do not have experience with writing tests, please ping us on IRC/forum or create a PR and ask our help.

## Contributing to plugins

Check our [documentation](https://www.elastic.co/guide/en/logstash/current/contributing-to-logstash.html) on how to contribute to plugins or write your own! It is super easy!

## Contribution Steps

1. Test your changes! [Run](https:/elastic/logstash#testing) the test suite
2. Please make sure you have signed our [Contributor License
Agreement](https://www.elastic.co/contributor-agreement/). We are not
asking you to assign copyright to us, but to give us the right to distribute
your code without restriction. We ask this of all contributors in order to
assure our users of the origin and continuing existence of the code. You
only need to sign the CLA once.
3. Send a pull request! Push your changes to your fork of the repository and
[submit a pull
request](https://help.github.com/articles/using-pull-requests). In the pull
request, describe what your changes do and mention any bugs/issues related
to the pull request.


9 changes: 9 additions & 0 deletions .github/ISSUE_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Please post all product and debugging questions on our [forum](https://discuss.elastic.co/c/logstash). Your questions will reach our wider community members there, and if we confirm that there is a bug, then we can open a new issue here.

For all general issues, please provide the following details for fast resolution:

- Version:
- Operating System:
- Config File (if you have sensitive info, please remove it):
- Sample Data:
- Steps to Reproduce:
1 change: 1 addition & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Thanks for contributing to Logstash! If you haven't already signed our CLA, here's a handy link: https://www.elastic.co/contributor-agreement/
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

- Implements boolean `ecs_compatibility` config option, unless already provided by Logstash core.
- 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.
31 changes: 22 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# ECS Compatibility Support Mixin

[![Build Status](https://travis-ci.org/logstash-plugins/logstash-mixin-ecs_compatibility_support.svg?branch=master)](https://travis-ci.org/logstash-plugins/logstash-mixin-ecs_compatibility_support)

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

1. Add this gem as a runtime dependency of your plugin:
1. Add version `~>1.0` of this gem as a runtime dependency of your Logstash plugin's `gemspec`:

~~~ ruby
Gem::Specification.new do |s|
Expand All @@ -18,8 +20,8 @@ ECS-compatibility mode, while still supporting older Logstash versions.
end
~~~

2. In your plugin code, require this library and include it into your class or
module that already inherits `LogStash::Util::Loggable`:
2. In your plugin code, require this library and include it into your plugin class
that already inherits `LogStash::Plugin`:

~~~ ruby
require 'logstash/plugin_mixins/ecs_compatibility_support'
Expand All @@ -31,15 +33,26 @@ ECS-compatibility mode, while still supporting older Logstash versions.
end
~~~

3. Use the `@ecs_compatibility` value; your plugin does not need to know whether
this config option was provided by Logstash core or by this gem.
3. Use the `ecs_compatibility` method, which will reflect the user's desired
ECS-Compatibility mode (either `:disabled` or a symbol holding a v-prefixed
integer major version of ECS, e.g., `:v1`) after the plugin has been sent
`#config_init`; your plugin does not need to know whether the user specified
the value 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 :v1
# ...
else
fail(NotImplementedError, "ECS #{ecs_compatibility} is not supported by this plugin.")
end
end
~~~
Expand Down
83 changes: 72 additions & 11 deletions lib/logstash/plugin_mixins/ecs_compatibility_support.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,96 @@ 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 a v-prefixed integer representing a major
# version of ECS (e.g., `v1`).
#
# 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),
# this adapter will _NOT_ override the existing implementation.
module ECSCompatibilitySupport
##
# @param: a class that inherits `LogStash::Plugin` and includes
# `LogStash::Config::Mixin`, typically one descending from one of
# the four plugin base classes (e.g., `LogStash::Inputs::Base`)
# @api internal (use: `LogStash::Plugin::include`)
# @param: a class that inherits `LogStash::Plugin`, typically one
# descending from one of the four plugin base classes (e.g.,
# `LogStash::Inputs::Base`)
# @return [void]
def self.included(base)
fail(ArgumentError, "`#{base}` must inherit LogStash::Plugin") unless base < LogStash::Plugin
fail(ArgumentError, "`#{base}` must include LogStash::Config::Mixin") unless base < LogStash::Config::Mixin

# 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.get_config.include?("ecs_compatibility")
base.send(:include, LegacyAdapter) unless base.method_defined?(:ecs_compatibility)
end

##
# Declares a boolean `ecs_compatibility` config option on the `base` that
# defaults to `false`.
#
# This `ECSCompatibilitySupport` cannot be extended into an existing object.
# @api private
#
# @param base [Object]
# @raise [ArgumentError]
def self.extended(base)
fail(ArgumentError, "`#{self}` cannot be extended into an existing object.")
end

##
# Implements `ecs_compatibility` method backed by an `ecs_compatibility`
# config option accepting the literal `disabled` or a v-prefixed integer
# representing a major version of ECS (e.g., `v1`).
#
# @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 a `Symbol`
# representing 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 [:disabled, :v1, Symbol]
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

##
# Intercepts calls to `validate_value(value, validator)` whose `validator` is
# the symbol :ecs_compatibility_argument.
#
# Ensures that the provided value is either:
# - the literal `disabled`; OR
# - a v-prefixed integer (e.g., `v1` )
#
# @api internal
module ArgumentValidator
V_PREFIXED_INTEGER_PATTERN = %r(\Av[1-9][0-9]?\Z).freeze
private_constant :V_PREFIXED_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_sym if value.first.to_s =~ V_PREFIXED_INTEGER_PATTERN
end

return false, "Expected a v-prefixed integer major-version number (e.g., `v1`) or the literal `disabled`, got #{value.inspect}"
end
end
end
end
Expand Down
65 changes: 45 additions & 20 deletions spec/logstash/plugin_mixins/ecs_compatibility_support_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,11 @@
plugin_class.send(:include, ecs_compatibility_support)
end

it 'supports an `ecs_compatibility` option' do
it 'supports an `ecs_compatibility` config option' do
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
end

Expand All @@ -63,12 +66,15 @@
end

# TODO: Remove once ECS Compatibility config is included in one or
# more Logstash release branches. This speculative spec is meant to
# run on Logstashes prior to the introduction of a core implementation.
# more Logstash release branches. This speculative spec is meant
# to prove that this implementation will not override an existing
# implementation.
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
config :ecs_compatibility
def ecs_compatibility
end
end
end
before(:each) do
Expand All @@ -80,34 +86,53 @@

# 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 }
it 'populates the @ecs_compatibility ivar with `true`' do
expect(instance.send(:instance_variable_get, :@ecs_compatibility)).to be true
end
context 'with `ecs_compatibility => v1`' do
let(:plugin_options) { super().merge('ecs_compatibility' => 'v1') }
its(:ecs_compatibility) { should equal :v1 }
end

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

context 'with `ecs_compatibility => false`' do
let(:plugin_options) { super().merge('ecs_compatibility' => 'false') }
its(:ecs_compatibility) { should be false }
it 'populates the @ecs_compatibility ivar with `false`' do
expect(instance.send(:instance_variable_get, :@ecs_compatibility)).to be false
context 'with an invalid value for `ecs_compatibility`' do
shared_examples 'invalid value' do |invalid_value|
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' => invalid_value) }

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(/\b#{Regexp.escape(invalid_value.to_s)}\b/)
end
end

context('a random string') do
include_examples 'invalid value', 'bananas'
end

context('nil') do
include_examples 'invalid value', nil
end

context('an integer') do
include_examples 'invalid value', 17
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 }
it 'populates the @ecs_compatibility ivar with `false`' do
expect(instance.send(:instance_variable_get, :@ecs_compatibility)).to be false
end
its(:ecs_compatibility) { should equal :disabled }
end
end
end
Expand Down

0 comments on commit f43148c

Please sign in to comment.