logstash-input-bro 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +4 -0
- data/CONTRIBUTORS +5 -0
- data/Gemfile +2 -0
- data/LICENSE +13 -0
- data/NOTICE.TXT +5 -0
- data/README.md +86 -0
- data/lib/logstash/inputs/bro.rb +340 -0
- data/logstash-input-bro.gemspec +34 -0
- data/spec/inputs/bro_spec.rb +307 -0
- metadata +179 -0
checksums.yaml
ADDED
@@ -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
|
data/CHANGELOG.md
ADDED
data/CONTRIBUTORS
ADDED
data/Gemfile
ADDED
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.
|
data/NOTICE.TXT
ADDED
data/README.md
ADDED
@@ -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
|