logstash-filter-elapsed 0.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 ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NmE0ZGMwNzkzZTEwZTI5NDFiMzNmM2ZkYjk1YjcxMTZkMTY2NWMyZg==
5
+ data.tar.gz: !binary |-
6
+ NjJiOTZkOGJjZTZjNDk5YTZlYWNkOTgwNjg5OWQ2MzBkOWFkYTJjMA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NDcxN2VmMDRkMWMwMDhjNjQ2YzkyMDE2ZjRhNjRlYmJhYzNkMTdhMTFkOWM4
10
+ YjM5MmVlYmI5MjRiNzk5NmI4YjU4ZTA4NjU5Y2YzOWFjY2FmOGExMGIxZjQ5
11
+ MjQwMDMzNzlmMmQ4MmQ0YmI0MzhmZGM4YWRkM2Y5ODVhOGUwMWI=
12
+ data.tar.gz: !binary |-
13
+ NmExNzYxZWQxMGM4ODc2YzJiNjFlYTA2MTA5MjNkZTk0NjY4MzYxNWI4OGQ1
14
+ ZjA1NThjODIxNDcwZGFjMTA0N2JmMWIzNjk3ZmE0ZTI2MmI4ZmUyOTYyZmRj
15
+ N2ZkYjY0YzA5NzMwOTQxOGM3ZjE1MDM0ZTk4NWJhYjEzYjQxYTk=
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ Gemfile.lock
3
+ .bundle
4
+ vendor
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'http://rubygems.org'
2
+ gem 'rake'
3
+ gem 'gem_publisher'
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2012-2014 Elasticsearch <http://www.elasticsearch.org>
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ @files=[]
2
+
3
+ task :default do
4
+ system("rake -T")
5
+ end
6
+
@@ -0,0 +1,259 @@
1
+ # elapsed filter
2
+ #
3
+ # This filter tracks a pair of start/end events and calculates the elapsed
4
+ # time between them.
5
+
6
+ require "logstash/filters/base"
7
+ require "logstash/namespace"
8
+ require 'thread'
9
+
10
+
11
+ # The elapsed filter tracks a pair of start/end events and uses their
12
+ # timestamps to calculate the elapsed time between them.
13
+ #
14
+ # The filter has been developed to track the execution time of processes and
15
+ # other long tasks.
16
+ #
17
+ # The configuration looks like this:
18
+ # [source,ruby]
19
+ # filter {
20
+ # elapsed {
21
+ # start_tag => "start event tag"
22
+ # end_tag => "end event tag"
23
+ # unique_id_field => "id field name"
24
+ # timeout => seconds
25
+ # new_event_on_match => true/false
26
+ # }
27
+ # }
28
+ #
29
+ # The events managed by this filter must have some particular properties.
30
+ # The event describing the start of the task (the "start event") must contain
31
+ # a tag equal to `start_tag`. On the other side, the event describing the end
32
+ # of the task (the "end event") must contain a tag equal to `end_tag`. Both
33
+ # these two kinds of event need to own an ID field which identify uniquely that
34
+ # particular task. The name of this field is stored in `unique_id_field`.
35
+ #
36
+ # You can use a Grok filter to prepare the events for the elapsed filter.
37
+ # An example of configuration can be:
38
+ # [source,ruby]
39
+ # filter {
40
+ # grok {
41
+ # match => ["message", "%{TIMESTAMP_ISO8601} START id: (?<task_id>.*)"]
42
+ # add_tag => [ "taskStarted" ]
43
+ # }
44
+ #
45
+ # grok {
46
+ # match => ["message", "%{TIMESTAMP_ISO8601} END id: (?<task_id>.*)"]
47
+ # add_tag => [ "taskTerminated"]
48
+ # }
49
+ #
50
+ # elapsed {
51
+ # start_tag => "taskStarted"
52
+ # end_tag => "taskTerminated"
53
+ # unique_id_field => "task_id"
54
+ # }
55
+ # }
56
+ #
57
+ # The elapsed filter collects all the "start events". If two, or more, "start
58
+ # events" have the same ID, only the first one is recorded, the others are
59
+ # discarded.
60
+ #
61
+ # When an "end event" matching a previously collected "start event" is
62
+ # received, there is a match. The configuration property `new_event_on_match`
63
+ # tells where to insert the elapsed information: they can be added to the
64
+ # "end event" or a new "match event" can be created. Both events store the
65
+ # following information:
66
+ #
67
+ # * the tags `elapsed` and `elapsed.match`
68
+ # * the field `elapsed.time` with the difference, in seconds, between
69
+ # the two events timestamps
70
+ # * an ID filed with the task ID
71
+ # * the field `elapsed.timestamp_start` with the timestamp of the start event
72
+ #
73
+ # If the "end event" does not arrive before "timeout" seconds, the
74
+ # "start event" is discarded and an "expired event" is generated. This event
75
+ # contains:
76
+ #
77
+ # * the tags `elapsed` and `elapsed.expired_error`
78
+ # * a field called `elapsed.time` with the age, in seconds, of the
79
+ # "start event"
80
+ # * an ID filed with the task ID
81
+ # * the field `elapsed.timestamp_start` with the timestamp of the "start event"
82
+ #
83
+ class LogStash::Filters::Elapsed < LogStash::Filters::Base
84
+ PREFIX = "elapsed."
85
+ ELAPSED_FIELD = PREFIX + "time"
86
+ TIMESTAMP_START_EVENT_FIELD = PREFIX + "timestamp_start"
87
+ HOST_FIELD = "host"
88
+
89
+ ELAPSED_TAG = "elapsed"
90
+ EXPIRED_ERROR_TAG = PREFIX + "expired_error"
91
+ END_WITHOUT_START_TAG = PREFIX + "end_wtihout_start"
92
+ MATCH_TAG = PREFIX + "match"
93
+
94
+ config_name "elapsed"
95
+ milestone 1
96
+
97
+ # The name of the tag identifying the "start event"
98
+ config :start_tag, :validate => :string, :required => true
99
+
100
+ # The name of the tag identifying the "end event"
101
+ config :end_tag, :validate => :string, :required => true
102
+
103
+ # The name of the field containing the task ID.
104
+ # This value must uniquely identify the task in the system, otherwise
105
+ # it's impossible to match the couple of events.
106
+ config :unique_id_field, :validate => :string, :required => true
107
+
108
+ # The amount of seconds after an "end event" can be considered lost.
109
+ # The corresponding "start event" is discarded and an "expired event"
110
+ # is generated. The default value is 30 minutes (1800 seconds).
111
+ config :timeout, :validate => :number, :required => false, :default => 1800
112
+
113
+ # This property manage what to do when an "end event" matches a "start event".
114
+ # If it's set to `false` (default value), the elapsed information are added
115
+ # to the "end event"; if it's set to `true` a new "match event" is created.
116
+ config :new_event_on_match, :validate => :boolean, :required => false, :default => false
117
+
118
+ public
119
+ def register
120
+ @mutex = Mutex.new
121
+ # This is the state of the filter. The keys are the "unique_id_field",
122
+ # the values are couples of values: <start event, age>
123
+ @start_events = {}
124
+
125
+ @logger.info("Elapsed, timeout: #{@timeout} seconds")
126
+ end
127
+
128
+ # Getter method used for the tests
129
+ def start_events
130
+ @start_events
131
+ end
132
+
133
+ def filter(event)
134
+ return unless filter?(event)
135
+
136
+ unique_id = event[@unique_id_field]
137
+ return if unique_id.nil?
138
+
139
+ if(start_event?(event))
140
+ filter_matched(event)
141
+ @logger.info("Elapsed, 'start event' received", start_tag: @start_tag, unique_id_field: @unique_id_field)
142
+
143
+ @mutex.synchronize do
144
+ unless(@start_events.has_key?(unique_id))
145
+ @start_events[unique_id] = LogStash::Filters::Elapsed::Element.new(event)
146
+ end
147
+ end
148
+
149
+ elsif(end_event?(event))
150
+ filter_matched(event)
151
+ @logger.info("Elapsed, 'end event' received", end_tag: @end_tag, unique_id_field: @unique_id_field)
152
+
153
+ @mutex.lock
154
+ if(@start_events.has_key?(unique_id))
155
+ start_event = @start_events.delete(unique_id).event
156
+ @mutex.unlock
157
+ elapsed = event["@timestamp"] - start_event["@timestamp"]
158
+ if(@new_event_on_match)
159
+ elapsed_event = new_elapsed_event(elapsed, unique_id, start_event["@timestamp"])
160
+ filter_matched(elapsed_event)
161
+ yield elapsed_event if block_given?
162
+ else
163
+ return add_elapsed_info(event, elapsed, unique_id, start_event["@timestamp"])
164
+ end
165
+ else
166
+ @mutex.unlock
167
+ # The "start event" did not arrive.
168
+ event.tag(END_WITHOUT_START_TAG)
169
+ end
170
+ end
171
+ end # def filter
172
+
173
+ # The method is invoked by LogStash every 5 seconds.
174
+ def flush()
175
+ expired_elements = []
176
+
177
+ @mutex.synchronize do
178
+ increment_age_by(5)
179
+ expired_elements = remove_expired_elements()
180
+ end
181
+
182
+ return create_expired_events_from(expired_elements)
183
+ end
184
+
185
+ private
186
+ def increment_age_by(seconds)
187
+ @start_events.each_pair do |key, element|
188
+ element.age += seconds
189
+ end
190
+ end
191
+
192
+ # Remove the expired "start events" from the internal
193
+ # buffer and return them.
194
+ def remove_expired_elements()
195
+ expired = []
196
+ @start_events.delete_if do |key, element|
197
+ if(element.age >= @timeout)
198
+ expired << element
199
+ next true
200
+ end
201
+ next false
202
+ end
203
+
204
+ return expired
205
+ end
206
+
207
+ def create_expired_events_from(expired_elements)
208
+ events = []
209
+ expired_elements.each do |element|
210
+ error_event = LogStash::Event.new
211
+ error_event.tag(ELAPSED_TAG)
212
+ error_event.tag(EXPIRED_ERROR_TAG)
213
+
214
+ error_event[HOST_FIELD] = Socket.gethostname
215
+ error_event[@unique_id_field] = element.event[@unique_id_field]
216
+ error_event[ELAPSED_FIELD] = element.age
217
+ error_event[TIMESTAMP_START_EVENT_FIELD] = element.event["@timestamp"]
218
+
219
+ events << error_event
220
+ filter_matched(error_event)
221
+ end
222
+
223
+ return events
224
+ end
225
+
226
+ def start_event?(event)
227
+ return (event["tags"] != nil && event["tags"].include?(@start_tag))
228
+ end
229
+
230
+ def end_event?(event)
231
+ return (event["tags"] != nil && event["tags"].include?(@end_tag))
232
+ end
233
+
234
+ def new_elapsed_event(elapsed_time, unique_id, timestamp_start_event)
235
+ new_event = LogStash::Event.new
236
+ new_event[HOST_FIELD] = Socket.gethostname
237
+ return add_elapsed_info(new_event, elapsed_time, unique_id, timestamp_start_event)
238
+ end
239
+
240
+ def add_elapsed_info(event, elapsed_time, unique_id, timestamp_start_event)
241
+ event.tag(ELAPSED_TAG)
242
+ event.tag(MATCH_TAG)
243
+
244
+ event[ELAPSED_FIELD] = elapsed_time
245
+ event[@unique_id_field] = unique_id
246
+ event[TIMESTAMP_START_EVENT_FIELD] = timestamp_start_event
247
+
248
+ return event
249
+ end
250
+ end # class LogStash::Filters::Elapsed
251
+
252
+ class LogStash::Filters::Elapsed::Element
253
+ attr_accessor :event, :age
254
+
255
+ def initialize(event)
256
+ @event = event
257
+ @age = 0
258
+ end
259
+ end
@@ -0,0 +1,26 @@
1
+ Gem::Specification.new do |s|
2
+
3
+ s.name = 'logstash-filter-elapsed'
4
+ s.version = '0.1.0'
5
+ s.licenses = ['Apache License (2.0)']
6
+ s.summary = "This filter tracks a pair of start/end events and calculates the elapsed time between them."
7
+ s.description = "This filter tracks a pair of start/end events and calculates the elapsed time between them."
8
+ s.authors = ["Elasticsearch"]
9
+ s.email = 'richard.pijnenburg@elasticsearch.com'
10
+ s.homepage = "http://logstash.net/"
11
+ s.require_paths = ["lib"]
12
+
13
+ # Files
14
+ s.files = `git ls-files`.split($\)+::Dir.glob('vendor/*')
15
+
16
+ # Tests
17
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
18
+
19
+ # Special flag to let us know this is actually a logstash plugin
20
+ s.metadata = { "logstash_plugin" => "true", "group" => "filter" }
21
+
22
+ # Gem dependencies
23
+ s.add_runtime_dependency 'logstash', '>= 1.4.0', '< 2.0.0'
24
+
25
+ end
26
+
@@ -0,0 +1,9 @@
1
+ require "gem_publisher"
2
+
3
+ desc "Publish gem to RubyGems.org"
4
+ task :publish_gem do |t|
5
+ gem_file = Dir.glob(File.expand_path('../*.gemspec',File.dirname(__FILE__))).first
6
+ gem = GemPublisher.publish_if_updated(gem_file, :rubygems)
7
+ puts "Published #{gem}" if gem
8
+ end
9
+
@@ -0,0 +1,169 @@
1
+ require "net/http"
2
+ require "uri"
3
+ require "digest/sha1"
4
+
5
+ def vendor(*args)
6
+ return File.join("vendor", *args)
7
+ end
8
+
9
+ directory "vendor/" => ["vendor"] do |task, args|
10
+ mkdir task.name
11
+ end
12
+
13
+ def fetch(url, sha1, output)
14
+
15
+ puts "Downloading #{url}"
16
+ actual_sha1 = download(url, output)
17
+
18
+ if actual_sha1 != sha1
19
+ fail "SHA1 does not match (expected '#{sha1}' but got '#{actual_sha1}')"
20
+ end
21
+ end # def fetch
22
+
23
+ def file_fetch(url, sha1)
24
+ filename = File.basename( URI(url).path )
25
+ output = "vendor/#{filename}"
26
+ task output => [ "vendor/" ] do
27
+ begin
28
+ actual_sha1 = file_sha1(output)
29
+ if actual_sha1 != sha1
30
+ fetch(url, sha1, output)
31
+ end
32
+ rescue Errno::ENOENT
33
+ fetch(url, sha1, output)
34
+ end
35
+ end.invoke
36
+
37
+ return output
38
+ end
39
+
40
+ def file_sha1(path)
41
+ digest = Digest::SHA1.new
42
+ fd = File.new(path, "r")
43
+ while true
44
+ begin
45
+ digest << fd.sysread(16384)
46
+ rescue EOFError
47
+ break
48
+ end
49
+ end
50
+ return digest.hexdigest
51
+ ensure
52
+ fd.close if fd
53
+ end
54
+
55
+ def download(url, output)
56
+ uri = URI(url)
57
+ digest = Digest::SHA1.new
58
+ tmp = "#{output}.tmp"
59
+ Net::HTTP.start(uri.host, uri.port, :use_ssl => (uri.scheme == "https")) do |http|
60
+ request = Net::HTTP::Get.new(uri.path)
61
+ http.request(request) do |response|
62
+ fail "HTTP fetch failed for #{url}. #{response}" if [200, 301].include?(response.code)
63
+ size = (response["content-length"].to_i || -1).to_f
64
+ count = 0
65
+ File.open(tmp, "w") do |fd|
66
+ response.read_body do |chunk|
67
+ fd.write(chunk)
68
+ digest << chunk
69
+ if size > 0 && $stdout.tty?
70
+ count += chunk.bytesize
71
+ $stdout.write(sprintf("\r%0.2f%%", count/size * 100))
72
+ end
73
+ end
74
+ end
75
+ $stdout.write("\r \r") if $stdout.tty?
76
+ end
77
+ end
78
+
79
+ File.rename(tmp, output)
80
+
81
+ return digest.hexdigest
82
+ rescue SocketError => e
83
+ puts "Failure while downloading #{url}: #{e}"
84
+ raise
85
+ ensure
86
+ File.unlink(tmp) if File.exist?(tmp)
87
+ end # def download
88
+
89
+ def untar(tarball, &block)
90
+ require "archive/tar/minitar"
91
+ tgz = Zlib::GzipReader.new(File.open(tarball))
92
+ # Pull out typesdb
93
+ tar = Archive::Tar::Minitar::Input.open(tgz)
94
+ tar.each do |entry|
95
+ path = block.call(entry)
96
+ next if path.nil?
97
+ parent = File.dirname(path)
98
+
99
+ mkdir_p parent unless File.directory?(parent)
100
+
101
+ # Skip this file if the output file is the same size
102
+ if entry.directory?
103
+ mkdir path unless File.directory?(path)
104
+ else
105
+ entry_mode = entry.instance_eval { @mode } & 0777
106
+ if File.exists?(path)
107
+ stat = File.stat(path)
108
+ # TODO(sissel): Submit a patch to archive-tar-minitar upstream to
109
+ # expose headers in the entry.
110
+ entry_size = entry.instance_eval { @size }
111
+ # If file sizes are same, skip writing.
112
+ next if stat.size == entry_size && (stat.mode & 0777) == entry_mode
113
+ end
114
+ puts "Extracting #{entry.full_name} from #{tarball} #{entry_mode.to_s(8)}"
115
+ File.open(path, "w") do |fd|
116
+ # eof? check lets us skip empty files. Necessary because the API provided by
117
+ # Archive::Tar::Minitar::Reader::EntryStream only mostly acts like an
118
+ # IO object. Something about empty files in this EntryStream causes
119
+ # IO.copy_stream to throw "can't convert nil into String" on JRuby
120
+ # TODO(sissel): File a bug about this.
121
+ while !entry.eof?
122
+ chunk = entry.read(16384)
123
+ fd.write(chunk)
124
+ end
125
+ #IO.copy_stream(entry, fd)
126
+ end
127
+ File.chmod(entry_mode, path)
128
+ end
129
+ end
130
+ tar.close
131
+ File.unlink(tarball) if File.file?(tarball)
132
+ end # def untar
133
+
134
+ def ungz(file)
135
+
136
+ outpath = file.gsub('.gz', '')
137
+ tgz = Zlib::GzipReader.new(File.open(file))
138
+ begin
139
+ File.open(outpath, "w") do |out|
140
+ IO::copy_stream(tgz, out)
141
+ end
142
+ File.unlink(file)
143
+ rescue
144
+ File.unlink(outpath) if File.file?(outpath)
145
+ raise
146
+ end
147
+ tgz.close
148
+ end
149
+
150
+ desc "Process any vendor files required for this plugin"
151
+ task "vendor" do |task, args|
152
+
153
+ @files.each do |file|
154
+ download = file_fetch(file['url'], file['sha1'])
155
+ if download =~ /.tar.gz/
156
+ prefix = download.gsub('.tar.gz', '').gsub('vendor/', '')
157
+ untar(download) do |entry|
158
+ if !file['files'].nil?
159
+ next unless file['files'].include?(entry.full_name.gsub(prefix, ''))
160
+ out = entry.full_name.split("/").last
161
+ end
162
+ File.join('vendor', out)
163
+ end
164
+ elsif download =~ /.gz/
165
+ ungz(download)
166
+ end
167
+ end
168
+
169
+ end
@@ -0,0 +1,297 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+ require "logstash/filters/elapsed"
4
+ require "logstash/event"
5
+ require "socket"
6
+
7
+ describe LogStash::Filters::Elapsed do
8
+ START_TAG = "startTag"
9
+ END_TAG = "endTag"
10
+ ID_FIELD = "uniqueIdField"
11
+
12
+ def event(data)
13
+ data["message"] ||= "Log message"
14
+ LogStash::Event.new(data)
15
+ end
16
+
17
+ def start_event(data)
18
+ data["tags"] ||= []
19
+ data["tags"] << START_TAG
20
+ event(data)
21
+ end
22
+
23
+ def end_event(data = {})
24
+ data["tags"] ||= []
25
+ data["tags"] << END_TAG
26
+ event(data)
27
+ end
28
+
29
+ before(:each) do
30
+ setup_filter()
31
+ end
32
+
33
+ def setup_filter(config = {})
34
+ @config = {"start_tag" => START_TAG, "end_tag" => END_TAG, "unique_id_field" => ID_FIELD}
35
+ @config.merge!(config)
36
+ @filter = LogStash::Filters::Elapsed.new(@config)
37
+ @filter.register
38
+ end
39
+
40
+ context "General validation" do
41
+ describe "receiving an event without start or end tag" do
42
+ it "does not record it" do
43
+ @filter.filter(event("message" => "Log message"))
44
+ insist { @filter.start_events.size } == 0
45
+ end
46
+ end
47
+
48
+ describe "receiving an event with a different start/end tag from the ones specified in the configuration" do
49
+ it "does not record it" do
50
+ @filter.filter(event("tags" => ["tag1", "tag2"]))
51
+ insist { @filter.start_events.size } == 0
52
+ end
53
+ end
54
+ end
55
+
56
+ context "Start event" do
57
+ describe "receiving an event with a valid start tag" do
58
+ describe "but without an unique id field" do
59
+ it "does not record it" do
60
+ @filter.filter(event("tags" => ["tag1", START_TAG]))
61
+ insist { @filter.start_events.size } == 0
62
+ end
63
+ end
64
+
65
+ describe "and a valid id field" do
66
+ it "records it" do
67
+ event = start_event(ID_FIELD => "id123")
68
+ @filter.filter(event)
69
+
70
+ insist { @filter.start_events.size } == 1
71
+ insist { @filter.start_events["id123"].event } == event
72
+ end
73
+ end
74
+ end
75
+
76
+ describe "receiving two 'start events' for the same id field" do
77
+ it "keeps the first one and does not save the second one" do
78
+ args = {"tags" => [START_TAG], ID_FIELD => "id123"}
79
+ first_event = event(args)
80
+ second_event = event(args)
81
+
82
+ @filter.filter(first_event)
83
+ @filter.filter(second_event)
84
+
85
+ insist { @filter.start_events.size } == 1
86
+ insist { @filter.start_events["id123"].event } == first_event
87
+ end
88
+ end
89
+ end
90
+
91
+ context "End event" do
92
+ describe "receiving an event with a valid end tag" do
93
+ describe "and without an id" do
94
+ it "does nothing" do
95
+ insist { @filter.start_events.size } == 0
96
+ @filter.filter(end_event())
97
+ insist { @filter.start_events.size } == 0
98
+ end
99
+ end
100
+
101
+ describe "and with an id" do
102
+ describe "but without a previous 'start event'" do
103
+ it "adds a tag 'elapsed.end_witout_start' to the 'end event'" do
104
+ end_event = end_event(ID_FIELD => "id_123")
105
+
106
+ @filter.filter(end_event)
107
+
108
+ insist { end_event["tags"].include?("elapsed.end_wtihout_start") } == true
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ context "Start/end events interaction" do
116
+ describe "receiving a 'start event'" do
117
+ before(:each) do
118
+ @id_value = "id_123"
119
+ @start_event = start_event(ID_FIELD => @id_value)
120
+ @filter.filter(@start_event)
121
+ end
122
+
123
+ describe "and receiving an event with a valid end tag" do
124
+ describe "and without an id" do
125
+ it "does nothing" do
126
+ @filter.filter(end_event())
127
+ insist { @filter.start_events.size } == 1
128
+ insist { @filter.start_events[@id_value].event } == @start_event
129
+ end
130
+ end
131
+
132
+ describe "and an id different from the one of the 'start event'" do
133
+ it "does nothing" do
134
+ different_id_value = @id_value + "_different"
135
+ @filter.filter(end_event(ID_FIELD => different_id_value))
136
+
137
+ insist { @filter.start_events.size } == 1
138
+ insist { @filter.start_events[@id_value].event } == @start_event
139
+ end
140
+ end
141
+
142
+ describe "and the same id of the 'start event'" do
143
+ it "deletes the recorded 'start event'" do
144
+ insist { @filter.start_events.size } == 1
145
+
146
+ @filter.filter(end_event(ID_FIELD => @id_value))
147
+
148
+ insist { @filter.start_events.size } == 0
149
+ end
150
+
151
+ shared_examples_for "match event" do
152
+ it "contains the tag 'elapsed'" do
153
+ insist { @match_event["tags"].include?("elapsed") } == true
154
+ end
155
+
156
+ it "contains the tag tag 'elapsed.match'" do
157
+ insist { @match_event["tags"].include?("elapsed.match") } == true
158
+ end
159
+
160
+ it "contains an 'elapsed.time field' with the elapsed time" do
161
+ insist { @match_event["elapsed.time"] } == 10
162
+ end
163
+
164
+ it "contains an 'elapsed.timestamp_start field' with the timestamp of the 'start event'" do
165
+ insist { @match_event["elapsed.timestamp_start"] } == @start_event["@timestamp"]
166
+ end
167
+
168
+ it "contains an 'id field'" do
169
+ insist { @match_event[ID_FIELD] } == @id_value
170
+ end
171
+ end
172
+
173
+ context "if 'new_event_on_match' is set to 'true'" do
174
+ before(:each) do
175
+ # I need to create a new filter because I need to set
176
+ # the config property 'new_event_on_match" to 'true'.
177
+ setup_filter("new_event_on_match" => true)
178
+ @start_event = start_event(ID_FIELD => @id_value)
179
+ @filter.filter(@start_event)
180
+
181
+ end_timestamp = @start_event["@timestamp"] + 10
182
+ end_event = end_event(ID_FIELD => @id_value, "@timestamp" => end_timestamp)
183
+ @filter.filter(end_event) do |new_event|
184
+ @match_event = new_event
185
+ end
186
+ end
187
+
188
+ context "creates a new event that" do
189
+ it_behaves_like "match event"
190
+
191
+ it "contains the 'host field'" do
192
+ insist { @match_event["host"] } == Socket.gethostname
193
+ end
194
+ end
195
+ end
196
+
197
+ context "if 'new_event_on_match' is set to 'false'" do
198
+ before(:each) do
199
+ end_timestamp = @start_event["@timestamp"] + 10
200
+ end_event = end_event(ID_FIELD => @id_value, "@timestamp" => end_timestamp)
201
+ @filter.filter(end_event)
202
+
203
+ @match_event = end_event
204
+ end
205
+
206
+ context "modifies the 'end event' that" do
207
+ it_behaves_like "match event"
208
+ end
209
+ end
210
+
211
+ end
212
+ end
213
+ end
214
+ end
215
+
216
+ describe "#flush" do
217
+ def setup(timeout = 1000)
218
+ @config["timeout"] = timeout
219
+ @filter = LogStash::Filters::Elapsed.new(@config)
220
+ @filter.register
221
+
222
+ @start_event_1 = start_event(ID_FIELD => "1")
223
+ @start_event_2 = start_event(ID_FIELD => "2")
224
+ @start_event_3 = start_event(ID_FIELD => "3")
225
+
226
+ @filter.filter(@start_event_1)
227
+ @filter.filter(@start_event_2)
228
+ @filter.filter(@start_event_3)
229
+
230
+ # Force recorded events to different ages
231
+ @filter.start_events["2"].age = 25
232
+ @filter.start_events["3"].age = 26
233
+ end
234
+
235
+ it "increments the 'age' of all the recorded 'start events' by 5 seconds" do
236
+ setup()
237
+ old_age = ages()
238
+
239
+ @filter.flush()
240
+
241
+ ages().each_with_index do |new_age, i|
242
+ insist { new_age } == (old_age[i] + 5)
243
+ end
244
+ end
245
+
246
+ def ages()
247
+ @filter.start_events.each_value.map{|element| element.age }
248
+ end
249
+
250
+ context "if the 'timeout interval' is set to 30 seconds" do
251
+ before(:each) do
252
+ setup(30)
253
+
254
+ @expired_events = @filter.flush()
255
+
256
+ insist { @filter.start_events.size } == 1
257
+ insist { @expired_events.size } == 2
258
+ end
259
+
260
+ it "deletes the recorded 'start events' with 'age' greater, or equal to, the timeout" do
261
+ insist { @filter.start_events.key?("1") } == true
262
+ insist { @filter.start_events.key?("2") } == false
263
+ insist { @filter.start_events.key?("3") } == false
264
+ end
265
+
266
+ it "creates a new event with tag 'elapsed.expired_error' for each expired 'start event'" do
267
+ insist { @expired_events[0]["tags"].include?("elapsed.expired_error") } == true
268
+ insist { @expired_events[1]["tags"].include?("elapsed.expired_error") } == true
269
+ end
270
+
271
+ it "creates a new event with tag 'elapsed' for each expired 'start event'" do
272
+ insist { @expired_events[0]["tags"].include?("elapsed") } == true
273
+ insist { @expired_events[1]["tags"].include?("elapsed") } == true
274
+ end
275
+
276
+ it "creates a new event containing the 'id field' of the expired 'start event'" do
277
+ insist { @expired_events[0][ID_FIELD] } == "2"
278
+ insist { @expired_events[1][ID_FIELD] } == "3"
279
+ end
280
+
281
+ it "creates a new event containing an 'elapsed.time field' with the age of the expired 'start event'" do
282
+ insist { @expired_events[0]["elapsed.time"] } == 30
283
+ insist { @expired_events[1]["elapsed.time"] } == 31
284
+ end
285
+
286
+ it "creates a new event containing an 'elapsed.timestamp_start field' with the timestamp of the expired 'start event'" do
287
+ insist { @expired_events[0]["elapsed.timestamp_start"] } == @start_event_2["@timestamp"]
288
+ insist { @expired_events[1]["elapsed.timestamp_start"] } == @start_event_3["@timestamp"]
289
+ end
290
+
291
+ it "creates a new event containing a 'host field' for each expired 'start event'" do
292
+ insist { @expired_events[0]["host"] } == Socket.gethostname
293
+ insist { @expired_events[1]["host"] } == Socket.gethostname
294
+ end
295
+ end
296
+ end
297
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logstash-filter-elapsed
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Elasticsearch
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-11-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: logstash
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 1.4.0
20
+ - - <
21
+ - !ruby/object:Gem::Version
22
+ version: 2.0.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.4.0
30
+ - - <
31
+ - !ruby/object:Gem::Version
32
+ version: 2.0.0
33
+ description: This filter tracks a pair of start/end events and calculates the elapsed
34
+ time between them.
35
+ email: richard.pijnenburg@elasticsearch.com
36
+ executables: []
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - .gitignore
41
+ - Gemfile
42
+ - LICENSE
43
+ - Rakefile
44
+ - lib/logstash/filters/elapsed.rb
45
+ - logstash-filter-elapsed.gemspec
46
+ - rakelib/publish.rake
47
+ - rakelib/vendor.rake
48
+ - spec/filters/elapsed_spec.rb
49
+ homepage: http://logstash.net/
50
+ licenses:
51
+ - Apache License (2.0)
52
+ metadata:
53
+ logstash_plugin: 'true'
54
+ group: filter
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubyforge_project:
71
+ rubygems_version: 2.4.1
72
+ signing_key:
73
+ specification_version: 4
74
+ summary: This filter tracks a pair of start/end events and calculates the elapsed
75
+ time between them.
76
+ test_files:
77
+ - spec/filters/elapsed_spec.rb