logstash-filter-ruby 3.0.4 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/docs/index.asciidoc +118 -8
- data/lib/logstash/filters/ruby.rb +85 -11
- data/lib/logstash/filters/ruby/script.rb +40 -0
- data/lib/logstash/filters/ruby/script/context.rb +75 -0
- data/lib/logstash/filters/ruby/script/execution_context.rb +24 -0
- data/lib/logstash/filters/ruby/script/expect_context.rb +40 -0
- data/lib/logstash/filters/ruby/script/test_context.rb +65 -0
- data/lib/logstash/filters/ruby/script_error.rb +2 -0
- data/logstash-filter-ruby.gemspec +1 -1
- data/spec/filters/ruby_spec.rb +134 -95
- data/spec/fixtures/broken.rb +13 -0
- data/spec/fixtures/field_multiplier.rb +34 -0
- data/spec/spec_helper.rb +2 -0
- metadata +14 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 273ed2da72e09943995b2bcbbeb25e5ff5066173
|
4
|
+
data.tar.gz: adccac89c884c0123b10a382a138bcec9ee58529
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 465d9a82af6a18daf470d767b4e3a07ad57783d4cfee66a2979631333922381360058cedecbd4b08423a5e0c84f78bf0a6e89dfb4cbec4bc43a9940f1340aea4
|
7
|
+
data.tar.gz: b28977f8356f8ab9639c96025da48d8bef8260b279c5dacd8cb1ed953d7ea76ea7613513c743e8a56999aa9b9109760a3a208369af20e47c9056a63d0d0a9bc1
|
data/CHANGELOG.md
CHANGED
data/docs/index.asciidoc
CHANGED
@@ -20,7 +20,13 @@ include::{include_path}/plugin_header.asciidoc[]
|
|
20
20
|
|
21
21
|
==== Description
|
22
22
|
|
23
|
-
Execute ruby code.
|
23
|
+
Execute ruby code. This filter accepts inline ruby code or a ruby file.
|
24
|
+
The two options are mutually exclusive and have slightly different ways of working,
|
25
|
+
which are described below.
|
26
|
+
|
27
|
+
===== Inline ruby code
|
28
|
+
|
29
|
+
To inline ruby in your filter, place all code in the `code` option. This code will be executed for every event the filter receives. You can also place ruby code in the `init` option - it will be executed only once during the plugin's register phase.
|
24
30
|
|
25
31
|
For example, to cancel 90% of events, you can do this:
|
26
32
|
[source,ruby]
|
@@ -31,8 +37,7 @@ For example, to cancel 90% of events, you can do this:
|
|
31
37
|
}
|
32
38
|
}
|
33
39
|
|
34
|
-
If you need to create additional events,
|
35
|
-
you must use a specific syntax `new_event_block.call(event)` like in this example duplicating the input event
|
40
|
+
If you need to create additional events, you must use a specific syntax `new_event_block.call(event)` like in this example duplicating the input event
|
36
41
|
[source,ruby]
|
37
42
|
filter {
|
38
43
|
ruby {
|
@@ -40,6 +45,92 @@ filter {
|
|
40
45
|
}
|
41
46
|
}
|
42
47
|
|
48
|
+
===== Using a Ruby script file
|
49
|
+
|
50
|
+
As the inline code can become complex and hard to structure inside of a text string in `code`, it's then preferrable to place the Ruby code in a .rb file, using the `path` option.
|
51
|
+
|
52
|
+
[source,ruby]
|
53
|
+
filter {
|
54
|
+
ruby {
|
55
|
+
# Cancel 90% of events
|
56
|
+
path => "/etc/logstash/drop_percentage.rb"
|
57
|
+
script_params => { "percentage" => 0.9 }
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
The ruby script file should define the following methods:
|
62
|
+
|
63
|
+
* `register(params)`: An optional register method that receives the key/value hash passed in the `script_params` configuration option
|
64
|
+
* `filter(event)`: A mandatory Ruby method that accepts a Logstash event and must return an array of events
|
65
|
+
|
66
|
+
Below is an example implementation of the `drop_percentage.rb` ruby script that drops a configurable percentage of events:
|
67
|
+
|
68
|
+
[source,ruby]
|
69
|
+
----
|
70
|
+
# the value of `params` is the value of the hash passed to `script_params`
|
71
|
+
# in the logstash configuration
|
72
|
+
def register(params)
|
73
|
+
@drop_percentage = params["percentage"]
|
74
|
+
end
|
75
|
+
|
76
|
+
# the filter method receives an event and must return a list of events.
|
77
|
+
# Dropping an event means not including it in the return array,
|
78
|
+
# while creating new ones only requires you to add a new instance of
|
79
|
+
# LogStash::Event to the returned array
|
80
|
+
def filter(event)
|
81
|
+
if rand >= @drop_percentage
|
82
|
+
return [event]
|
83
|
+
else
|
84
|
+
return [] # return empty array to cancel event
|
85
|
+
end
|
86
|
+
end
|
87
|
+
----
|
88
|
+
|
89
|
+
====== Testing the ruby script
|
90
|
+
|
91
|
+
To validate the behaviour of the `filter` method you implemented,
|
92
|
+
the Ruby filter plugin provides an inline test framework where you
|
93
|
+
can assert expectations.
|
94
|
+
The tests you define will run when the pipeline is created and will
|
95
|
+
prevent it from starting if a test fails.
|
96
|
+
|
97
|
+
You can also verify if the tests pass using the logstash `-t` flag.
|
98
|
+
|
99
|
+
For example above, you can write at the bottom of the `drop_percentage.rb`
|
100
|
+
ruby script the following test:
|
101
|
+
|
102
|
+
[source,ruby]
|
103
|
+
----
|
104
|
+
def register(params)
|
105
|
+
# ..
|
106
|
+
end
|
107
|
+
|
108
|
+
def filter(event)
|
109
|
+
# ..
|
110
|
+
end
|
111
|
+
|
112
|
+
test "drop percentage 100%" do
|
113
|
+
parameters do
|
114
|
+
{ "percentage" => 1 }
|
115
|
+
end
|
116
|
+
|
117
|
+
in_event { { "message" => "hello" } }
|
118
|
+
|
119
|
+
expect("drops the event") do |events|
|
120
|
+
events.size == 0
|
121
|
+
end
|
122
|
+
end
|
123
|
+
----
|
124
|
+
|
125
|
+
We can now test that the ruby script we're using is implemented correctly:
|
126
|
+
|
127
|
+
[source]
|
128
|
+
----
|
129
|
+
% bin/logstash -e "filter { ruby { path => '/etc/logstash/drop_percentage.rb' script_params => { 'drop_percentage' => 0.5 } } }" -t
|
130
|
+
[2017-10-13T13:44:29,723][INFO ][logstash.filters.ruby.script] Test run complete {:script_path=>"/etc/logstash/drop_percentage.rb", :results=>{:passed=>1, :failed=>0, :errored=>0}}
|
131
|
+
Configuration OK
|
132
|
+
[2017-10-13T13:44:29,887][INFO ][logstash.runner ] Using config.test_and_exit mode. Config Validation Result: OK. Exiting Logstash
|
133
|
+
----
|
43
134
|
|
44
135
|
[id="plugins-{type}s-{plugin}-options"]
|
45
136
|
==== Ruby Filter Configuration Options
|
@@ -49,8 +140,11 @@ This plugin supports the following configuration options plus the <<plugins-{typ
|
|
49
140
|
[cols="<,<,<",options="header",]
|
50
141
|
|=======================================================================
|
51
142
|
|Setting |Input type|Required
|
52
|
-
| <<plugins-{type}s-{plugin}-code>> |<<string,string>>|
|
143
|
+
| <<plugins-{type}s-{plugin}-code>> |<<string,string>>|No
|
53
144
|
| <<plugins-{type}s-{plugin}-init>> |<<string,string>>|No
|
145
|
+
| <<plugins-{type}s-{plugin}-path>> |<<string,string>>|No
|
146
|
+
| <<plugins-{type}s-{plugin}-script_params>> |<<hash,hash>>,{}|No
|
147
|
+
| <<plugins-{type}s-{plugin}-tag_on_exception>> |<<string,string>>,_rubyexception|No
|
54
148
|
|=======================================================================
|
55
149
|
|
56
150
|
Also see <<plugins-{type}s-{plugin}-common-options>> for a list of options supported by all
|
@@ -59,24 +153,40 @@ filter plugins.
|
|
59
153
|
|
60
154
|
|
61
155
|
[id="plugins-{type}s-{plugin}-code"]
|
62
|
-
===== `code`
|
156
|
+
===== `code`
|
63
157
|
|
64
|
-
* This is a required setting.
|
65
158
|
* Value type is <<string,string>>
|
66
159
|
* There is no default value for this setting.
|
160
|
+
* This setting cannot be used together with `path`.
|
67
161
|
|
68
162
|
The code to execute for every event.
|
69
163
|
You will have an `event` variable available that is the event itself. See the <<event-api,Event API>> for more information.
|
70
164
|
|
71
165
|
[id="plugins-{type}s-{plugin}-init"]
|
72
|
-
===== `init`
|
166
|
+
===== `init`
|
73
167
|
|
74
168
|
* Value type is <<string,string>>
|
75
169
|
* There is no default value for this setting.
|
76
170
|
|
77
171
|
Any code to execute at logstash startup-time
|
78
172
|
|
173
|
+
[id="plugins-{type}s-{plugin}-path"]
|
174
|
+
===== `path`
|
175
|
+
|
176
|
+
* Value type is <<string,string>>
|
177
|
+
* There is no default value for this setting.
|
178
|
+
* This setting cannot be used together with `code`.
|
179
|
+
|
180
|
+
The path of the ruby script file that implements the `filter` method.
|
181
|
+
|
182
|
+
[id="plugins-{type}s-{plugin}-script_params"]
|
183
|
+
===== `script_params`
|
184
|
+
|
185
|
+
* Value type is <<hash,hash>>
|
186
|
+
* Default value is `{}`
|
79
187
|
|
188
|
+
A key/value hash with parameters that are passed to the register method
|
189
|
+
of your ruby script file defined in `path`.
|
80
190
|
|
81
191
|
[id="plugins-{type}s-{plugin}-common-options"]
|
82
|
-
include::{include_path}/{type}.asciidoc[]
|
192
|
+
include::{include_path}/{type}.asciidoc[]
|
@@ -23,6 +23,9 @@ require "logstash/namespace"
|
|
23
23
|
# }
|
24
24
|
#
|
25
25
|
class LogStash::Filters::Ruby < LogStash::Filters::Base
|
26
|
+
require "logstash/filters/ruby/script_error"
|
27
|
+
require "logstash/filters/ruby/script"
|
28
|
+
|
26
29
|
config_name "ruby"
|
27
30
|
|
28
31
|
# Any code to execute at logstash startup-time
|
@@ -30,21 +33,92 @@ class LogStash::Filters::Ruby < LogStash::Filters::Base
|
|
30
33
|
|
31
34
|
# The code to execute for every event.
|
32
35
|
# You will have an `event` variable available that is the event itself. See the <<event-api,Event API>> for more information.
|
33
|
-
config :code, :validate => :string
|
36
|
+
config :code, :validate => :string
|
37
|
+
|
38
|
+
# Path to the script
|
39
|
+
config :path, :validate => :path
|
40
|
+
|
41
|
+
# Parameters for this specific script
|
42
|
+
config :script_params, :type => :hash, :default => {}
|
43
|
+
|
44
|
+
# Tag to add to events that cause an exception in the script filter
|
45
|
+
config :tag_on_exception, :type => :string, :default => "_rubyexception"
|
46
|
+
|
47
|
+
def initialize(*params)
|
48
|
+
super(*params)
|
49
|
+
if @path # run tests on the ruby file script
|
50
|
+
@script = Script.new(@path, @script_params)
|
51
|
+
@script.load
|
52
|
+
@script.test
|
53
|
+
end
|
54
|
+
end
|
34
55
|
|
35
56
|
def register
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
57
|
+
if @code && @path.nil?
|
58
|
+
eval(@init, binding, "(ruby filter init)") if @init
|
59
|
+
eval("@codeblock = lambda { |event, &new_event_block| #{@code} }", binding, "(ruby filter code)")
|
60
|
+
elsif @path && @code.nil?
|
61
|
+
@script.register
|
62
|
+
else
|
63
|
+
@logger.fatal("You must either use an inline script with the \"code\" option or a script file using \"path\".")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.check_result_events!(results)
|
68
|
+
if !results.is_a?(Array)
|
69
|
+
raise "Custom script did not return an array from 'filter'. Only arrays may be returned!"
|
70
|
+
end
|
71
|
+
|
72
|
+
results.each do |r_event|
|
73
|
+
if !r_event.is_a?(::LogStash::Event)
|
74
|
+
raise "Custom script returned a non event object '#{r_event.inspect}'!" +
|
75
|
+
" You must an array of events from this function! To drop an event simply return nil."
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def filter(event, &block)
|
81
|
+
if @code
|
82
|
+
inline_script(event, &block)
|
83
|
+
elsif @path
|
84
|
+
file_script(event)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def inline_script(event, &block)
|
89
|
+
@codeblock.call(event, &block)
|
90
|
+
filter_matched(event)
|
91
|
+
rescue Exception => e
|
92
|
+
@logger.error("Ruby exception occurred: #{e}")
|
93
|
+
event.tag(@tag_on_exception)
|
94
|
+
end
|
40
95
|
|
41
|
-
def
|
96
|
+
def file_script(event)
|
42
97
|
begin
|
43
|
-
@
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
event.tag(
|
98
|
+
results = @script.execute(event)
|
99
|
+
|
100
|
+
self.class.check_result_events!(results)
|
101
|
+
rescue => e
|
102
|
+
event.tag(@tag_on_exception)
|
103
|
+
message = "Could not process event: " + e.message
|
104
|
+
@logger.error(message, :script_path => @path,
|
105
|
+
:class => e.class.name,
|
106
|
+
:backtrace => e.backtrace)
|
107
|
+
return event
|
108
|
+
end
|
109
|
+
|
110
|
+
returned_original = false
|
111
|
+
results.each do |r_event|
|
112
|
+
# If the user has generated a new event we yield that for them here
|
113
|
+
if event == r_event
|
114
|
+
returned_original = true
|
115
|
+
else
|
116
|
+
yield r_event
|
117
|
+
end
|
118
|
+
|
119
|
+
r_event
|
48
120
|
end
|
121
|
+
|
122
|
+
event.cancel unless returned_original
|
49
123
|
end
|
50
124
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class LogStash::Filters::Ruby::Script
|
2
|
+
include ::LogStash::Util::Loggable
|
3
|
+
require "logstash/filters/ruby/script/context"
|
4
|
+
|
5
|
+
attr_reader :content, :script_path
|
6
|
+
|
7
|
+
def initialize(script_path, parameters)
|
8
|
+
@content = File.read(script_path)
|
9
|
+
@script_path = script_path
|
10
|
+
@context = Context.new(self, parameters)
|
11
|
+
end
|
12
|
+
|
13
|
+
def load
|
14
|
+
@context.load_script
|
15
|
+
|
16
|
+
if !@context.execution_context.methods.include?(:filter)
|
17
|
+
raise "Script does not define a filter! Please ensure that you have defined a filter method!"
|
18
|
+
end
|
19
|
+
rescue => e
|
20
|
+
raise ::LogStash::Filters::Ruby::ScriptError.new("Error during load of '#{script_path}': #{e.inspect}")
|
21
|
+
end
|
22
|
+
|
23
|
+
def register
|
24
|
+
@context.execute_register
|
25
|
+
rescue => e
|
26
|
+
raise ::LogStash::Filters::Ruby::ScriptError.new("Error during register of '#{script_path}': #{e.inspect}")
|
27
|
+
end
|
28
|
+
|
29
|
+
def execute(event)
|
30
|
+
@context.execute_filter(event)
|
31
|
+
end
|
32
|
+
|
33
|
+
def test
|
34
|
+
results = @context.execute_tests
|
35
|
+
logger.info("Test run complete", :script_path => script_path, :results => results)
|
36
|
+
if results[:failed] > 0 || results[:errored] > 0
|
37
|
+
raise ::LogStash::Filters::Ruby::ScriptError.new("Script '#{script_path}' had #{results[:failed] + results[:errored]} failing tests! Check the error log for details.")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
class LogStash::Filters::Ruby::Script::Context
|
2
|
+
require "logstash/filters/ruby/script/execution_context"
|
3
|
+
require "logstash/filters/ruby/script/test_context"
|
4
|
+
|
5
|
+
include ::LogStash::Util::Loggable
|
6
|
+
|
7
|
+
attr_reader :script,
|
8
|
+
:execution_context
|
9
|
+
|
10
|
+
def initialize(script, parameters)
|
11
|
+
@script = script
|
12
|
+
@script_path = @script.script_path
|
13
|
+
@parameters = parameters
|
14
|
+
@test_contexts = []
|
15
|
+
@script_lock = Mutex.new
|
16
|
+
@concurrency = :single
|
17
|
+
end
|
18
|
+
|
19
|
+
def make_execution_context(name, test_mode)
|
20
|
+
execution_context = LogStash::Filters::Ruby::Script::ExecutionContext.new(name, logger)
|
21
|
+
|
22
|
+
# Proxy all the methods from this instance needed to be run from the execution context
|
23
|
+
this = self # We need to use a clojure to retain access to this object
|
24
|
+
# If we aren't in test mode we define the test. If we *are* then we don't define anything
|
25
|
+
# since our tests are already defined
|
26
|
+
if test_mode
|
27
|
+
execution_context.define_singleton_method(:test) {|name,&block| nil }
|
28
|
+
else
|
29
|
+
execution_context.define_singleton_method(:test) {|name,&block| this.test(name, &block) }
|
30
|
+
end
|
31
|
+
execution_context
|
32
|
+
end
|
33
|
+
|
34
|
+
def load_execution_context(ec)
|
35
|
+
ec.instance_eval(@script.content, @script_path, 1)
|
36
|
+
end
|
37
|
+
|
38
|
+
def load_script
|
39
|
+
@execution_context = self.make_execution_context(:main, false)
|
40
|
+
load_execution_context(@execution_context)
|
41
|
+
end
|
42
|
+
|
43
|
+
def execute_register()
|
44
|
+
@execution_context.register(@parameters)
|
45
|
+
end
|
46
|
+
|
47
|
+
def concurrency(type)
|
48
|
+
@concurrency = type
|
49
|
+
end
|
50
|
+
|
51
|
+
def execute_filter(event)
|
52
|
+
if @concurrency == :shared
|
53
|
+
@script_lock.synchronize { @execution_context.filter(event) }
|
54
|
+
else
|
55
|
+
@execution_context.filter(event)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def execute_tests
|
60
|
+
@test_contexts.
|
61
|
+
map(&:execute).
|
62
|
+
reduce({:passed => 0, :failed => 0, :errored => 0}) do |acc,res|
|
63
|
+
acc[:passed] += res[:passed]
|
64
|
+
acc[:failed] += res[:failed]
|
65
|
+
acc[:errored] += res[:errored]
|
66
|
+
acc
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def test(name, &block)
|
71
|
+
test_context = LogStash::Filters::Ruby::Script::TestContext.new(self, name)
|
72
|
+
test_context.instance_eval(&block)
|
73
|
+
@test_contexts << test_context
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# A blank area for our script to live in.
|
2
|
+
# Everything is instance_e{val,exec}'d against this
|
3
|
+
# to eliminate instance var and method def conflicts against other
|
4
|
+
# objects
|
5
|
+
class LogStash::Filters::Ruby::Script::ExecutionContext
|
6
|
+
def initialize(name, logger)
|
7
|
+
# Namespaced with underscore so as not to conflict with anything the user sets
|
8
|
+
@__name__ = name
|
9
|
+
@__logger__ = logger
|
10
|
+
end
|
11
|
+
|
12
|
+
def logger
|
13
|
+
@__logger__
|
14
|
+
end
|
15
|
+
|
16
|
+
def register(params)
|
17
|
+
logger.debug("skipping register since the script didn't define it")
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
"<ExecutionContext #{@__name__}>"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# Handle expect blocks inside of test blocks
|
2
|
+
class ExpectContext
|
3
|
+
include ::LogStash::Util::Loggable
|
4
|
+
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
def initialize(test_context, name, block)
|
8
|
+
@test_context = test_context
|
9
|
+
@name = name
|
10
|
+
@block = block
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
"<Expect #{@test_context.name}/#{self.name}>"
|
15
|
+
end
|
16
|
+
|
17
|
+
def execute(events)
|
18
|
+
begin
|
19
|
+
if @block.call(events)
|
20
|
+
return :passed
|
21
|
+
else
|
22
|
+
result = :failed
|
23
|
+
message = "***TEST FAILURE FOR: '#{@test_context.name} #{@name}'***"
|
24
|
+
log_hash = {}
|
25
|
+
end
|
26
|
+
rescue => e
|
27
|
+
result = :errored
|
28
|
+
message = "***TEST RAISED ERROR: '#{@test_context.name} #{@name}'***"
|
29
|
+
log_hash = { "exception" => e.inspect, "backtrace" => e.backtrace }
|
30
|
+
end
|
31
|
+
script_path = @test_context.script_context.script.script_path
|
32
|
+
log_hash.merge!({
|
33
|
+
:parameters => @test_context.parameters,
|
34
|
+
:in_events => @test_context.in_events.map(&:to_hash_with_metadata),
|
35
|
+
:results => events.map(&:to_hash_with_metadata)
|
36
|
+
})
|
37
|
+
logger.error(message, log_hash)
|
38
|
+
result
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# Handle top level test blocks
|
2
|
+
class LogStash::Filters::Ruby::Script::TestContext
|
3
|
+
require "logstash/filters/ruby/script/expect_context"
|
4
|
+
attr_reader :name, :script_context
|
5
|
+
|
6
|
+
def initialize(script_context, name)
|
7
|
+
@name = name
|
8
|
+
@script_context = script_context
|
9
|
+
@expect_contexts = []
|
10
|
+
@parameters = {}
|
11
|
+
@execution_context = script_context.make_execution_context("Test/#{name}", true)
|
12
|
+
@script_context.load_execution_context(@execution_context)
|
13
|
+
end
|
14
|
+
|
15
|
+
def parameters(&block)
|
16
|
+
# Can act as a reader if no block passed
|
17
|
+
return @parameters unless block
|
18
|
+
|
19
|
+
@parameters = block.call
|
20
|
+
if !@parameters.is_a?(Hash)
|
21
|
+
raise ArgumentError, "Test parameters must be a hash in #{@name}!"
|
22
|
+
end
|
23
|
+
|
24
|
+
@execution_context.register(@parameters)
|
25
|
+
end
|
26
|
+
|
27
|
+
def in_event(&block)
|
28
|
+
return @in_events unless block
|
29
|
+
|
30
|
+
orig = block.call
|
31
|
+
event_hashes = orig.is_a?(Hash) ? [orig] : orig
|
32
|
+
event_hashes.each do |e|
|
33
|
+
if !e.is_a?(Hash)
|
34
|
+
raise ArgumentError,
|
35
|
+
"In event for #{self.name} must receive either a hash or an array of hashes! got a '#{e.class}' in #{event_hashes.inspect}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
@in_events = Array(event_hashes).map {|h| ::LogStash::Event.new(h) }
|
39
|
+
end
|
40
|
+
alias_method :in_events, :in_event
|
41
|
+
|
42
|
+
def execute
|
43
|
+
if !@in_events
|
44
|
+
raise "You must declare an `in_event` to run tests!"
|
45
|
+
end
|
46
|
+
|
47
|
+
results = []
|
48
|
+
@in_events.each do |e|
|
49
|
+
single_result = @execution_context.filter(e)
|
50
|
+
::LogStash::Filters::Ruby.check_result_events!(single_result)
|
51
|
+
results += single_result
|
52
|
+
end
|
53
|
+
|
54
|
+
@expect_contexts.map do |ec|
|
55
|
+
ec.execute(results)
|
56
|
+
end.reduce({:passed => 0, :failed => 0, :errored => 0}) do |acc,res|
|
57
|
+
acc[res] += 1
|
58
|
+
acc
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def expect(name, &block)
|
63
|
+
@expect_contexts << ExpectContext.new(self, name, block)
|
64
|
+
end
|
65
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
|
3
3
|
s.name = 'logstash-filter-ruby'
|
4
|
-
s.version = '3.0
|
4
|
+
s.version = '3.1.0'
|
5
5
|
s.licenses = ['Apache License (2.0)']
|
6
6
|
s.summary = "Filter for executing ruby code on the event"
|
7
7
|
s.description = "This gem is a Logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This gem is not a stand-alone program"
|
data/spec/filters/ruby_spec.rb
CHANGED
@@ -1,121 +1,160 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
-
|
3
|
-
require "logstash/devutils/rspec/spec_helper"
|
2
|
+
require_relative '../spec_helper'
|
4
3
|
require "logstash/filters/ruby"
|
5
4
|
require "logstash/filters/date"
|
6
5
|
|
7
6
|
describe LogStash::Filters::Ruby do
|
7
|
+
context "when using inline script" do
|
8
|
+
describe "generate pretty json on event.to_hash" do
|
9
|
+
# this obviously tests the Ruby filter but also makes sure
|
10
|
+
# the fix for issue #1771 is correct and that to_json is
|
11
|
+
# compatible with the json gem convention.
|
8
12
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
}
|
21
|
-
ruby {
|
22
|
-
init => "require 'json'"
|
23
|
-
code => "event.set('pretty', JSON.pretty_generate(event.to_hash))"
|
13
|
+
config <<-CONFIG
|
14
|
+
filter {
|
15
|
+
date {
|
16
|
+
match => [ "mydate", "ISO8601" ]
|
17
|
+
locale => "en"
|
18
|
+
timezone => "UTC"
|
19
|
+
}
|
20
|
+
ruby {
|
21
|
+
init => "require 'json'"
|
22
|
+
code => "event.set('pretty', JSON.pretty_generate(event.to_hash))"
|
23
|
+
}
|
24
24
|
}
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
25
|
+
CONFIG
|
26
|
+
|
27
|
+
sample("message" => "hello world", "mydate" => "2014-09-23T00:00:00-0800") do
|
28
|
+
# json is rendered in pretty json since the JSON.pretty_generate created json from the event hash
|
29
|
+
# pretty json contains \n
|
30
|
+
insist { subject.get("pretty").count("\n") } == 5
|
31
|
+
# usage of JSON.parse here is to avoid parser-specific order assertions
|
32
|
+
insist { JSON.parse(subject.get("pretty")) } == JSON.parse("{\n \"message\": \"hello world\",\n \"mydate\": \"2014-09-23T00:00:00-0800\",\n \"@version\": \"1\",\n \"@timestamp\": \"2014-09-23T08:00:00.000Z\"\n}")
|
33
|
+
end
|
34
34
|
end
|
35
|
-
end
|
36
35
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
36
|
+
describe "generate pretty json on event.to_hash" do
|
37
|
+
# this obviously tests the Ruby filter but asses that using the json gem directly
|
38
|
+
# on even will correctly call the to_json method but will use the logstash json
|
39
|
+
# generation and thus will not work with pretty_generate.
|
40
|
+
config <<-CONFIG
|
41
|
+
filter {
|
42
|
+
date {
|
43
|
+
match => [ "mydate", "ISO8601" ]
|
44
|
+
locale => "en"
|
45
|
+
timezone => "UTC"
|
46
|
+
}
|
47
|
+
ruby {
|
48
|
+
init => "require 'json'"
|
49
|
+
code => "event.set('pretty', JSON.pretty_generate(event))"
|
50
|
+
}
|
51
51
|
}
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
52
|
+
CONFIG
|
53
|
+
|
54
|
+
sample("message" => "hello world", "mydate" => "2014-09-23T00:00:00-0800") do
|
55
|
+
# if this eventually breaks because we removed the custom to_json and/or added pretty support to JrJackson then all is good :)
|
56
|
+
# non-pretty json does not contain \n
|
57
|
+
insist { subject.get("pretty").count("\n") } == 0
|
58
|
+
# usage of JSON.parse here is to avoid parser-specific order assertions
|
59
|
+
insist { JSON.parse(subject.get("pretty")) } == JSON.parse("{\"message\":\"hello world\",\"mydate\":\"2014-09-23T00:00:00-0800\",\"@version\":\"1\",\"@timestamp\":\"2014-09-23T08:00:00.000Z\"}")
|
60
|
+
end
|
61
61
|
end
|
62
|
-
end
|
63
62
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
63
|
+
describe "catch all exceptions and don't let them ruin your day buy stopping the entire worker" do
|
64
|
+
# If exception is raised, it stops entine processing pipeline, and never resumes
|
65
|
+
# Code section should always be wrapped with begin/rescue
|
66
|
+
config <<-CONFIG
|
67
|
+
filter {
|
68
|
+
date {
|
69
|
+
match => [ "mydate", "ISO8601" ]
|
70
|
+
locale => "en"
|
71
|
+
timezone => "UTC"
|
72
|
+
}
|
73
|
+
ruby {
|
74
|
+
init => "require 'json'"
|
75
|
+
code => "raise 'You shall not pass'"
|
76
|
+
add_tag => ["ok"]
|
77
|
+
}
|
73
78
|
}
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
79
|
+
CONFIG
|
80
|
+
|
81
|
+
sample("message" => "hello world", "mydate" => "2014-09-23T00:00:00-0800") do
|
82
|
+
insist { subject.get("mydate") } == "2014-09-23T00:00:00-0800"
|
83
|
+
insist { subject.get("tags") } == ["_rubyexception"]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "allow to create new event inside the ruby filter" do
|
88
|
+
config <<-CONFIG
|
89
|
+
filter {
|
90
|
+
ruby {
|
91
|
+
code => "new_event_block.call(event.clone)"
|
92
|
+
}
|
78
93
|
}
|
79
|
-
|
80
|
-
CONFIG
|
94
|
+
CONFIG
|
81
95
|
|
82
|
-
|
83
|
-
|
84
|
-
|
96
|
+
sample("message" => "hello world", "mydate" => "2014-09-23T00:00:00-0800") do
|
97
|
+
expect(subject).to be_a Array
|
98
|
+
expect(subject[0]).not_to eq(subject[1])
|
99
|
+
expect(subject[0].to_hash).to eq(subject[1].to_hash)
|
100
|
+
end
|
85
101
|
end
|
86
|
-
end
|
87
102
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
103
|
+
describe "allow to replace event by another one" do
|
104
|
+
config <<-CONFIG
|
105
|
+
filter {
|
106
|
+
ruby {
|
107
|
+
code => "new_event_block.call(event.clone);
|
108
|
+
event.cancel;"
|
109
|
+
add_tag => ["ok"]
|
110
|
+
}
|
93
111
|
}
|
94
|
-
|
95
|
-
CONFIG
|
112
|
+
CONFIG
|
96
113
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
114
|
+
sample("message" => "hello world", "mydate" => "2014-09-23T00:00:00-0800") do
|
115
|
+
expect(subject.get("message")).to eq("hello world");
|
116
|
+
expect(subject.get("mydate")).to eq("2014-09-23T00:00:00-0800");
|
117
|
+
end
|
101
118
|
end
|
102
119
|
end
|
103
120
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
121
|
+
context "when using file based script" do
|
122
|
+
let(:fixtures_path) { File.join(File.dirname(__FILE__), '../fixtures/') }
|
123
|
+
let(:script_filename) { 'field_multiplier.rb' }
|
124
|
+
let(:script_path) { File.join(fixtures_path, script_filename)}
|
125
|
+
let(:script_params) { { 'field' => 'foo', 'multiplier' => 2 } }
|
126
|
+
let(:filter_params) { { 'path' => script_path, 'script_params' => script_params} }
|
127
|
+
let(:incoming_event) { ::LogStash::Event.new('foo' => 42) }
|
128
|
+
|
129
|
+
subject(:filter) { ::LogStash::Filters::Ruby.new(filter_params) }
|
130
|
+
|
131
|
+
describe "basics" do
|
132
|
+
it "should register cleanly" do
|
133
|
+
expect do
|
134
|
+
filter.register
|
135
|
+
end.not_to raise_error
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "filtering" do
|
139
|
+
before(:each) do
|
140
|
+
filter.register
|
141
|
+
filter.filter(incoming_event)
|
142
|
+
end
|
114
143
|
|
115
|
-
|
116
|
-
|
117
|
-
|
144
|
+
it "should filter data as expected" do
|
145
|
+
expect(incoming_event.get('foo')).to eq(84)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe "scripts with failing test suites" do
|
151
|
+
let(:script_filename) { 'broken.rb' }
|
152
|
+
|
153
|
+
it "should error out during register" do
|
154
|
+
expect do
|
155
|
+
filter.register
|
156
|
+
end.to raise_error(LogStash::Filters::Ruby::ScriptError)
|
157
|
+
end
|
118
158
|
end
|
119
159
|
end
|
120
160
|
end
|
121
|
-
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Disables mutex around the `filter` function
|
2
|
+
# Only use this if you know your code is threadsafe!
|
3
|
+
def concurrency
|
4
|
+
:shared
|
5
|
+
end
|
6
|
+
|
7
|
+
def register(params)
|
8
|
+
@field = params['field']
|
9
|
+
@multiplier = params['multiplier']
|
10
|
+
end
|
11
|
+
|
12
|
+
def filter(event)
|
13
|
+
event.set(@field, event.get(@field) * @multiplier)
|
14
|
+
# Filter blocks must return any events that are to be passed on
|
15
|
+
# return a nil or [] here if all events are to be cancelled
|
16
|
+
# You can even return one or more brand new events here!
|
17
|
+
[event]
|
18
|
+
end
|
19
|
+
|
20
|
+
test "standard flow" do
|
21
|
+
parameters do
|
22
|
+
{ "field" => "myfield", "multiplier" => 3 }
|
23
|
+
end
|
24
|
+
|
25
|
+
in_event { { "myfield" => 123 } }
|
26
|
+
|
27
|
+
expect("there to be only one result event") do |events|
|
28
|
+
events.size == 1
|
29
|
+
end
|
30
|
+
|
31
|
+
expect("result to be equal to 123*3(369)") do |events|
|
32
|
+
events.first.get("myfield") == 369
|
33
|
+
end
|
34
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logstash-filter-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0
|
4
|
+
version: 3.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Elastic
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-11-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
@@ -72,8 +72,17 @@ files:
|
|
72
72
|
- README.md
|
73
73
|
- docs/index.asciidoc
|
74
74
|
- lib/logstash/filters/ruby.rb
|
75
|
+
- lib/logstash/filters/ruby/script.rb
|
76
|
+
- lib/logstash/filters/ruby/script/context.rb
|
77
|
+
- lib/logstash/filters/ruby/script/execution_context.rb
|
78
|
+
- lib/logstash/filters/ruby/script/expect_context.rb
|
79
|
+
- lib/logstash/filters/ruby/script/test_context.rb
|
80
|
+
- lib/logstash/filters/ruby/script_error.rb
|
75
81
|
- logstash-filter-ruby.gemspec
|
76
82
|
- spec/filters/ruby_spec.rb
|
83
|
+
- spec/fixtures/broken.rb
|
84
|
+
- spec/fixtures/field_multiplier.rb
|
85
|
+
- spec/spec_helper.rb
|
77
86
|
homepage: http://www.elastic.co/guide/en/logstash/current/index.html
|
78
87
|
licenses:
|
79
88
|
- Apache License (2.0)
|
@@ -102,3 +111,6 @@ specification_version: 4
|
|
102
111
|
summary: Filter for executing ruby code on the event
|
103
112
|
test_files:
|
104
113
|
- spec/filters/ruby_spec.rb
|
114
|
+
- spec/fixtures/broken.rb
|
115
|
+
- spec/fixtures/field_multiplier.rb
|
116
|
+
- spec/spec_helper.rb
|