mirror of
https://github.com/elastic/logstash.git
synced 2025-04-24 22:57:16 -04:00
397 lines
16 KiB
Text
397 lines
16 KiB
Text
[[contributing-patch-plugin]]
|
||
=== Contributing a Patch to a Logstash Plugin
|
||
|
||
This section discusses the information you need to know to successfully contribute a patch to a Logstash plugin.
|
||
|
||
Each plugin defines its own configuration options. These control the behaviour of the plugin to some degree. Configuration
|
||
option definitions commonly include:
|
||
|
||
* Data validation
|
||
* The default value
|
||
* Any required flags
|
||
|
||
Plugins are subclasses of a Logstash base class. A plugin's base class defines common configuration and methods.
|
||
|
||
==== Input Plugins
|
||
|
||
Input plugins ingest data from an external source. Input plugins are always associated with a codec. An input plugin
|
||
always has an associated codec plugin. Input and codec plugins operate in conjunction to create a Logstash event and add
|
||
that event to the processing queue. An input codec is a subclass of the `LogStash::Inputs::Base` class.
|
||
|
||
.Input API
|
||
[horizontal]
|
||
`#register() -> nil`:: Required. This API sets up resources for the plugin, typically the connection to the
|
||
external source.
|
||
`#run(queue) -> nil`:: Required. This API fetches or listens for source data, typically looping until stopped. Must handle
|
||
errors inside the loop. Pushes any created events to the queue object specified in the method argument. Some inputs may
|
||
receive batched data to minimize the external call overhead.
|
||
`#stop() -> nil`:: Optional. Stops external connections and cleans up.
|
||
|
||
==== Codec Plugins
|
||
|
||
Codec plugins decode input data that has a specific structure, such as JSON input data. A codec plugin is a subclass of
|
||
`LogStash::Codecs::Base`.
|
||
|
||
.Codec API
|
||
[horizontal]
|
||
`#register() -> nil`:: Identical to the API of the same name for input plugins.
|
||
`#decode(data){|event| block} -> nil`:: Must be implemented. Used to create an Event from the raw data given in the method
|
||
argument. Must handle errors. The caller must provide a Ruby block. The block is called with the created Event.
|
||
`#encode(event) -> nil`:: Required. Used to create a structured data object from the given Event. May handle
|
||
errors. This method calls a block that was previously stored as @on_event with two arguments: the original event and the
|
||
data object.
|
||
|
||
==== Filter Plugins
|
||
|
||
A mechanism to change, mutate or merge one or more Events. A filter plugin is a subclass of the `LogStash::Filters::Base`
|
||
class.
|
||
|
||
.Filter API
|
||
[horizontal]
|
||
`#register() -> nil`:: Identical to the API of the same name for input plugins.
|
||
`#filter(event) -> nil`:: Required. May handle errors. Used to apply a mutation function to the given event.
|
||
|
||
==== Output Plugins
|
||
|
||
A mechanism to send an event to an external destination. This process may require serialization. An output plugin is a
|
||
subclass of the `LogStash::Outputs::Base` class.
|
||
|
||
.Output API
|
||
[horizontal]
|
||
`#register() -> nil`:: Identical to the API of the same name for input plugins.
|
||
`#receive(event) -> nil`:: Required. Must handle errors. Used to prepare the given event for transmission to
|
||
the external destination. Some outputs may buffer the prepared events to batch transmit to the destination.
|
||
|
||
[[patch-process]]
|
||
==== Process
|
||
|
||
A bug or feature is identified. An issue is created in the plugin repository. A patch is created and a pull request (PR)
|
||
is submitted. After review and possible rework the PR is merged and the plugin is published.
|
||
|
||
The <<community-maintainer,Community Maintainer Guide>> explains, in more detail, the process of getting a patch accepted,
|
||
merged and published. The Community Maintainer Guide also details the roles that contributors and maintainers are
|
||
expected to perform.
|
||
|
||
==== Testing Methodologies
|
||
|
||
===== Test Driven Development
|
||
|
||
Test Driven Development, colloquially known as TDD, describes a methodology for using tests to guide evolution of source
|
||
code. For our purposes, we are only going to use a part of it, that is, before writing the fix - we create tests that
|
||
illustrate the bug by failing. We stop when we have written enough code to make the tests pass and submit the fix and
|
||
tests as a patch. It is not necessary to write the tests before the fix, but it is very easy to write a passing test
|
||
afterwards that may not actually verify that the fault is really fixed especially if the fault can be triggered via
|
||
multiple execution paths or varying input data.
|
||
|
||
===== The RSpec Framework
|
||
|
||
Logstash uses Rspec, a Ruby testing framework, to define and run the test suite. What follows is a summary of various
|
||
sources.
|
||
|
||
. Rspec Example
|
||
[source,ruby]
|
||
1 # encoding: utf-8
|
||
2 require "logstash/devutils/rspec/spec_helper"
|
||
3 require "logstash/plugin"
|
||
4
|
||
5 describe "outputs/riemann" do
|
||
6 describe "#register" do
|
||
7 let(:output) do
|
||
8 LogStash::Plugin.lookup("output", "riemann").new(configuration)
|
||
9 end
|
||
10
|
||
11 context "when no protocol is specified" do
|
||
12 let(:configuration) { Hash.new }
|
||
13
|
||
14 it "the method completes without error" do
|
||
15 expect {output.register}.not_to raise_error
|
||
16 end
|
||
17 end
|
||
18
|
||
19 context "when a bad protocol is specified" do
|
||
20 let(:configuration) { {"protocol" => "fake"} }
|
||
21
|
||
22 it "the method fails with error" do
|
||
23 expect {output.register}.to raise_error
|
||
24 end
|
||
25 end
|
||
26
|
||
27 context "when the tcp protocol is specified" do
|
||
28 let(:configuration) { {"protocol" => "tcp"} }
|
||
29
|
||
30 it "the method completes without error" do
|
||
31 expect {output.register}.not_to raise_error
|
||
32 end
|
||
33 end
|
||
34 end
|
||
35
|
||
36 describe "#receive" do
|
||
37 let(:output) do
|
||
38 LogStash::Plugin.lookup("output", "riemann").new(configuration)
|
||
39 end
|
||
40
|
||
41 context "when operating normally" do
|
||
42 let(:configuration) { Hash.new }
|
||
43 let(:event) do
|
||
44 data = {"message"=>"hello", "@version"=>"1",
|
||
45 "@timestamp"=>"2015-06-03T23:34:54.076Z",
|
||
46 "host"=>"vagrant-ubuntu-trusty-64"}
|
||
47 LogStash::Event.new(data)
|
||
48 end
|
||
49
|
||
50 before(:example) do
|
||
51 output.register
|
||
52 end
|
||
53
|
||
54 it "should accept the event" do
|
||
55 expect { output.receive event }.not_to raise_error
|
||
56 end
|
||
57 end
|
||
58 end
|
||
59 end
|
||
|
||
.Describe blocks (lines 5, 6 and 36 in Example 1)
|
||
[source,ruby]
|
||
describe(string){block} -> nil
|
||
describe(Class){block} -> nil
|
||
|
||
With RSpec, we are always describing the plugin method behavior. The describe block is added in logical sections and can
|
||
accept either an existing class name or a string. The string used in line 5 is the plugin name. Line 6 is the register
|
||
method, line 36 is the receive method. It is a RSpec convention to prefix instance methods with one hash and class
|
||
methods with one dot.
|
||
|
||
.Context blocks (lines 11, 19, 27 and 41)
|
||
[source,ruby]
|
||
context(string){block} -> nil
|
||
|
||
In RSpec, context blocks define sections that group tests by a variation. The string should start with the word `when`
|
||
and then detail the variation. See line 11. The tests in the content block should should only be for that variation.
|
||
|
||
.Let blocks (lines 7, 12, 20, 28, 37, 42 and 43)
|
||
[source,ruby]
|
||
let(symbol){block} -> nil
|
||
|
||
In RSpec, `let` blocks define resources for use in the test blocks. These resources are reinitialized for every test
|
||
block. They are available as method calls inside the test block. Define `let` blocks in `describe` and `context` blocks,
|
||
which scope the `let` block and any other nested blocks.
|
||
You can use other `let` methods defined later within the `let` block body. See lines 7-9, which define the output resource
|
||
and use the configuration method, defined with different variations in lines 12, 20 and 28.
|
||
|
||
.Before blocks (line 50)
|
||
[source,ruby]
|
||
before(symbol){block} -> nil - symbol is one of :suite, :context, :example, but :all and :each are synonyms for :suite and :example respectively.
|
||
|
||
In RSpec, `before` blocks are used to further set up any resources that would have been initialized in a `let` block.
|
||
You cannot define `let` blocks inside `before` blocks.
|
||
|
||
You can also define `after` blocks, which are typically used to clean up any setup activity performed by a `before` block.
|
||
|
||
.It blocks (lines 14, 22, 30 and 54)
|
||
[source,ruby]
|
||
it(string){block} -> nil
|
||
|
||
In RSpec, `it` blocks set the expectations that verify the behavior of the tested code. The string should not start with
|
||
'it' or 'should', but needs to express the outcome of the expectation. When put together the texts from the enclosing
|
||
describe, `context` and `it` blocks should form a fairly readable sentence, as in lines 5, 6, 11 and 14:
|
||
|
||
[source,ruby]
|
||
outputs/riemann
|
||
#register when no protocol is specified the method completes without error
|
||
|
||
Readable code like this make the goals of tests easy to understand.
|
||
|
||
.Expect method (lines 15, 23, 31, 55)
|
||
[source,ruby]
|
||
expect(object){block} -> nil
|
||
|
||
In RSpec, the expect method verifies a statement that compares an actual result to an expected result. The `expect` method
|
||
is usually paired with a call to the `to` or `not_to` methods. Use the block form when expecting errors or observing for
|
||
changes. The `to` or `not_to` methods require a `matcher` object that encapsulates the expected value. The argument form
|
||
of the `expect` method encapsulates the actual value. When put together the whole line tests the actual against the
|
||
expected value.
|
||
|
||
.Matcher methods (lines 15, 23, 31, 55)
|
||
[source,ruby]
|
||
raise_error(error class|nil) -> matcher instance
|
||
be(object) -> matcher instance
|
||
eq(object) -> matcher instance
|
||
eql(object) -> matcher instance
|
||
for more see http://www.relishapp.com/rspec/rspec-expectations/docs/built-in-matchers
|
||
|
||
In RSpec, a matcher is an object generated by the equivalent method call (be, eq) that will be used to evaluate the
|
||
expected against the actual values.
|
||
|
||
==== Putting it all together
|
||
|
||
This example fixes an https://github.com/logstash-plugins/logstash-output-zeromq/issues/9[issue] in the ZeroMQ output
|
||
plugin. The issue does not require knowledge of ZeroMQ.
|
||
|
||
The activities in this example have the following prerequisites:
|
||
|
||
--
|
||
* A minimal knowledge of Git and Github. See the https://help.github.com/categories/bootcamp/[Github boot camp].
|
||
* A text editor.
|
||
* A JRuby https://www.ruby-lang.org/en/documentation/installation/#managers[runtime]
|
||
https://howistart.org/posts/ruby/1[environment]. The `chruby` tool manages Ruby versions.
|
||
* JRuby 1.7.22 or later.
|
||
* The `bundler` and `rake` gems installed.
|
||
* ZeroMQ http://zeromq.org/intro:get-the-software[installed].
|
||
--
|
||
|
||
. In Github, fork the ZeroMQ https://github.com/logstash-plugins/logstash-output-zeromq[output plugin repository].
|
||
|
||
. On your local machine, https://help.github.com/articles/fork-a-repo/[clone] the fork to a known folder such as
|
||
`logstash/`.
|
||
|
||
. Open the following files in a text editor:
|
||
* `logstash-output-zeromq/lib/logstash/outputs/zeromq.rb`
|
||
* `logstash-output-zeromq/lib/logstash/util/zeromq.rb`
|
||
* `logstash-output-zeromq/spec/outputs/zeromq_spec.rb`
|
||
|
||
. According to the issue, log output in server mode must indicate `bound`. Furthermore, the test file contains no tests.
|
||
+
|
||
NOTE: Line 21 of `util/zeromq.rb` reads `@logger.info("0mq: #{server? ? 'connected' : 'bound'}", :address => address)`
|
||
|
||
. In the text editor, set the file encoding and require `zeromq.rb` for the file `zeromq_spec.rb` by adding the following
|
||
lines:
|
||
+
|
||
[source,ruby]
|
||
# encoding: utf-8
|
||
require "logstash/outputs/zeromq"
|
||
require "logstash/devutils/rspec/spec_helper"
|
||
|
||
. The desired error message should read:
|
||
+
|
||
[source,ruby]
|
||
LogStash::Outputs::ZeroMQ when in server mode a 'bound' info line is logged
|
||
+
|
||
To properly generate this message, add a `describe` block with the fully qualified class name as the argument, a context
|
||
block, and an `it` block.
|
||
+
|
||
[source,ruby]
|
||
describe LogStash::Outputs::ZeroMQ do
|
||
context "when in server mode" do
|
||
it "a 'bound' info line is logged" do
|
||
end
|
||
end
|
||
end
|
||
|
||
. To add the missing test, use an instance of the ZeroMQ output and a substitute logger. This example uses an RSpec feature
|
||
called _test doubles_ as the substitute logger.
|
||
+
|
||
Add the following lines to `zeromq_spec.rb`, after `describe LogStash::Outputs::ZeroMQ do` and before `context "when in
|
||
server mode" do`:
|
||
[source,ruby]
|
||
let(:output) { described_class.new("mode" => "server", "topology" => "pushpull" }
|
||
let(:tracer) { double("logger") }
|
||
|
||
. Add the body to the `it` block. Add the following five lines after the line `context "when in server mode" do`:
|
||
[source,ruby]
|
||
allow(tracer).to receive(:debug)<1>
|
||
output.logger = logger<2>
|
||
expect(tracer).to receive(:info).with("0mq: bound", {:address=>"tcp://127.0.0.1:2120"})<3>
|
||
output.register<4>
|
||
output.do_close<5>
|
||
|
||
<1> Allow the double to receive `debug` method calls.
|
||
<2> Make the output use the test double.
|
||
<3> Set an expectation on the test to receive an `info` method call.
|
||
<4> Call `register` on the output.
|
||
<5> Call `do_close` on the output so the test does not hang.
|
||
|
||
At the end of the modifications, the relevant code section reads:
|
||
|
||
[source,ruby]
|
||
--------
|
||
# encoding: utf-8
|
||
require "logstash/outputs/zeromq"
|
||
require "logstash/devutils/rspec/spec_helper"
|
||
|
||
describe LogStash::Outputs::ZeroMQ do
|
||
let(:output) { described_class.new("mode" => "server", "topology" => "pushpull") }
|
||
let(:tracer) { double("logger") }
|
||
|
||
context "when in server mode" do
|
||
it "a ‘bound’ info line is logged" do
|
||
allow(tracer).to receive(:debug)
|
||
output.logger = tracer
|
||
expect(tracer).to receive(:info).with("0mq: bound", {:address=>"tcp://127.0.0.1:2120"})
|
||
output.register
|
||
output.do_close
|
||
end
|
||
end
|
||
end
|
||
--------
|
||
|
||
To run this test:
|
||
|
||
. Open a terminal window
|
||
. Navigate to the cloned plugin folder
|
||
. The first time you run the test, run the command `bundle install`
|
||
. Run the command `bundle exec rspec`
|
||
|
||
Assuming all prerequisites were installed correctly, the test fails with output similar to:
|
||
|
||
[source,shell]
|
||
--------
|
||
Using Accessor#strict_set for specs
|
||
Run options: exclude {:redis=>true, :socket=>true, :performance=>true, :couchdb=>true, :elasticsearch=>true,
|
||
:elasticsearch_secure=>true, :export_cypher=>true, :integration=>true, :windows=>true}
|
||
|
||
LogStash::Outputs::ZeroMQ
|
||
when in server mode
|
||
a ‘bound’ info line is logged (FAILED - 1)
|
||
|
||
Failures:
|
||
|
||
1) LogStash::Outputs::ZeroMQ when in server mode a ‘bound’ info line is logged
|
||
Failure/Error: output.register
|
||
Double "logger" received :info with unexpected arguments
|
||
expected: ("0mq: bound", {:address=>"tcp://127.0.0.1:2120"})
|
||
got: ("0mq: connected", {:address=>"tcp://127.0.0.1:2120"})
|
||
# ./lib/logstash/util/zeromq.rb:21:in `setup'
|
||
# ./lib/logstash/outputs/zeromq.rb:92:in `register'
|
||
# ./lib/logstash/outputs/zeromq.rb:91:in `register'
|
||
# ./spec/outputs/zeromq_spec.rb:13:in `(root)'
|
||
# /Users/guy/.gem/jruby/1.9.3/gems/rspec-wait-0.0.7/lib/rspec/wait.rb:46:in `(root)'
|
||
|
||
Finished in 0.133 seconds (files took 1.28 seconds to load)
|
||
1 example, 1 failure
|
||
|
||
Failed examples:
|
||
|
||
rspec ./spec/outputs/zeromq_spec.rb:10 # LogStash::Outputs::ZeroMQ when in server mode a ‘bound’ info line is logged
|
||
|
||
Randomized with seed 2568
|
||
--------
|
||
|
||
To correct the error, open the `util/zeromq.rb` file in your text editor and swap the positions of the words `connected`
|
||
and `bound` on line 21. Line 21 now reads:
|
||
|
||
[source,ruby]
|
||
@logger.info("0mq: #{server? ? 'bound' : 'connected'}", :address => address)
|
||
|
||
Run the test again with the `bundle exec rspec` command.
|
||
|
||
The test passes with output similar to:
|
||
|
||
[source,shell]
|
||
--------
|
||
Using Accessor#strict_set for specs
|
||
Run options: exclude {:redis=>true, :socket=>true, :performance=>true, :couchdb=>true, :elasticsearch=>true, :elasticsearch_secure=>true, :export_cypher=>true, :integration=>true, :windows=>true}
|
||
|
||
LogStash::Outputs::ZeroMQ
|
||
when in server mode
|
||
a ‘bound’ info line is logged
|
||
|
||
Finished in 0.114 seconds (files took 1.22 seconds to load)
|
||
1 example, 0 failures
|
||
|
||
Randomized with seed 45887
|
||
--------
|
||
|
||
https://help.github.com/articles/fork-a-repo/#next-steps[Commit] the changes to git and Github.
|
||
|
||
Your pull request is visible from the https://github.com/logstash-plugins/logstash-output-zeromq/pulls[Pull Requests]
|
||
section of the original Github repository. The plugin maintainers review your work, suggest changes if necessary, and
|
||
merge and publish a new version of the plugin.
|