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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c4ddfea521d6f9a2fb33a994635e6fe2e3527a56
4
- data.tar.gz: dd8208bffa6ff7ef0f47be2f4c2855834a88b981
3
+ metadata.gz: 273ed2da72e09943995b2bcbbeb25e5ff5066173
4
+ data.tar.gz: adccac89c884c0123b10a382a138bcec9ee58529
5
5
  SHA512:
6
- metadata.gz: 033d7c85b4acf7d894abf39766729821d0bd044e983b2991ab28a6d031af1046f7b6c56d7c8c3525ba67f74bf37bf0195a39d1d389f7ea2e13d574ed9af45b86
7
- data.tar.gz: 6b1db324b71e23d3468d0a613355b9d60cf8e3ace06d310feaffeb1ae4383ae20e050b00c186126676dffa954c76c97b269733d70b21dcd7dcaa7027dc370c4b
6
+ metadata.gz: 465d9a82af6a18daf470d767b4e3a07ad57783d4cfee66a2979631333922381360058cedecbd4b08423a5e0c84f78bf0a6e89dfb4cbec4bc43a9940f1340aea4
7
+ data.tar.gz: b28977f8356f8ab9639c96025da48d8bef8260b279c5dacd8cb1ed953d7ea76ea7613513c743e8a56999aa9b9109760a3a208369af20e47c9056a63d0d0a9bc1
@@ -1,3 +1,6 @@
1
+ ## 3.1.0
2
+ - Add file base ruby script support
3
+
1
4
  ## 3.0.4
2
5
  - Fix some documentation issues
3
6
 
@@ -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, it cannot be done as in other filters where you would use `yield`,
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>>|Yes
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
  &nbsp;
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, :required => true
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
- # TODO(sissel): Compile the ruby code
37
- eval(@init, binding, "(ruby filter init)") if @init
38
- eval("@codeblock = lambda { |event, &new_event_block| #{@code} }", binding, "(ruby filter code)")
39
- end # def register
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 filter(event,&block)
96
+ def file_script(event)
42
97
  begin
43
- @codeblock.call(event,&block)
44
- filter_matched(event)
45
- rescue Exception => e
46
- @logger.error("Ruby exception occurred: #{e}")
47
- event.tag("_rubyexception")
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
@@ -0,0 +1,2 @@
1
+ class LogStash::Filters::Ruby::ScriptError < StandardError
2
+ 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'
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"
@@ -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
- describe "generate pretty json on event.to_hash" do
10
- # this obviously tests the Ruby filter but also makes sure
11
- # the fix for issue #1771 is correct and that to_json is
12
- # compatible with the json gem convention.
13
-
14
- config <<-CONFIG
15
- filter {
16
- date {
17
- match => [ "mydate", "ISO8601" ]
18
- locale => "en"
19
- timezone => "UTC"
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
- CONFIG
27
-
28
- sample("message" => "hello world", "mydate" => "2014-09-23T00:00:00-0800") do
29
- # json is rendered in pretty json since the JSON.pretty_generate created json from the event hash
30
- # pretty json contains \n
31
- insist { subject.get("pretty").count("\n") } == 5
32
- # usage of JSON.parse here is to avoid parser-specific order assertions
33
- 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}")
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
- describe "generate pretty json on event.to_hash" do
38
- # this obviously tests the Ruby filter but asses that using the json gem directly
39
- # on even will correctly call the to_json method but will use the logstash json
40
- # generation and thus will not work with pretty_generate.
41
- config <<-CONFIG
42
- filter {
43
- date {
44
- match => [ "mydate", "ISO8601" ]
45
- locale => "en"
46
- timezone => "UTC"
47
- }
48
- ruby {
49
- init => "require 'json'"
50
- code => "event.set('pretty', JSON.pretty_generate(event))"
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
- CONFIG
54
-
55
- sample("message" => "hello world", "mydate" => "2014-09-23T00:00:00-0800") do
56
- # if this eventually breaks because we removed the custom to_json and/or added pretty support to JrJackson then all is good :)
57
- # non-pretty json does not contain \n
58
- insist { subject.get("pretty").count("\n") } == 0
59
- # usage of JSON.parse here is to avoid parser-specific order assertions
60
- 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\"}")
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
- describe "catch all exceptions and don't let them ruin your day buy stopping the entire worker" do
65
- # If exception is raised, it stops entine processing pipeline, and never resumes
66
- # Code section should always be wrapped with begin/rescue
67
- config <<-CONFIG
68
- filter {
69
- date {
70
- match => [ "mydate", "ISO8601" ]
71
- locale => "en"
72
- timezone => "UTC"
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
- ruby {
75
- init => "require 'json'"
76
- code => "raise 'You shall not pass'"
77
- add_tag => ["ok"]
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
- sample("message" => "hello world", "mydate" => "2014-09-23T00:00:00-0800") do
83
- insist { subject.get("mydate") } == "2014-09-23T00:00:00-0800"
84
- insist { subject.get("tags") } == ["_rubyexception"]
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
- describe "allow to create new event inside the ruby filter" do
89
- config <<-CONFIG
90
- filter {
91
- ruby {
92
- code => "new_event_block.call(event.clone)"
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
- sample("message" => "hello world", "mydate" => "2014-09-23T00:00:00-0800") do
98
- expect(subject).to be_a Array
99
- expect(subject[0]).not_to eq(subject[1])
100
- expect(subject[0].to_hash).to eq(subject[1].to_hash)
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
- describe "allow to replace event by another one" do
105
- config <<-CONFIG
106
- filter {
107
- ruby {
108
- code => "new_event_block.call(event.clone);
109
- event.cancel;"
110
- add_tag => ["ok"]
111
- }
112
- }
113
- CONFIG
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
- sample("message" => "hello world", "mydate" => "2014-09-23T00:00:00-0800") do
116
- expect(subject.get("message")).to eq("hello world");
117
- expect(subject.get("mydate")).to eq("2014-09-23T00:00:00-0800");
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,13 @@
1
+ def filter(event)
2
+ event.set('foo', 'bar')
3
+ [event]
4
+ end
5
+
6
+ test "setting the field" do
7
+ in_event { { "myfield" => 123 } }
8
+
9
+ # This should fail!
10
+ expect("foo to equal baz") do |events|
11
+ events.first.get('foo') == 'baz'
12
+ end
13
+ end
@@ -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
@@ -0,0 +1,2 @@
1
+ # encoding: utf-8
2
+ require "logstash/devutils/rspec/spec_helper"
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
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-08-15 00:00:00.000000000 Z
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