logstash-input-bro 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 98252e0b73f359a7d9970b1816199f19b7e4a9bc
4
+ data.tar.gz: 1fabda0d94eb4b9ba80f8f11940d7e6cba16120b
5
+ SHA512:
6
+ metadata.gz: adaa8c0ee26c19d4cff2a3c5697464d027b61af6de91f319210249a0117a67ef9cb4159d61da1281c2c419e494e252c7cd88b26faa552921fb52c2475b81a6a5
7
+ data.tar.gz: 3ff95f51b92b1c82df5d5b33b113e4ef7e106141e285900f4dca78793059bec8277f5424e8b8c0abc19422c264fd5b5f95314d653c96f96860951a2e1ae1a713
@@ -0,0 +1,4 @@
1
+ ## 0.1.0
2
+ - Working with bro input files and multiple workers using mutex.
3
+ ## 0.0.1
4
+ - Snagged logstash-input-file code to use as base.
@@ -0,0 +1,5 @@
1
+ The following is a list of people who have contributed ideas, code, bug
2
+ reports, or in general have helped logstash along its way.
3
+
4
+ Contributors:
5
+ * Blake Mackey (brashendeavours)
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2015 BrashEndeavours <http://www.brashendeavours.co>
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.
@@ -0,0 +1,5 @@
1
+ BrashEndeavours
2
+ Copyright 2015 BrashEndeavours
3
+
4
+ This product includes software developed by The Apache Software
5
+ Foundation (http://www.apache.org/).
@@ -0,0 +1,86 @@
1
+ # Logstash Plugin
2
+
3
+ This is a plugin for [Logstash](https://github.com/elastic/logstash).
4
+
5
+ It is fully free and fully open source. The license is Apache 2.0, meaning you are pretty much free to use it however you want in whatever way.
6
+
7
+ ## Documentation
8
+
9
+ Logstash provides infrastructure to automatically generate documentation for this plugin. We use the asciidoc format to write documentation so any comments in the source code will be first converted into asciidoc and then into html. All plugin documentation are placed under one [central location](http://www.elastic.co/guide/en/logstash/current/).
10
+
11
+ - For formatting code or config example, you can use the asciidoc `[source,ruby]` directive
12
+ - For more asciidoc formatting tips, see the excellent reference here https://github.com/elastic/docs#asciidoc-guide
13
+
14
+ ## Need Help?
15
+
16
+ Need help? Try #logstash on freenode IRC or the https://discuss.elastic.co/c/logstash discussion forum.
17
+
18
+ ## Developing
19
+
20
+ ### 1. Plugin Developement and Testing
21
+
22
+ #### Code
23
+ - To get started, you'll need JRuby with the Bundler gem installed.
24
+
25
+ - Create a new plugin or clone and existing from the GitHub [logstash-plugins](https://github.com/logstash-plugins) organization. We also provide [example plugins](https://github.com/logstash-plugins?query=example).
26
+
27
+ - Install dependencies
28
+ ```sh
29
+ bundle install
30
+ ```
31
+
32
+ #### Test
33
+
34
+ - Update your dependencies
35
+
36
+ ```sh
37
+ bundle install
38
+ ```
39
+
40
+ - Run tests
41
+
42
+ ```sh
43
+ bundle exec rspec
44
+ ```
45
+
46
+ ### 2. Running your unpublished Plugin in Logstash
47
+
48
+ #### 2.1 Run in a local Logstash clone
49
+
50
+ - Edit Logstash `Gemfile` and add the local plugin path, for example:
51
+ ```ruby
52
+ gem "logstash-filter-awesome", :path => "/your/local/logstash-filter-awesome"
53
+ ```
54
+ - Install plugin
55
+ ```sh
56
+ bin/plugin install --no-verify
57
+ ```
58
+ - Run Logstash with your plugin
59
+ ```sh
60
+ bin/logstash -e 'filter {awesome {}}'
61
+ ```
62
+ At this point any modifications to the plugin code will be applied to this local Logstash setup. After modifying the plugin, simply rerun Logstash.
63
+
64
+ #### 2.2 Run in an installed Logstash
65
+
66
+ You can use the same **2.1** method to run your plugin in an installed Logstash by editing its `Gemfile` and pointing the `:path` to your local plugin development directory or you can build the gem and install it using:
67
+
68
+ - Build your plugin gem
69
+ ```sh
70
+ gem build logstash-filter-awesome.gemspec
71
+ ```
72
+ - Install the plugin from the Logstash home
73
+ ```sh
74
+ bin/plugin install /your/local/plugin/logstash-filter-awesome.gem
75
+ ```
76
+ - Start Logstash and proceed to test the plugin
77
+
78
+ ## Contributing
79
+
80
+ All contributions are welcome: ideas, patches, documentation, bug reports, complaints, and even something you drew up on a napkin.
81
+
82
+ Programming is not a required skill. Whatever you've seen about open source and maintainers or community members saying "send patches or die" - you will not see that here.
83
+
84
+ It is more important to the community that you are able to contribute.
85
+
86
+ For more information about contributing, see the [CONTRIBUTING](https://github.com/elastic/logstash/blob/master/CONTRIBUTING.md) file.
@@ -0,0 +1,340 @@
1
+ # encoding: utf-8
2
+ require "logstash/namespace"
3
+ require "logstash/inputs/base"
4
+ require "logstash/codecs/identity_map_codec"
5
+
6
+ require "pathname"
7
+ require "socket" # for Socket.gethostname
8
+
9
+ # Stream events from a bro log file, normally by tailing them in a manner
10
+ # similar to `tail -0F` but optionally reading them from the
11
+ # beginning.
12
+ #
13
+ # Each event is assumed to be one line, because of the nature of the
14
+ # way bro logs are defined.
15
+ #
16
+ # The plugin aims to track changing files and emit new content as it's
17
+ # appended to each file. It's not well-suited for reading a file from
18
+ # beginning to end and storing all of it in a single event (not even
19
+ # with the multiline codec or filter).
20
+ #
21
+ # ==== Tracking of current position in watched files
22
+ #
23
+ # The plugin keeps track of the current position in the file by
24
+ # recording it in a separate file named sincedb. This makes it
25
+ # possible to stop and restart Logstash and have it pick up where it
26
+ # left off without missing the lines that were added to the file while
27
+ # Logstash was stopped.
28
+ #
29
+ # By default, the sincedb file is placed in the home directory of the
30
+ # user running Logstash with a filename based on the filename patterns
31
+ # being watched (i.e. the `path` option). Thus, changing the filename
32
+ # patterns will result in a new sincedb file being used and any
33
+ # existing current position state will be lost. If you change your
34
+ # patterns with any frequency it might make sense to explicitly choose
35
+ # a sincedb path with the `sincedb_path` option.
36
+ #
37
+ # Sincedb files are text files with four columns:
38
+ #
39
+ # . The inode number (or equivalent).
40
+ # . The major device number of the file system (or equivalent).
41
+ # . The minor device number of the file system (or equivalent).
42
+ # . The current byte offset within the file.
43
+ #
44
+ # On non-Windows systems you can obtain the inode number of a file
45
+ # with e.g. `ls -li`.
46
+ #
47
+ # ==== File rotation
48
+ #
49
+ # File rotation is detected and handled by this input, regardless of
50
+ # whether the file is rotated via a rename or a copy operation. To
51
+ # support programs that write to the rotated file for some time after
52
+ # the rotation has taken place, include both the original filename and
53
+ # the rotated filename (e.g. /var/log/syslog and /var/log/syslog.1) in
54
+ # the filename patterns to watch (the `path` option). Note that the
55
+ # rotated filename will be treated as a new file so if
56
+ # `start_position` is set to 'beginning' the rotated file will be
57
+ # reprocessed.
58
+ #
59
+ # With the default value of `start_position` ('end') any messages
60
+ # written to the end of the file between the last read operation prior
61
+ # to the rotation and its reopening under the new name (an interval
62
+ # determined by the `stat_interval` and `discover_interval` options)
63
+ # will not get picked up.
64
+ class LogStash::Inputs::Bro < LogStash::Inputs::Base
65
+ config_name "bro"
66
+
67
+ # The path to the file(s) to use as an input.
68
+ # You can use filename patterns here, such as `/var/log/*.log`.
69
+ # If you use a pattern like `/var/log/**/*.log`, a recursive search
70
+ # of `/var/log` will be done for all `*.log` files.
71
+ # Paths must be absolute and cannot be relative.
72
+ #
73
+ # You may also configure multiple paths. See an example
74
+ # on the <<array,Logstash configuration page>>.
75
+ config :path, :validate => :string, :required => true
76
+
77
+ # How often (in seconds) we stat files to see if they have been modified.
78
+ # Increasing this interval will decrease the number of system calls we make,
79
+ # but increase the time to detect new log lines.
80
+ config :stat_interval, :validate => :number, :default => 1
81
+
82
+ # How often (in seconds) we expand the filename patterns in the
83
+ # `path` option to discover new files to watch.
84
+ config :discover_interval, :validate => :number, :default => 15
85
+
86
+ # Path of the sincedb database file (keeps track of the current
87
+ # position of monitored log files) that will be written to disk.
88
+ # The default will write sincedb files to some path matching `$HOME/.sincedb*`
89
+ # NOTE: it must be a file path and not a directory path
90
+ config :sincedb_path, :validate => :string
91
+
92
+ # How often (in seconds) to write a since database with the current position of
93
+ # monitored log files.
94
+ config :sincedb_write_interval, :validate => :number, :default => 15
95
+
96
+ # Choose where Logstash starts initially reading files: at the beginning or
97
+ # at the end. The default behavior treats files like live streams and thus
98
+ # starts at the end. If you have old data you want to import, set this
99
+ # to 'beginning'.
100
+ #
101
+ # This option only modifies "first contact" situations where a file
102
+ # is new and not seen before, i.e. files that don't have a current
103
+ # position recorded in a sincedb file read by Logstash. If a file
104
+ # has already been seen before, this option has no effect and the
105
+ # position recorded in the sincedb file will be used.
106
+ config :start_position, :validate => [ "beginning", "end"], :default => "end"
107
+
108
+ # set the new line delimiter, defaults to "\n"
109
+ config :delimiter, :validate => :string, :default => "\n"
110
+
111
+ public
112
+ def register
113
+ require "addressable/uri"
114
+ require "filewatch/tail"
115
+ require "digest/md5"
116
+ @logger.info("Registering file input", :path => @path)
117
+ @host = Socket.gethostname.force_encoding(Encoding::UTF_8)
118
+
119
+ @tail_config = {
120
+ #:exclude => @exclude,
121
+ :stat_interval => @stat_interval,
122
+ :discover_interval => @discover_interval,
123
+ :sincedb_write_interval => @sincedb_write_interval,
124
+ :delimiter => @delimiter,
125
+ :logger => @logger,
126
+ }
127
+
128
+ if Pathname.new(path).relative?
129
+ raise ArgumentError.new("File paths must be absolute, relative path specified: #{path}")
130
+ end
131
+
132
+ if @sincedb_path.nil?
133
+ if ENV["SINCEDB_DIR"].nil? && ENV["HOME"].nil?
134
+ @logger.error("No SINCEDB_DIR or HOME environment variable set, I don't know where " \
135
+ "to keep track of the files I'm watching. Either set " \
136
+ "HOME or SINCEDB_DIR in your environment, or set sincedb_path in " \
137
+ "in your Logstash config for the file input with " \
138
+ "path '#{@path.inspect}'")
139
+ raise # TODO(sissel): HOW DO I FAIL PROPERLY YO
140
+ end
141
+
142
+ #pick SINCEDB_DIR if available, otherwise use HOME
143
+ sincedb_dir = ENV["SINCEDB_DIR"] || ENV["HOME"]
144
+
145
+ # Join by ',' to make it easy for folks to know their own sincedb
146
+ # generated path (vs, say, inspecting the @path array)
147
+ @sincedb_path = File.join(sincedb_dir, ".sincedb_" + Digest::MD5.hexdigest(@path))
148
+
149
+ # Migrate any old .sincedb to the new file (this is for version <=1.1.1 compatibility)
150
+ old_sincedb = File.join(sincedb_dir, ".sincedb")
151
+ if File.exists?(old_sincedb)
152
+ @logger.info("Renaming old ~/.sincedb to new one", :old => old_sincedb,
153
+ :new => @sincedb_path)
154
+ File.rename(old_sincedb, @sincedb_path)
155
+ end
156
+
157
+ @logger.info("No sincedb_path set, generating one based on the file path",
158
+ :sincedb_path => @sincedb_path, :path => @path)
159
+ end
160
+
161
+ if File.directory?(@sincedb_path)
162
+ raise ArgumentError.new("The \"sincedb_path\" argument must point to a file, received a directory: \"#{@sincedb_path}\"")
163
+ end
164
+
165
+ @tail_config[:sincedb_path] = @sincedb_path
166
+
167
+ if @start_position == "beginning"
168
+ @tail_config[:start_new_files_at] = :beginning
169
+ end
170
+
171
+ @codec = LogStash::Codecs::IdentityMapCodec.new(LogStash::Codecs::Plain.new)
172
+ @filter_initialized = false
173
+ @mutex = Mutex.new
174
+ end # def register
175
+
176
+ def begin_tailing
177
+ stop # if the pipeline restarts this input.
178
+ @tail = FileWatch::Tail.new(@tail_config)
179
+ @tail.logger = @logger
180
+ @tail.tail(path)
181
+ end
182
+
183
+ def run(queue)
184
+ begin_tailing
185
+ @tail.subscribe do |path, line|
186
+ log_line_received(path, line)
187
+ @codec.decode(line, path) do |event|
188
+ # path is the identity
189
+ # Note: this block is cached in the
190
+ # identity_map_codec for use when
191
+ # buffered lines are flushed.
192
+
193
+ # Ignore the setup lines initially
194
+ if event["message"].start_with?('#')
195
+ next
196
+ end
197
+
198
+ queue << add_path_meta(event, path)
199
+ end
200
+ end
201
+ end # def run
202
+
203
+ def log_line_received(path, line)
204
+ return if !@logger.debug?
205
+ @logger.debug("Received line", :path => path, :text => line)
206
+ end
207
+
208
+ def add_path_meta(event, path)
209
+ @path = path
210
+ # Until we get a line that doesn't have a comment, then we
211
+ # check if we're setup.
212
+ unless @filter_initialized
213
+ initialize_filter()
214
+ print_config()
215
+ end
216
+ event["@host"] = @host
217
+ event["@path"] = @path
218
+ event["@message"] = event["message"]
219
+ event.remove("message")
220
+ event.remove("host") unless event["host"].nil?
221
+ event.remove("@version") unless event["@version"].nil?
222
+
223
+ values = event["@message"].split(/#{@separator}/)
224
+ values.each_index do |i|
225
+
226
+ if values[i].start_with?(@empty_field, @unset_field) then next end
227
+
228
+ field_name = @fields[i] || "uninitialized#{i+1}"
229
+ field_type = @types[i] || "string"
230
+
231
+
232
+ if field_type.start_with?("interval", "double")
233
+ values[i] = values[i].to_f
234
+ elsif field_type.start_with?("count", "int")
235
+ values[i] = values[i].to_i
236
+ elsif field_type.start_with?("set", "vector")
237
+ if field_type =~ /interval/ || /double/
238
+ values[i] = values[i].split(',').map(&:to_f)
239
+ elsif field_type =~ /int/ || /count/
240
+ values[i] = values[i].split(',').map(&:to_i)
241
+ elsif field_type =~ /time/
242
+ values[i] = values[i].split(',').map do |block_value|
243
+ # Truncate timestamp to millisecond precision
244
+ secs = BigDecimal.new(block_value)
245
+ msec = secs * 1000 # convert to whole number of milliseconds
246
+ msec = msec.to_i
247
+ block_value = Time.at(msec / 1000, (msec % 1000) * 1000).utc
248
+ end
249
+ else
250
+ values[i] = values[i].split(',')
251
+ end
252
+
253
+ elsif field_type.start_with?("time") # Create an actual timestamp
254
+ # Truncate timestamp to millisecond precision
255
+ secs = BigDecimal.new(values[i])
256
+ #event["#{field_name}_secs"] = secs.to_f
257
+ msec = secs * 1000 # convert to whole number of milliseconds
258
+ msec = msec.to_i
259
+ values[i] = Time.at(msec / 1000, (msec % 1000) * 1000).utc
260
+ end
261
+
262
+ field_array = field_name.split('.')
263
+ field_hash = field_array.reverse.inject(values[i]) { |a, n| { n => a } }
264
+ field_hash = field_hash[field_hash.keys[0]]
265
+ #event[field_name] = values[i]
266
+ if event.include?(field_array[0])
267
+ event[field_array[0]] = event[field_array[0]].to_hash.merge!(field_hash) { |_, v1, v2| [v1,v2] }
268
+ else
269
+ event[field_array[0]] = field_hash
270
+ end
271
+ end
272
+
273
+ # Add some additional data
274
+ if event.include?("@timestamp")
275
+ event["timestamp"] = {}
276
+ event["timestamp"]["start"] = event["@timestamp"]
277
+ if event.include?("duration")
278
+ event["timestamp"]["duration"] = event["duration"]
279
+ event["timestamp"]["end"] = LogStash::Timestamp.new(event["timestamp"]["start"] + event["duration"].to_f)
280
+ event.remove("duration")
281
+ end
282
+ end
283
+ #event["bro_logtype"] = @meta[current_event][:logname]
284
+ #rescue => e
285
+ # event.tag "_broparsefailure"
286
+ # @logger.warn("Trouble parsing bro", :event => event, :exception => e)
287
+ # print e
288
+ # return
289
+ #end # begin
290
+
291
+
292
+ decorate(event)
293
+ event
294
+ end
295
+
296
+ def initialize_filter()
297
+ @mutex.synchronize do
298
+ unless @filter_initialized
299
+ lines = File.foreach(@path).first(8)
300
+ lines.each do |line|
301
+ startword = line.chomp!.split.first
302
+ case startword
303
+ when "#separator"
304
+ @separator = line.partition(/ /)[2]
305
+ when "#set_separator"
306
+ @set_separator = line.partition(/#{@separator}/)[2]
307
+ when "#empty_field"
308
+ @empty_field = line.partition(/#{@separator}/)[2]
309
+ when "#unset_field"
310
+ @unset_field = line.partition(/#{@separator}/)[2]
311
+ when "#path"
312
+ @logname = line.partition(/#{@separator}/)[2]
313
+ when "#fields"
314
+ @fields = line.partition(/#{@separator}/)[2].split(/#{@separator}/)
315
+ when "#types"
316
+ @types = line.partition(/#{@separator}/)[2].split(/#{@separator}/)
317
+ end
318
+ end # line.each
319
+ @filter_initialized = true
320
+ end # filter_initialized
321
+ end # synchronize
322
+ end # def initialize_filter
323
+
324
+ def print_config()
325
+ @logger.info("separator: \"#{@separator}\"")
326
+ @logger.info("set separator: \"#{@set_separator}\"")
327
+ @logger.info("empty field: \"#{@empty_field}\"")
328
+ @logger.info("unset field: \"#{@unset_field}\"")
329
+ @logger.info("logname: \"#{@logname}\"")
330
+ @logger.info("columns: \"#{@fields}\"")
331
+ @logger.info("types: \"#{@types}\"")
332
+ end # def print_path_config
333
+
334
+ def stop
335
+ # in filewatch >= 0.6.7, quit will closes and forget all files
336
+ # but it will write their last read positions to since_db
337
+ # beforehand
338
+ @tail.quit if @tail
339
+ end
340
+ end # class LogStash::Inputs::File
@@ -0,0 +1,34 @@
1
+ Gem::Specification.new do |s|
2
+
3
+ s.name = 'logstash-input-bro'
4
+ s.version = '0.1.0'
5
+ s.licenses = ['Apache License (2.0)']
6
+ s.summary = "Stream events from bro formatted files."
7
+ s.description = "This gem is a logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/plugin install gemname. This gem is not a stand-alone program"
8
+ s.authors = ["Blake Mackey"]
9
+ s.email = 'blake_mackey@hotmail.com'
10
+ s.homepage = "http://github.com/brashendeavours/logstash-input-bro"
11
+ s.require_paths = ["lib"]
12
+
13
+ # Files
14
+ s.files = Dir['lib/**/*','spec/**/*','vendor/**/*','*.gemspec','*.md','CONTRIBUTORS','Gemfile','LICENSE','NOTICE.TXT']
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", "logstash_group" => "input" }
21
+
22
+ # Gem dependencies
23
+ s.add_runtime_dependency "logstash-core", ">= 2.0.0.beta2", "< 3.0.0"
24
+
25
+ s.add_runtime_dependency 'logstash-codec-plain'
26
+ s.add_runtime_dependency 'addressable'
27
+ s.add_runtime_dependency 'filewatch', ['>= 0.6.7', '~> 0.6']
28
+ s.add_runtime_dependency 'logstash-codec-multiline', ['~> 2.0.3']
29
+
30
+ s.add_development_dependency 'stud', ['~> 0.0.19']
31
+ s.add_development_dependency 'logstash-devutils'
32
+ s.add_development_dependency 'logstash-codec-json'
33
+ end
34
+
@@ -0,0 +1,307 @@
1
+ # encoding: utf-8
2
+
3
+ require "logstash/devutils/rspec/spec_helper"
4
+ require "tempfile"
5
+ require "stud/temporary"
6
+ require "logstash/inputs/bro"
7
+
8
+ FILE_DELIMITER = LogStash::Environment.windows? ? "\r\n" : "\n"
9
+
10
+ describe LogStash::Inputs::Bro do
11
+
12
+ before(:all) do
13
+ @abort_on_exception = Thread.abort_on_exception
14
+ Thread.abort_on_exception = true
15
+ end
16
+
17
+ after(:all) do
18
+ Thread.abort_on_exception = @abort_on_exception
19
+ end
20
+
21
+ it_behaves_like "an interruptible input plugin" do
22
+ let(:config) do
23
+ {
24
+ "path" => Stud::Temporary.pathname,
25
+ "sincedb_path" => Stud::Temporary.pathname
26
+ }
27
+ end
28
+ end
29
+
30
+ it "should starts at the end of an existing file" do
31
+ tmpfile_path = Stud::Temporary.pathname
32
+ sincedb_path = Stud::Temporary.pathname
33
+
34
+ conf = <<-CONFIG
35
+ input {
36
+ bro {
37
+ type => "blah"
38
+ path => "#{tmpfile_path}"
39
+ sincedb_path => "#{sincedb_path}"
40
+ delimiter => "#{FILE_DELIMITER}"
41
+ }
42
+ }
43
+ CONFIG
44
+
45
+ File.open(tmpfile_path, "w") do |fd|
46
+ fd.puts("ignore me 1")
47
+ fd.puts("ignore me 2")
48
+ end
49
+
50
+ events = input(conf) do |pipeline, queue|
51
+
52
+ # at this point the plugins
53
+ # threads might still be initializing so we cannot know when the
54
+ # file plugin will have seen the original file, it could see it
55
+ # after the first(s) hello world appends below, hence the
56
+ # retry logic.
57
+
58
+ events = []
59
+
60
+ retries = 0
61
+ while retries < 20
62
+ File.open(tmpfile_path, "a") do |fd|
63
+ fd.puts("hello")
64
+ fd.puts("world")
65
+ end
66
+
67
+ if queue.size >= 2
68
+ events = 2.times.collect { queue.pop }
69
+ break
70
+ end
71
+
72
+ sleep(0.1)
73
+ retries += 1
74
+ end
75
+
76
+ events
77
+ end
78
+
79
+ insist { events[0]["message"] } == "hello"
80
+ insist { events[1]["message"] } == "world"
81
+ end
82
+
83
+ it "should start at the beginning of an existing file" do
84
+ tmpfile_path = Stud::Temporary.pathname
85
+ sincedb_path = Stud::Temporary.pathname
86
+
87
+ conf = <<-CONFIG
88
+ input {
89
+ bro {
90
+ type => "blah"
91
+ path => "#{tmpfile_path}"
92
+ start_position => "beginning"
93
+ sincedb_path => "#{sincedb_path}"
94
+ delimiter => "#{FILE_DELIMITER}"
95
+ }
96
+ }
97
+ CONFIG
98
+
99
+ File.open(tmpfile_path, "a") do |fd|
100
+ fd.puts("hello")
101
+ fd.puts("world")
102
+ end
103
+
104
+ events = input(conf) do |pipeline, queue|
105
+ 2.times.collect { queue.pop }
106
+ end
107
+
108
+ insist { events[0]["message"] } == "hello"
109
+ insist { events[1]["message"] } == "world"
110
+ end
111
+
112
+ it "should restarts at the sincedb value" do
113
+ tmpfile_path = Stud::Temporary.pathname
114
+ sincedb_path = Stud::Temporary.pathname
115
+
116
+ conf = <<-CONFIG
117
+ input {
118
+ bro {
119
+ type => "blah"
120
+ path => "#{tmpfile_path}"
121
+ start_position => "beginning"
122
+ sincedb_path => "#{sincedb_path}"
123
+ delimiter => "#{FILE_DELIMITER}"
124
+ }
125
+ }
126
+ CONFIG
127
+
128
+ File.open(tmpfile_path, "w") do |fd|
129
+ fd.puts("hello3")
130
+ fd.puts("world3")
131
+ end
132
+
133
+ events = input(conf) do |pipeline, queue|
134
+ 2.times.collect { queue.pop }
135
+ end
136
+
137
+ insist { events[0]["message"] } == "hello3"
138
+ insist { events[1]["message"] } == "world3"
139
+
140
+ File.open(tmpfile_path, "a") do |fd|
141
+ fd.puts("foo")
142
+ fd.puts("bar")
143
+ fd.puts("baz")
144
+ end
145
+
146
+ events = input(conf) do |pipeline, queue|
147
+ 3.times.collect { queue.pop }
148
+ end
149
+
150
+ insist { events[0]["message"] } == "foo"
151
+ insist { events[1]["message"] } == "bar"
152
+ insist { events[2]["message"] } == "baz"
153
+ end
154
+
155
+ it "should not overwrite existing path and host fields" do
156
+ tmpfile_path = Stud::Temporary.pathname
157
+ sincedb_path = Stud::Temporary.pathname
158
+
159
+ conf = <<-CONFIG
160
+ input {
161
+ bro {
162
+ type => "blah"
163
+ path => "#{tmpfile_path}"
164
+ start_position => "beginning"
165
+ sincedb_path => "#{sincedb_path}"
166
+ delimiter => "#{FILE_DELIMITER}"
167
+ codec => "json"
168
+ }
169
+ }
170
+ CONFIG
171
+
172
+ File.open(tmpfile_path, "w") do |fd|
173
+ fd.puts('{"path": "my_path", "host": "my_host"}')
174
+ fd.puts('{"my_field": "my_val"}')
175
+ end
176
+
177
+ events = input(conf) do |pipeline, queue|
178
+ 2.times.collect { queue.pop }
179
+ end
180
+
181
+ insist { events[0]["path"] } == "my_path"
182
+ insist { events[0]["host"] } == "my_host"
183
+
184
+ insist { events[1]["path"] } == "#{tmpfile_path}"
185
+ insist { events[1]["host"] } == "#{Socket.gethostname.force_encoding(Encoding::UTF_8)}"
186
+ end
187
+
188
+ context "when sincedb_path is an existing directory" do
189
+ let(:tmpfile_path) { Stud::Temporary.pathname }
190
+ let(:sincedb_path) { Stud::Temporary.directory }
191
+ subject { LogStash::Inputs::File.new("path" => tmpfile_path, "sincedb_path" => sincedb_path) }
192
+
193
+ after :each do
194
+ FileUtils.rm_rf(sincedb_path)
195
+ end
196
+
197
+ it "should raise exception" do
198
+ expect { subject.register }.to raise_error(ArgumentError)
199
+ end
200
+ end
201
+
202
+ context "when #run is called multiple times", :unix => true do
203
+ let(:tmpdir_path) { Stud::Temporary.directory }
204
+ let(:sincedb_path) { Stud::Temporary.pathname }
205
+ let(:file_path) { "#{tmpdir_path}/a.log" }
206
+ let(:buffer) { [] }
207
+ let(:lsof) { [] }
208
+ let(:stop_proc) do
209
+ lambda do |input, arr|
210
+ Thread.new(input, arr) do |i, a|
211
+ sleep 0.5
212
+ a << `lsof -p #{Process.pid} | grep "a.log"`
213
+ i.stop
214
+ end
215
+ end
216
+ end
217
+
218
+ subject { LogStash::Inputs::File.new("path" => tmpdir_path + "/*.log", "start_position" => "beginning", "sincedb_path" => sincedb_path) }
219
+
220
+ after :each do
221
+ FileUtils.rm_rf(tmpdir_path)
222
+ FileUtils.rm_rf(sincedb_path)
223
+ end
224
+ before do
225
+ File.open(file_path, "w") do |fd|
226
+ fd.puts('foo')
227
+ fd.puts('bar')
228
+ end
229
+ end
230
+ it "should only have one set of files open" do
231
+ subject.register
232
+ lsof_before = `lsof -p #{Process.pid} | grep #{file_path}`
233
+ expect(lsof_before).to eq("")
234
+ stop_proc.call(subject, lsof)
235
+ subject.run(buffer)
236
+ expect(lsof.first).not_to eq("")
237
+ stop_proc.call(subject, lsof)
238
+ subject.run(buffer)
239
+ expect(lsof.last).to eq(lsof.first)
240
+ end
241
+ end
242
+
243
+ context "when wildcard path and a multiline codec is specified" do
244
+ let(:tmpdir_path) { Stud::Temporary.directory }
245
+ let(:sincedb_path) { Stud::Temporary.pathname }
246
+ let(:conf) do
247
+ <<-CONFIG
248
+ input {
249
+ bro {
250
+ type => "blah"
251
+ path => "#{tmpdir_path}/*.log"
252
+ start_position => "beginning"
253
+ sincedb_path => "#{sincedb_path}"
254
+ delimiter => "#{FILE_DELIMITER}"
255
+ codec => multiline { pattern => "^\s" what => previous }
256
+ }
257
+ }
258
+ CONFIG
259
+ end
260
+
261
+ let(:writer_proc) do
262
+ -> do
263
+ File.open("#{tmpdir_path}/a.log", "a") do |fd|
264
+ fd.puts("line1.1-of-a")
265
+ fd.puts(" line1.2-of-a")
266
+ fd.puts(" line1.3-of-a")
267
+ fd.puts("line2.1-of-a")
268
+ end
269
+
270
+ File.open("#{tmpdir_path}/z.log", "a") do |fd|
271
+ fd.puts("line1.1-of-z")
272
+ fd.puts(" line1.2-of-z")
273
+ fd.puts(" line1.3-of-z")
274
+ fd.puts("line2.1-of-z")
275
+ end
276
+ end
277
+ end
278
+
279
+ after do
280
+ FileUtils.rm_rf(tmpdir_path)
281
+ end
282
+
283
+ let(:event_count) { 2 }
284
+
285
+ it "collects separate multiple line events from each file" do
286
+ writer_proc.call
287
+
288
+ events = input(conf) do |pipeline, queue|
289
+ queue.size.times.collect { queue.pop }
290
+ end
291
+
292
+ expect(events.size).to eq(event_count)
293
+
294
+ e1_message = events[0]["message"]
295
+ e2_message = events[1]["message"]
296
+
297
+ # can't assume File A will be read first
298
+ if e1_message.start_with?('line1.1-of-z')
299
+ expect(e1_message).to eq("line1.1-of-z#{FILE_DELIMITER} line1.2-of-z#{FILE_DELIMITER} line1.3-of-z")
300
+ expect(e2_message).to eq("line1.1-of-a#{FILE_DELIMITER} line1.2-of-a#{FILE_DELIMITER} line1.3-of-a")
301
+ else
302
+ expect(e1_message).to eq("line1.1-of-a#{FILE_DELIMITER} line1.2-of-a#{FILE_DELIMITER} line1.3-of-a")
303
+ expect(e2_message).to eq("line1.1-of-z#{FILE_DELIMITER} line1.2-of-z#{FILE_DELIMITER} line1.3-of-z")
304
+ end
305
+ end
306
+ end
307
+ end
metadata ADDED
@@ -0,0 +1,179 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logstash-input-bro
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Blake Mackey
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - '>='
17
+ - !ruby/object:Gem::Version
18
+ version: 2.0.0.beta2
19
+ - - <
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.0
22
+ name: logstash-core
23
+ prerelease: false
24
+ type: :runtime
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 2.0.0.beta2
30
+ - - <
31
+ - !ruby/object:Gem::Version
32
+ version: 3.0.0
33
+ - !ruby/object:Gem::Dependency
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ name: logstash-codec-plain
40
+ prerelease: false
41
+ type: :runtime
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ name: addressable
54
+ prerelease: false
55
+ type: :runtime
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - '>='
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ - !ruby/object:Gem::Dependency
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - '>='
65
+ - !ruby/object:Gem::Version
66
+ version: 0.6.7
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '0.6'
70
+ name: filewatch
71
+ prerelease: false
72
+ type: :runtime
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - '>='
76
+ - !ruby/object:Gem::Version
77
+ version: 0.6.7
78
+ - - ~>
79
+ - !ruby/object:Gem::Version
80
+ version: '0.6'
81
+ - !ruby/object:Gem::Dependency
82
+ requirement: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ~>
85
+ - !ruby/object:Gem::Version
86
+ version: 2.0.3
87
+ name: logstash-codec-multiline
88
+ prerelease: false
89
+ type: :runtime
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ~>
93
+ - !ruby/object:Gem::Version
94
+ version: 2.0.3
95
+ - !ruby/object:Gem::Dependency
96
+ requirement: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ~>
99
+ - !ruby/object:Gem::Version
100
+ version: 0.0.19
101
+ name: stud
102
+ prerelease: false
103
+ type: :development
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ~>
107
+ - !ruby/object:Gem::Version
108
+ version: 0.0.19
109
+ - !ruby/object:Gem::Dependency
110
+ requirement: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - '>='
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ name: logstash-devutils
116
+ prerelease: false
117
+ type: :development
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - '>='
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ - !ruby/object:Gem::Dependency
124
+ requirement: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - '>='
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ name: logstash-codec-json
130
+ prerelease: false
131
+ type: :development
132
+ version_requirements: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - '>='
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ description: This gem is a logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/plugin install gemname. This gem is not a stand-alone program
138
+ email: blake_mackey@hotmail.com
139
+ executables: []
140
+ extensions: []
141
+ extra_rdoc_files: []
142
+ files:
143
+ - CHANGELOG.md
144
+ - CONTRIBUTORS
145
+ - Gemfile
146
+ - LICENSE
147
+ - NOTICE.TXT
148
+ - README.md
149
+ - lib/logstash/inputs/bro.rb
150
+ - logstash-input-bro.gemspec
151
+ - spec/inputs/bro_spec.rb
152
+ homepage: http://github.com/brashendeavours/logstash-input-bro
153
+ licenses:
154
+ - Apache License (2.0)
155
+ metadata:
156
+ logstash_plugin: 'true'
157
+ logstash_group: input
158
+ post_install_message:
159
+ rdoc_options: []
160
+ require_paths:
161
+ - lib
162
+ required_ruby_version: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - '>='
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ required_rubygems_version: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - '>='
170
+ - !ruby/object:Gem::Version
171
+ version: '0'
172
+ requirements: []
173
+ rubyforge_project:
174
+ rubygems_version: 2.4.5
175
+ signing_key:
176
+ specification_version: 4
177
+ summary: Stream events from bro formatted files.
178
+ test_files:
179
+ - spec/inputs/bro_spec.rb