logstash-input-file 2.0.3 → 2.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 +4 -4
- data/CHANGELOG.md +6 -0
- data/Gemfile +1 -1
- data/lib/logstash/inputs/file.rb +100 -26
- data/logstash-input-file.gemspec +4 -4
- data/spec/inputs/file_spec.rb +330 -192
- data/spec/spec_helper.rb +79 -0
- metadata +15 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5447c5eea15c163ea16990bd7362f52081d665bb
|
4
|
+
data.tar.gz: 0768e19af5a4694fbe8cad1d143d03db42321099
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 105bfd207188e1625d888fee6515ad25f1d8c9abdc3309f5f70ddfb431c989611d3ca60e18d9b39feff39578312cba74e0ab69ba40e26c4e96a9d657713ea448
|
7
|
+
data.tar.gz: 080fd1a1aca5a2c199d0d3c0a86096cbce28dc60e7be7be83bb2612838d0d0c6c3137bc22c7f8397a9749638e368ef2dc82f56a2c7672bfeddc576e0bf81bdef
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
## 2.1.0
|
2
|
+
- Implement new config options: ignore_older and close_older. When close_older is set, any buffered data will be flushed.
|
3
|
+
- Fixes [#81](https://github.com/logstash-plugins/logstash-input-file/issues/81)
|
4
|
+
- Fixes [#81](https://github.com/logstash-plugins/logstash-input-file/issues/89)
|
5
|
+
- Fixes [#81](https://github.com/logstash-plugins/logstash-input-file/issues/90)
|
6
|
+
|
1
7
|
## 2.0.3
|
2
8
|
- Implement Stream Identity mapping of codecs: distinct codecs will collect input per stream identity (filename)
|
3
9
|
|
data/Gemfile
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
|
-
gemspec
|
2
|
+
gemspec
|
data/lib/logstash/inputs/file.rb
CHANGED
@@ -62,15 +62,29 @@ require "socket" # for Socket.gethostname
|
|
62
62
|
# to the rotation and its reopening under the new name (an interval
|
63
63
|
# determined by the `stat_interval` and `discover_interval` options)
|
64
64
|
# will not get picked up.
|
65
|
+
|
66
|
+
class LogStash::Codecs::Base
|
67
|
+
# TODO - move this to core
|
68
|
+
if !method_defined?(:accept)
|
69
|
+
def accept(listener)
|
70
|
+
decode(listener.data) do |event|
|
71
|
+
listener.process_event(event)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
if !method_defined?(:auto_flush)
|
76
|
+
def auto_flush
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
65
81
|
class LogStash::Inputs::File < LogStash::Inputs::Base
|
66
82
|
config_name "file"
|
67
83
|
|
68
|
-
# TODO(sissel): This should switch to use the `line` codec by default
|
69
|
-
# once file following
|
70
|
-
default :codec, "plain"
|
71
|
-
|
72
84
|
# The path(s) to the file(s) to use as an input.
|
73
85
|
# You can use filename patterns here, such as `/var/log/*.log`.
|
86
|
+
# If you use a pattern like `/var/log/**/*.log`, a recursive search
|
87
|
+
# of `/var/log` will be done for all `*.log` files.
|
74
88
|
# Paths must be absolute and cannot be relative.
|
75
89
|
#
|
76
90
|
# You may also configure multiple paths. See an example
|
@@ -121,6 +135,17 @@ class LogStash::Inputs::File < LogStash::Inputs::Base
|
|
121
135
|
# set the new line delimiter, defaults to "\n"
|
122
136
|
config :delimiter, :validate => :string, :default => "\n"
|
123
137
|
|
138
|
+
# If this option is specified, when the file input discovers a file that
|
139
|
+
# was last modified before the specified timespan in seconds, the file is
|
140
|
+
# ignored. After it's discovery, if an ignored file is modified it is no
|
141
|
+
# longer ignored and any new data is read. The default is 24 hours.
|
142
|
+
config :ignore_older, :validate => :number, :default => 24 * 60 * 60
|
143
|
+
|
144
|
+
# If this option is specified, the file input closes any files that remain
|
145
|
+
# unmodified for longer than the specified timespan in seconds.
|
146
|
+
# The default is 1 hour
|
147
|
+
config :close_older, :validate => :number, :default => 1 * 60 * 60
|
148
|
+
|
124
149
|
public
|
125
150
|
def register
|
126
151
|
require "addressable/uri"
|
@@ -135,7 +160,8 @@ class LogStash::Inputs::File < LogStash::Inputs::Base
|
|
135
160
|
:discover_interval => @discover_interval,
|
136
161
|
:sincedb_write_interval => @sincedb_write_interval,
|
137
162
|
:delimiter => @delimiter,
|
138
|
-
:
|
163
|
+
:ignore_older => @ignore_older,
|
164
|
+
:close_older => @close_older
|
139
165
|
}
|
140
166
|
|
141
167
|
@path.each do |path|
|
@@ -186,36 +212,84 @@ class LogStash::Inputs::File < LogStash::Inputs::Base
|
|
186
212
|
@codec = LogStash::Codecs::IdentityMapCodec.new(@codec)
|
187
213
|
end # def register
|
188
214
|
|
189
|
-
|
190
|
-
|
215
|
+
class ListenerTail
|
216
|
+
# use attr_reader to define noop methods
|
217
|
+
attr_reader :input, :path, :data
|
218
|
+
attr_reader :deleted, :created, :error, :eof
|
219
|
+
|
220
|
+
# construct with upstream state
|
221
|
+
def initialize(path, input)
|
222
|
+
@path, @input = path, input
|
223
|
+
end
|
224
|
+
|
225
|
+
def timed_out
|
226
|
+
input.codec.evict(path)
|
227
|
+
end
|
228
|
+
|
229
|
+
def accept(data)
|
230
|
+
# and push transient data filled dup listener downstream
|
231
|
+
input.log_line_received(path, data)
|
232
|
+
input.codec.accept(dup_adding_state(data))
|
233
|
+
end
|
234
|
+
|
235
|
+
def process_event(event)
|
236
|
+
event["[@metadata][path]"] = path
|
237
|
+
event["path"] = path if !event.include?("path")
|
238
|
+
input.post_process_this(event)
|
239
|
+
end
|
240
|
+
|
241
|
+
def add_state(data)
|
242
|
+
@data = data
|
243
|
+
self
|
244
|
+
end
|
245
|
+
|
246
|
+
private
|
247
|
+
|
248
|
+
# duplicate and add state for downstream
|
249
|
+
def dup_adding_state(line)
|
250
|
+
self.class.new(path, input).add_state(line)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def listener_for(path)
|
255
|
+
# path is the identity
|
256
|
+
ListenerTail.new(path, self)
|
257
|
+
end
|
258
|
+
|
259
|
+
def begin_tailing
|
260
|
+
# if the pipeline restarts this input,
|
261
|
+
# make sure previous files are closed
|
262
|
+
stop
|
263
|
+
# use observer listener api
|
264
|
+
@tail = FileWatch::Tail.new_observing(@tail_config)
|
191
265
|
@tail.logger = @logger
|
192
266
|
@path.each { |path| @tail.tail(path) }
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
# buffered lines are flushed.
|
200
|
-
queue << add_path_meta(event, path)
|
201
|
-
end
|
202
|
-
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def run(queue)
|
270
|
+
begin_tailing
|
271
|
+
@queue = queue
|
272
|
+
@tail.subscribe(self)
|
203
273
|
end # def run
|
204
274
|
|
275
|
+
def post_process_this(event)
|
276
|
+
event["host"] = @host if !event.include?("host")
|
277
|
+
decorate(event)
|
278
|
+
@queue << event
|
279
|
+
end
|
280
|
+
|
205
281
|
def log_line_received(path, line)
|
206
282
|
return if !@logger.debug?
|
207
283
|
@logger.debug("Received line", :path => path, :text => line)
|
208
284
|
end
|
209
285
|
|
210
|
-
def add_path_meta(event, path)
|
211
|
-
event["[@metadata][path]"] = path
|
212
|
-
event["host"] = @host if !event.include?("host")
|
213
|
-
event["path"] = path if !event.include?("path")
|
214
|
-
decorate(event)
|
215
|
-
event
|
216
|
-
end
|
217
|
-
|
218
286
|
def stop
|
219
|
-
|
287
|
+
# in filewatch >= 0.6.7, quit will closes and forget all files
|
288
|
+
# but it will write their last read positions to since_db
|
289
|
+
# beforehand
|
290
|
+
if @tail
|
291
|
+
@codec.close
|
292
|
+
@tail.quit
|
293
|
+
end
|
220
294
|
end
|
221
295
|
end # class LogStash::Inputs::File
|
data/logstash-input-file.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
|
3
3
|
s.name = 'logstash-input-file'
|
4
|
-
s.version = '2.0
|
4
|
+
s.version = '2.1.0'
|
5
5
|
s.licenses = ['Apache License (2.0)']
|
6
6
|
s.summary = "Stream events from files."
|
7
7
|
s.description = "This gem is a logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/plugin install gemname. This gem is not a stand-alone program"
|
@@ -20,12 +20,12 @@ Gem::Specification.new do |s|
|
|
20
20
|
s.metadata = { "logstash_plugin" => "true", "logstash_group" => "input" }
|
21
21
|
|
22
22
|
# Gem dependencies
|
23
|
-
s.add_runtime_dependency "logstash-core", ">= 2.0.0
|
23
|
+
s.add_runtime_dependency "logstash-core", ">= 2.0.0", "< 3.0.0.alpha0"
|
24
24
|
|
25
25
|
s.add_runtime_dependency 'logstash-codec-plain'
|
26
26
|
s.add_runtime_dependency 'addressable'
|
27
|
-
s.add_runtime_dependency 'filewatch', ['>= 0.
|
28
|
-
s.add_runtime_dependency 'logstash-codec-multiline', ['~> 2.0.
|
27
|
+
s.add_runtime_dependency 'filewatch', ['>= 0.7.0', '~> 0.7']
|
28
|
+
s.add_runtime_dependency 'logstash-codec-multiline', ['~> 2.0.5']
|
29
29
|
|
30
30
|
s.add_development_dependency 'stud', ['~> 0.0.19']
|
31
31
|
s.add_development_dependency 'logstash-devutils'
|
data/spec/inputs/file_spec.rb
CHANGED
@@ -1,256 +1,394 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
require "logstash/
|
3
|
+
require "logstash/inputs/file"
|
4
|
+
require_relative "../spec_helper"
|
4
5
|
require "tempfile"
|
5
6
|
require "stud/temporary"
|
6
|
-
require "logstash/
|
7
|
+
require "logstash/codecs/multiline"
|
7
8
|
|
8
9
|
FILE_DELIMITER = LogStash::Environment.windows? ? "\r\n" : "\n"
|
9
10
|
|
10
11
|
describe LogStash::Inputs::File do
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
12
|
+
describe "testing with input(conf) do |pipeline, queue|" do
|
13
|
+
it_behaves_like "an interruptible input plugin" do
|
14
|
+
let(:config) do
|
15
|
+
{
|
16
|
+
"path" => Stud::Temporary.pathname,
|
17
|
+
"sincedb_path" => Stud::Temporary.pathname
|
18
|
+
}
|
19
|
+
end
|
18
20
|
end
|
19
|
-
end
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
22
|
+
it "should start at the beginning of an existing file" do
|
23
|
+
tmpfile_path = Stud::Temporary.pathname
|
24
|
+
sincedb_path = Stud::Temporary.pathname
|
25
|
+
|
26
|
+
conf = <<-CONFIG
|
27
|
+
input {
|
28
|
+
file {
|
29
|
+
type => "blah"
|
30
|
+
path => "#{tmpfile_path}"
|
31
|
+
start_position => "beginning"
|
32
|
+
sincedb_path => "#{sincedb_path}"
|
33
|
+
delimiter => "#{FILE_DELIMITER}"
|
34
|
+
}
|
32
35
|
}
|
33
|
-
|
34
|
-
CONFIG
|
36
|
+
CONFIG
|
35
37
|
|
36
|
-
|
37
|
-
|
38
|
-
|
38
|
+
File.open(tmpfile_path, "a") do |fd|
|
39
|
+
fd.puts("hello")
|
40
|
+
fd.puts("world")
|
41
|
+
fd.fsync
|
42
|
+
end
|
43
|
+
|
44
|
+
events = input(conf) do |pipeline, queue|
|
45
|
+
2.times.collect { queue.pop }
|
46
|
+
end
|
47
|
+
|
48
|
+
insist { events[0]["message"] } == "hello"
|
49
|
+
insist { events[1]["message"] } == "world"
|
39
50
|
end
|
40
51
|
|
41
|
-
|
52
|
+
it "should restarts at the sincedb value" do
|
53
|
+
tmpfile_path = Stud::Temporary.pathname
|
54
|
+
sincedb_path = Stud::Temporary.pathname
|
42
55
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
56
|
+
conf = <<-CONFIG
|
57
|
+
input {
|
58
|
+
file {
|
59
|
+
type => "blah"
|
60
|
+
path => "#{tmpfile_path}"
|
61
|
+
start_position => "beginning"
|
62
|
+
sincedb_path => "#{sincedb_path}"
|
63
|
+
delimiter => "#{FILE_DELIMITER}"
|
64
|
+
}
|
65
|
+
}
|
66
|
+
CONFIG
|
48
67
|
|
49
|
-
|
68
|
+
File.open(tmpfile_path, "w") do |fd|
|
69
|
+
fd.puts("hello3")
|
70
|
+
fd.puts("world3")
|
71
|
+
end
|
50
72
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
fd.puts("hello")
|
55
|
-
fd.puts("world")
|
56
|
-
end
|
73
|
+
events = input(conf) do |pipeline, queue|
|
74
|
+
2.times.collect { queue.pop }
|
75
|
+
end
|
57
76
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
77
|
+
insist { events[0]["message"] } == "hello3"
|
78
|
+
insist { events[1]["message"] } == "world3"
|
79
|
+
|
80
|
+
File.open(tmpfile_path, "a") do |fd|
|
81
|
+
fd.puts("foo")
|
82
|
+
fd.puts("bar")
|
83
|
+
fd.puts("baz")
|
84
|
+
fd.fsync
|
85
|
+
end
|
62
86
|
|
63
|
-
|
64
|
-
|
87
|
+
events = input(conf) do |pipeline, queue|
|
88
|
+
3.times.collect { queue.pop }
|
65
89
|
end
|
66
90
|
|
67
|
-
events
|
91
|
+
insist { events[0]["message"] } == "foo"
|
92
|
+
insist { events[1]["message"] } == "bar"
|
93
|
+
insist { events[2]["message"] } == "baz"
|
68
94
|
end
|
69
95
|
|
70
|
-
|
71
|
-
|
72
|
-
|
96
|
+
it "should not overwrite existing path and host fields" do
|
97
|
+
tmpfile_path = Stud::Temporary.pathname
|
98
|
+
sincedb_path = Stud::Temporary.pathname
|
73
99
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
sincedb_path => "#{sincedb_path}"
|
85
|
-
delimiter => "#{FILE_DELIMITER}"
|
100
|
+
conf = <<-CONFIG
|
101
|
+
input {
|
102
|
+
file {
|
103
|
+
type => "blah"
|
104
|
+
path => "#{tmpfile_path}"
|
105
|
+
start_position => "beginning"
|
106
|
+
sincedb_path => "#{sincedb_path}"
|
107
|
+
delimiter => "#{FILE_DELIMITER}"
|
108
|
+
codec => "json"
|
109
|
+
}
|
86
110
|
}
|
87
|
-
|
88
|
-
CONFIG
|
89
|
-
|
90
|
-
File.open(tmpfile_path, "a") do |fd|
|
91
|
-
fd.puts("hello")
|
92
|
-
fd.puts("world")
|
93
|
-
end
|
111
|
+
CONFIG
|
94
112
|
|
95
|
-
|
96
|
-
|
97
|
-
|
113
|
+
File.open(tmpfile_path, "w") do |fd|
|
114
|
+
fd.puts('{"path": "my_path", "host": "my_host"}')
|
115
|
+
fd.puts('{"my_field": "my_val"}')
|
116
|
+
fd.fsync
|
117
|
+
end
|
98
118
|
|
99
|
-
|
100
|
-
|
101
|
-
|
119
|
+
events = input(conf) do |pipeline, queue|
|
120
|
+
2.times.collect { queue.pop }
|
121
|
+
end
|
102
122
|
|
103
|
-
|
104
|
-
|
105
|
-
sincedb_path = Stud::Temporary.pathname
|
106
|
-
|
107
|
-
conf = <<-CONFIG
|
108
|
-
input {
|
109
|
-
file {
|
110
|
-
type => "blah"
|
111
|
-
path => "#{tmpfile_path}"
|
112
|
-
start_position => "beginning"
|
113
|
-
sincedb_path => "#{sincedb_path}"
|
114
|
-
delimiter => "#{FILE_DELIMITER}"
|
115
|
-
}
|
116
|
-
}
|
117
|
-
CONFIG
|
123
|
+
insist { events[0]["path"] } == "my_path"
|
124
|
+
insist { events[0]["host"] } == "my_host"
|
118
125
|
|
119
|
-
|
120
|
-
|
121
|
-
fd.puts("world3")
|
126
|
+
insist { events[1]["path"] } == "#{tmpfile_path}"
|
127
|
+
insist { events[1]["host"] } == "#{Socket.gethostname.force_encoding(Encoding::UTF_8)}"
|
122
128
|
end
|
123
129
|
|
124
|
-
|
125
|
-
|
126
|
-
|
130
|
+
context "when sincedb_path is an existing directory" do
|
131
|
+
let(:tmpfile_path) { Stud::Temporary.pathname }
|
132
|
+
let(:sincedb_path) { Stud::Temporary.directory }
|
133
|
+
subject { LogStash::Inputs::File.new("path" => tmpfile_path, "sincedb_path" => sincedb_path) }
|
127
134
|
|
128
|
-
|
129
|
-
|
135
|
+
after :each do
|
136
|
+
FileUtils.rm_rf(sincedb_path)
|
137
|
+
end
|
130
138
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
fd.puts("baz")
|
139
|
+
it "should raise exception" do
|
140
|
+
expect { subject.register }.to raise_error(ArgumentError)
|
141
|
+
end
|
135
142
|
end
|
143
|
+
end
|
136
144
|
|
137
|
-
|
138
|
-
|
139
|
-
|
145
|
+
describe "testing with new, register, run and stop" do
|
146
|
+
let(:conf) { Hash.new }
|
147
|
+
let(:mlconf) { Hash.new }
|
148
|
+
let(:events) { Array.new }
|
149
|
+
let(:mlcodec) { LogStash::Codecs::Multiline.new(mlconf) }
|
150
|
+
let(:codec) { CodecTracer.new }
|
151
|
+
let(:tmpfile_path) { Stud::Temporary.pathname }
|
152
|
+
let(:sincedb_path) { Stud::Temporary.pathname }
|
153
|
+
let(:tmpdir_path) { Stud::Temporary.directory }
|
140
154
|
|
141
|
-
|
142
|
-
|
143
|
-
insist { events[2]["message"] } == "baz"
|
144
|
-
end
|
155
|
+
context "when data exists and then more data is appended" do
|
156
|
+
subject { described_class.new(conf) }
|
145
157
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
158
|
+
before do
|
159
|
+
File.open(tmpfile_path, "w") do |fd|
|
160
|
+
fd.puts("ignore me 1")
|
161
|
+
fd.puts("ignore me 2")
|
162
|
+
fd.fsync
|
163
|
+
end
|
164
|
+
mlconf.update("pattern" => "^\s", "what" => "previous")
|
165
|
+
conf.update("type" => "blah",
|
166
|
+
"path" => tmpfile_path,
|
167
|
+
"sincedb_path" => sincedb_path,
|
168
|
+
"stat_interval" => 0.1,
|
169
|
+
"codec" => mlcodec,
|
170
|
+
"delimiter" => FILE_DELIMITER)
|
171
|
+
subject.register
|
172
|
+
Thread.new { subject.run(events) }
|
173
|
+
end
|
162
174
|
|
163
|
-
|
164
|
-
|
165
|
-
|
175
|
+
it "reads the appended data only" do
|
176
|
+
sleep 0.1
|
177
|
+
File.open(tmpfile_path, "a") do |fd|
|
178
|
+
fd.puts("hello")
|
179
|
+
fd.puts("world")
|
180
|
+
fd.fsync
|
181
|
+
end
|
182
|
+
# wait for one event, the last line is buffered
|
183
|
+
expect(pause_until{ events.size == 1 }).to be_truthy
|
184
|
+
subject.stop
|
185
|
+
# stop flushes the second event
|
186
|
+
expect(pause_until{ events.size == 2 }).to be_truthy
|
187
|
+
|
188
|
+
event1 = events[0]
|
189
|
+
expect(event1).not_to be_nil
|
190
|
+
expect(event1["path"]).to eq tmpfile_path
|
191
|
+
expect(event1["@metadata"]["path"]).to eq tmpfile_path
|
192
|
+
expect(event1["message"]).to eq "hello"
|
193
|
+
|
194
|
+
event2 = events[1]
|
195
|
+
expect(event2).not_to be_nil
|
196
|
+
expect(event2["path"]).to eq tmpfile_path
|
197
|
+
expect(event2["@metadata"]["path"]).to eq tmpfile_path
|
198
|
+
expect(event2["message"]).to eq "world"
|
199
|
+
end
|
166
200
|
end
|
167
201
|
|
168
|
-
|
169
|
-
|
170
|
-
end
|
202
|
+
context "when close_older config is specified" do
|
203
|
+
let(:line) { "line1.1-of-a" }
|
171
204
|
|
172
|
-
|
173
|
-
insist { events[0]["host"] } == "my_host"
|
205
|
+
subject { described_class.new(conf) }
|
174
206
|
|
175
|
-
|
176
|
-
|
177
|
-
|
207
|
+
before do
|
208
|
+
conf.update(
|
209
|
+
"type" => "blah",
|
210
|
+
"path" => "#{tmpdir_path}/*.log",
|
211
|
+
"sincedb_path" => sincedb_path,
|
212
|
+
"stat_interval" => 0.02,
|
213
|
+
"codec" => codec,
|
214
|
+
"close_older" => 1,
|
215
|
+
"delimiter" => FILE_DELIMITER)
|
178
216
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
subject { LogStash::Inputs::File.new("path" => tmpfile_path, "sincedb_path" => sincedb_path) }
|
217
|
+
subject.register
|
218
|
+
Thread.new { subject.run(events) }
|
219
|
+
end
|
183
220
|
|
184
|
-
|
185
|
-
|
221
|
+
it "having timed_out, the identity is evicted" do
|
222
|
+
sleep 0.1
|
223
|
+
File.open("#{tmpdir_path}/a.log", "a") do |fd|
|
224
|
+
fd.puts(line)
|
225
|
+
fd.fsync
|
226
|
+
end
|
227
|
+
expect(pause_until{ subject.codec.identity_count == 1 }).to be_truthy
|
228
|
+
expect(codec).to receive_call_and_args(:accept, [true])
|
229
|
+
# wait for expiry to kick in and close files.
|
230
|
+
expect(pause_until{ subject.codec.identity_count.zero? }).to be_truthy
|
231
|
+
expect(codec).to receive_call_and_args(:auto_flush, [true])
|
232
|
+
subject.stop
|
233
|
+
end
|
186
234
|
end
|
187
235
|
|
188
|
-
|
189
|
-
|
190
|
-
end
|
191
|
-
end
|
236
|
+
context "when ignore_older config is specified" do
|
237
|
+
let(:line) { "line1.1-of-a" }
|
192
238
|
|
193
|
-
|
194
|
-
let(:tmpdir_path) { Stud::Temporary.directory }
|
195
|
-
let(:sincedb_path) { Stud::Temporary.pathname }
|
196
|
-
let(:conf) do
|
197
|
-
<<-CONFIG
|
198
|
-
input {
|
199
|
-
file {
|
200
|
-
type => "blah"
|
201
|
-
path => "#{tmpdir_path}/*.log"
|
202
|
-
start_position => "beginning"
|
203
|
-
sincedb_path => "#{sincedb_path}"
|
204
|
-
delimiter => "#{FILE_DELIMITER}"
|
205
|
-
codec => multiline { pattern => "^\s" what => previous }
|
206
|
-
}
|
207
|
-
}
|
208
|
-
CONFIG
|
209
|
-
end
|
239
|
+
subject { described_class.new(conf) }
|
210
240
|
|
211
|
-
|
212
|
-
-> do
|
241
|
+
before do
|
213
242
|
File.open("#{tmpdir_path}/a.log", "a") do |fd|
|
214
|
-
fd.puts(
|
215
|
-
fd.
|
216
|
-
fd.puts(" line1.3-of-a")
|
217
|
-
fd.puts("line2.1-of-a")
|
243
|
+
fd.puts(line)
|
244
|
+
fd.fsync
|
218
245
|
end
|
246
|
+
sleep 1.1 # wait for file to age
|
247
|
+
conf.update(
|
248
|
+
"type" => "blah",
|
249
|
+
"path" => "#{tmpdir_path}/*.log",
|
250
|
+
"sincedb_path" => sincedb_path,
|
251
|
+
"stat_interval" => 0.02,
|
252
|
+
"codec" => codec,
|
253
|
+
"ignore_older" => 1,
|
254
|
+
"delimiter" => FILE_DELIMITER)
|
255
|
+
|
256
|
+
subject.register
|
257
|
+
Thread.new { subject.run(events) }
|
258
|
+
end
|
219
259
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
260
|
+
it "the file is not read" do
|
261
|
+
sleep 0.5
|
262
|
+
subject.stop
|
263
|
+
expect(codec).to receive_call_and_args(:accept, false)
|
264
|
+
expect(codec).to receive_call_and_args(:auto_flush, false)
|
265
|
+
expect(subject.codec.identity_count).to eq(0)
|
226
266
|
end
|
227
267
|
end
|
228
268
|
|
229
|
-
|
230
|
-
|
231
|
-
|
269
|
+
context "when wildcard path and a multiline codec is specified" do
|
270
|
+
subject { described_class.new(conf) }
|
271
|
+
let(:writer_proc) do
|
272
|
+
-> do
|
273
|
+
File.open("#{tmpdir_path}/a.log", "a") do |fd|
|
274
|
+
fd.puts("line1.1-of-a")
|
275
|
+
fd.puts(" line1.2-of-a")
|
276
|
+
fd.puts(" line1.3-of-a")
|
277
|
+
fd.fsync
|
278
|
+
end
|
279
|
+
File.open("#{tmpdir_path}/z.log", "a") do |fd|
|
280
|
+
fd.puts("line1.1-of-z")
|
281
|
+
fd.puts(" line1.2-of-z")
|
282
|
+
fd.puts(" line1.3-of-z")
|
283
|
+
fd.fsync
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
232
287
|
|
233
|
-
|
288
|
+
before do
|
289
|
+
mlconf.update("pattern" => "^\s", "what" => "previous")
|
290
|
+
conf.update(
|
291
|
+
"type" => "blah",
|
292
|
+
"path" => "#{tmpdir_path}/*.log",
|
293
|
+
"sincedb_path" => sincedb_path,
|
294
|
+
"stat_interval" => 0.05,
|
295
|
+
"codec" => mlcodec,
|
296
|
+
"delimiter" => FILE_DELIMITER)
|
297
|
+
|
298
|
+
subject.register
|
299
|
+
Thread.new { subject.run(events) }
|
300
|
+
sleep 0.1
|
301
|
+
writer_proc.call
|
302
|
+
end
|
234
303
|
|
235
|
-
|
236
|
-
|
304
|
+
it "collects separate multiple line events from each file" do
|
305
|
+
# wait for both paths to be mapped as identities
|
306
|
+
expect(pause_until{ subject.codec.identity_count == 2 }).to be_truthy
|
307
|
+
subject.stop
|
308
|
+
# stop flushes both events
|
309
|
+
expect(pause_until{ events.size == 2 }).to be_truthy
|
310
|
+
|
311
|
+
e1, e2 = events
|
312
|
+
e1_message = e1["message"]
|
313
|
+
e2_message = e2["message"]
|
314
|
+
|
315
|
+
# can't assume File A will be read first
|
316
|
+
if e1_message.start_with?('line1.1-of-z')
|
317
|
+
expect(e1["path"]).to match(/z.log/)
|
318
|
+
expect(e2["path"]).to match(/a.log/)
|
319
|
+
expect(e1_message).to eq("line1.1-of-z#{FILE_DELIMITER} line1.2-of-z#{FILE_DELIMITER} line1.3-of-z")
|
320
|
+
expect(e2_message).to eq("line1.1-of-a#{FILE_DELIMITER} line1.2-of-a#{FILE_DELIMITER} line1.3-of-a")
|
321
|
+
else
|
322
|
+
expect(e1["path"]).to match(/a.log/)
|
323
|
+
expect(e2["path"]).to match(/z.log/)
|
324
|
+
expect(e1_message).to eq("line1.1-of-a#{FILE_DELIMITER} line1.2-of-a#{FILE_DELIMITER} line1.3-of-a")
|
325
|
+
expect(e2_message).to eq("line1.1-of-z#{FILE_DELIMITER} line1.2-of-z#{FILE_DELIMITER} line1.3-of-z")
|
326
|
+
end
|
327
|
+
end
|
237
328
|
|
238
|
-
|
239
|
-
|
329
|
+
context "if auto_flush is enabled on the multiline codec" do
|
330
|
+
let(:writer_proc) do
|
331
|
+
-> do
|
332
|
+
File.open("#{tmpdir_path}/a.log", "a") do |fd|
|
333
|
+
fd.puts("line1.1-of-a")
|
334
|
+
fd.puts(" line1.2-of-a")
|
335
|
+
fd.puts(" line1.3-of-a")
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
let(:mlconf) { { "auto_flush_interval" => 1 } }
|
340
|
+
|
341
|
+
it "an event is generated via auto_flush" do
|
342
|
+
# wait for auto_flush
|
343
|
+
# without it lines are buffered and pause_until would time out i.e false
|
344
|
+
expect(pause_until{ events.size == 1 }).to be_truthy
|
345
|
+
subject.stop
|
346
|
+
|
347
|
+
e1 = events.first
|
348
|
+
e1_message = e1["message"]
|
349
|
+
expect(e1["path"]).to match(/a.log/)
|
350
|
+
expect(e1_message).to eq("line1.1-of-a#{FILE_DELIMITER} line1.2-of-a#{FILE_DELIMITER} line1.3-of-a")
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
context "when #run is called multiple times", :unix => true do
|
356
|
+
let(:file_path) { "#{tmpdir_path}/a.log" }
|
357
|
+
let(:buffer) { [] }
|
358
|
+
let(:lsof) { [] }
|
359
|
+
let(:run_thread_proc) do
|
360
|
+
lambda { Thread.new { subject.run(buffer) } }
|
361
|
+
end
|
362
|
+
let(:lsof_proc) do
|
363
|
+
lambda { `lsof -p #{Process.pid} | grep #{file_path}` }
|
240
364
|
end
|
241
365
|
|
242
|
-
|
366
|
+
subject { described_class.new(conf) }
|
243
367
|
|
244
|
-
|
245
|
-
|
368
|
+
before do
|
369
|
+
conf.update(
|
370
|
+
"path" => tmpdir_path + "/*.log",
|
371
|
+
"start_position" => "beginning",
|
372
|
+
"sincedb_path" => sincedb_path)
|
373
|
+
|
374
|
+
File.open(file_path, "w") do |fd|
|
375
|
+
fd.puts('foo')
|
376
|
+
fd.puts('bar')
|
377
|
+
fd.fsync
|
378
|
+
end
|
379
|
+
end
|
246
380
|
|
247
|
-
|
248
|
-
|
249
|
-
expect(
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
expect(
|
381
|
+
it "should only have one set of files open" do
|
382
|
+
subject.register
|
383
|
+
expect(lsof_proc.call).to eq("")
|
384
|
+
run_thread_proc.call
|
385
|
+
sleep 0.1
|
386
|
+
first_lsof = lsof_proc.call
|
387
|
+
expect(first_lsof).not_to eq("")
|
388
|
+
run_thread_proc.call
|
389
|
+
sleep 0.1
|
390
|
+
second_lsof = lsof_proc.call
|
391
|
+
expect(second_lsof).to eq(first_lsof)
|
254
392
|
end
|
255
393
|
end
|
256
394
|
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "logstash/devutils/rspec/spec_helper"
|
4
|
+
|
5
|
+
class TracerBase
|
6
|
+
def initialize() @tracer = []; end
|
7
|
+
|
8
|
+
def trace_for(symbol)
|
9
|
+
params = @tracer.map {|k,v| k == symbol ? v : nil}.compact
|
10
|
+
params.empty? ? false : params
|
11
|
+
end
|
12
|
+
|
13
|
+
def clear()
|
14
|
+
@tracer.clear()
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class FileLogTracer < TracerBase
|
19
|
+
def warn(*args) @tracer.push [:warn, args]; end
|
20
|
+
def error(*args) @tracer.push [:error, args]; end
|
21
|
+
def debug(*args) @tracer.push [:debug, args]; end
|
22
|
+
def info(*args) @tracer.push [:info, args]; end
|
23
|
+
|
24
|
+
def info?() true; end
|
25
|
+
def debug?() true; end
|
26
|
+
def warn?() true; end
|
27
|
+
def error?() true; end
|
28
|
+
end
|
29
|
+
|
30
|
+
class ComponentTracer < TracerBase
|
31
|
+
def accept(*args) @tracer.push [:accept, args]; end
|
32
|
+
def deliver(*args) @tracer.push [:deliver, args]; end
|
33
|
+
end
|
34
|
+
|
35
|
+
class CodecTracer < TracerBase
|
36
|
+
def decode_accept(ctx, data, listener)
|
37
|
+
@tracer.push [:decode_accept, [ctx, data]]
|
38
|
+
listener.process(ctx, {"message" => data})
|
39
|
+
end
|
40
|
+
def accept(listener)
|
41
|
+
@tracer.push [:accept, true]
|
42
|
+
end
|
43
|
+
def auto_flush()
|
44
|
+
@tracer.push [:auto_flush, true]
|
45
|
+
end
|
46
|
+
def close
|
47
|
+
@tracer.push [:close, true]
|
48
|
+
end
|
49
|
+
def clone
|
50
|
+
self.class.new
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
module Kernel
|
55
|
+
def pause_until(nap = 5, &block)
|
56
|
+
sq = SizedQueue.new(1)
|
57
|
+
th1 = Thread.new(sq) {|q| sleep nap; q.push(false) }
|
58
|
+
th2 = Thread.new(sq) do |q|
|
59
|
+
success = false
|
60
|
+
iters = nap * 5 + 1
|
61
|
+
iters.times do
|
62
|
+
break if !!(success = block.call)
|
63
|
+
sleep(0.2)
|
64
|
+
end
|
65
|
+
q.push(success)
|
66
|
+
end
|
67
|
+
sq.pop
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
RSpec::Matchers.define(:receive_call_and_args) do |m, args|
|
72
|
+
match do |actual|
|
73
|
+
actual.trace_for(m) == args
|
74
|
+
end
|
75
|
+
|
76
|
+
failure_message do
|
77
|
+
"Expecting method #{m} to receive: #{args} but got: #{actual.trace_for(m)}"
|
78
|
+
end
|
79
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logstash-input-file
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Elastic
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-12-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: logstash-core
|
@@ -16,18 +16,18 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - '>='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 2.0.0
|
19
|
+
version: 2.0.0
|
20
20
|
- - <
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: 3.0.0
|
22
|
+
version: 3.0.0.alpha0
|
23
23
|
requirement: !ruby/object:Gem::Requirement
|
24
24
|
requirements:
|
25
25
|
- - '>='
|
26
26
|
- !ruby/object:Gem::Version
|
27
|
-
version: 2.0.0
|
27
|
+
version: 2.0.0
|
28
28
|
- - <
|
29
29
|
- !ruby/object:Gem::Version
|
30
|
-
version: 3.0.0
|
30
|
+
version: 3.0.0.alpha0
|
31
31
|
prerelease: false
|
32
32
|
type: :runtime
|
33
33
|
- !ruby/object:Gem::Dependency
|
@@ -64,18 +64,18 @@ dependencies:
|
|
64
64
|
requirements:
|
65
65
|
- - '>='
|
66
66
|
- !ruby/object:Gem::Version
|
67
|
-
version: 0.
|
67
|
+
version: 0.7.0
|
68
68
|
- - ~>
|
69
69
|
- !ruby/object:Gem::Version
|
70
|
-
version: '0.
|
70
|
+
version: '0.7'
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - '>='
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: 0.
|
75
|
+
version: 0.7.0
|
76
76
|
- - ~>
|
77
77
|
- !ruby/object:Gem::Version
|
78
|
-
version: '0.
|
78
|
+
version: '0.7'
|
79
79
|
prerelease: false
|
80
80
|
type: :runtime
|
81
81
|
- !ruby/object:Gem::Dependency
|
@@ -84,12 +84,12 @@ dependencies:
|
|
84
84
|
requirements:
|
85
85
|
- - ~>
|
86
86
|
- !ruby/object:Gem::Version
|
87
|
-
version: 2.0.
|
87
|
+
version: 2.0.5
|
88
88
|
requirement: !ruby/object:Gem::Requirement
|
89
89
|
requirements:
|
90
90
|
- - ~>
|
91
91
|
- !ruby/object:Gem::Version
|
92
|
-
version: 2.0.
|
92
|
+
version: 2.0.5
|
93
93
|
prerelease: false
|
94
94
|
type: :runtime
|
95
95
|
- !ruby/object:Gem::Dependency
|
@@ -149,6 +149,7 @@ files:
|
|
149
149
|
- lib/logstash/inputs/file.rb
|
150
150
|
- logstash-input-file.gemspec
|
151
151
|
- spec/inputs/file_spec.rb
|
152
|
+
- spec/spec_helper.rb
|
152
153
|
homepage: http://www.elastic.co/guide/en/logstash/current/index.html
|
153
154
|
licenses:
|
154
155
|
- Apache License (2.0)
|
@@ -171,9 +172,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
171
172
|
version: '0'
|
172
173
|
requirements: []
|
173
174
|
rubyforge_project:
|
174
|
-
rubygems_version: 2.4.
|
175
|
+
rubygems_version: 2.4.8
|
175
176
|
signing_key:
|
176
177
|
specification_version: 4
|
177
178
|
summary: Stream events from files.
|
178
179
|
test_files:
|
179
180
|
- spec/inputs/file_spec.rb
|
181
|
+
- spec/spec_helper.rb
|