diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..d325328 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -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://github.com/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://github.com/elasticsearch/logstash/issues) +* If it is specific to a plugin, please file it in the respective repository under [logstash-plugins](https://github.com/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://github.com/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://github.com/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. + + diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..c3cc91d --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -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: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..a153827 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -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/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 23083b8..58dd774 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,3 @@ # 1.0.0 - - Implements boolean `ecs_compatibility` config option, unless already provided by Logstash core. \ No newline at end of file + - 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. diff --git a/README.md b/README.md index 51c86ee..44334ae 100644 --- a/README.md +++ b/README.md @@ -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| @@ -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' @@ -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 ~~~ diff --git a/lib/logstash/plugin_mixins/ecs_compatibility_support.rb b/lib/logstash/plugin_mixins/ecs_compatibility_support.rb index 5f87ecb..13faf7d 100644 --- a/lib/logstash/plugin_mixins/ecs_compatibility_support.rb +++ b/lib/logstash/plugin_mixins/ecs_compatibility_support.rb @@ -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 diff --git a/spec/logstash/plugin_mixins/ecs_compatibility_support_spec.rb b/spec/logstash/plugin_mixins/ecs_compatibility_support_spec.rb index 30f62e8..2dceb95 100644 --- a/spec/logstash/plugin_mixins/ecs_compatibility_support_spec.rb +++ b/spec/logstash/plugin_mixins/ecs_compatibility_support_spec.rb @@ -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 @@ -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 @@ -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