logstash-filter-ruby 3.0.4 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|