logstash-input-file 2.0.3 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5377f5857709a60359d657a8007d6ab0b3f87f4d
4
- data.tar.gz: 1af3cfea2b4135413c3cb347e61b7c0ff89985cc
3
+ metadata.gz: 5447c5eea15c163ea16990bd7362f52081d665bb
4
+ data.tar.gz: 0768e19af5a4694fbe8cad1d143d03db42321099
5
5
  SHA512:
6
- metadata.gz: 2bd62efe503f31050170ebc8f17455f8c108bb0e0430a7ee1718250831292a90bf218bec728a418aa0cb3644cd587f50dadb35ce51ed6b5256e353bfe90d143b
7
- data.tar.gz: 69ecdc00558c2b9da3a1d7e989e1a4936e0c24ab4a7f1c420bc64ba2dbd460f6751014821f71e25263b57dbd8e4598e637688233eee9233587ac73ceb58d776c
6
+ metadata.gz: 105bfd207188e1625d888fee6515ad25f1d8c9abdc3309f5f70ddfb431c989611d3ca60e18d9b39feff39578312cba74e0ab69ba40e26c4e96a9d657713ea448
7
+ data.tar.gz: 080fd1a1aca5a2c199d0d3c0a86096cbce28dc60e7be7be83bb2612838d0d0c6c3137bc22c7f8397a9749638e368ef2dc82f56a2c7672bfeddc576e0bf81bdef
@@ -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
@@ -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
- :logger => @logger,
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
- def run(queue)
190
- @tail = FileWatch::Tail.new(@tail_config)
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
- @tail.subscribe do |path, line|
194
- log_line_received(path, line)
195
- @codec.decode(line, path) do |event|
196
- # path is the identity
197
- # Note: this block is cached in the
198
- # identity_map_codec for use when
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
- @tail.quit if @tail # _sincedb_write is called implicitly
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
@@ -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.3'
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.beta2", "< 3.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.6.5', '~> 0.6']
28
- s.add_runtime_dependency 'logstash-codec-multiline', ['~> 2.0.3']
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'
@@ -1,256 +1,394 @@
1
1
  # encoding: utf-8
2
2
 
3
- require "logstash/devutils/rspec/spec_helper"
3
+ require "logstash/inputs/file"
4
+ require_relative "../spec_helper"
4
5
  require "tempfile"
5
6
  require "stud/temporary"
6
- require "logstash/inputs/file"
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
- it_behaves_like "an interruptible input plugin" do
13
- let(:config) do
14
- {
15
- "path" => Stud::Temporary.pathname,
16
- "sincedb_path" => Stud::Temporary.pathname
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
- it "should starts at the end of an existing file" do
22
- tmpfile_path = Stud::Temporary.pathname
23
- sincedb_path = Stud::Temporary.pathname
24
-
25
- conf = <<-CONFIG
26
- input {
27
- file {
28
- type => "blah"
29
- path => "#{tmpfile_path}"
30
- sincedb_path => "#{sincedb_path}"
31
- delimiter => "#{FILE_DELIMITER}"
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
- File.open(tmpfile_path, "w") do |fd|
37
- fd.puts("ignore me 1")
38
- fd.puts("ignore me 2")
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
- events = input(conf) do |pipeline, queue|
52
+ it "should restarts at the sincedb value" do
53
+ tmpfile_path = Stud::Temporary.pathname
54
+ sincedb_path = Stud::Temporary.pathname
42
55
 
43
- # at this point the plugins
44
- # threads might still be initializing so we cannot know when the
45
- # file plugin will have seen the original file, it could see it
46
- # after the first(s) hello world appends below, hence the
47
- # retry logic.
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
- events = []
68
+ File.open(tmpfile_path, "w") do |fd|
69
+ fd.puts("hello3")
70
+ fd.puts("world3")
71
+ end
50
72
 
51
- retries = 0
52
- while retries < 20
53
- File.open(tmpfile_path, "a") do |fd|
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
- if queue.size >= 2
59
- events = 2.times.collect { queue.pop }
60
- break
61
- end
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
- sleep(0.1)
64
- retries += 1
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
- insist { events[0]["message"] } == "hello"
71
- insist { events[1]["message"] } == "world"
72
- end
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
- it "should start at the beginning of an existing file" do
75
- tmpfile_path = Stud::Temporary.pathname
76
- sincedb_path = Stud::Temporary.pathname
77
-
78
- conf = <<-CONFIG
79
- input {
80
- file {
81
- type => "blah"
82
- path => "#{tmpfile_path}"
83
- start_position => "beginning"
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
- events = input(conf) do |pipeline, queue|
96
- 2.times.collect { queue.pop }
97
- end
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
- insist { events[0]["message"] } == "hello"
100
- insist { events[1]["message"] } == "world"
101
- end
119
+ events = input(conf) do |pipeline, queue|
120
+ 2.times.collect { queue.pop }
121
+ end
102
122
 
103
- it "should restarts at the sincedb value" do
104
- tmpfile_path = Stud::Temporary.pathname
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
- File.open(tmpfile_path, "w") do |fd|
120
- fd.puts("hello3")
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
- events = input(conf) do |pipeline, queue|
125
- 2.times.collect { queue.pop }
126
- end
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
- insist { events[0]["message"] } == "hello3"
129
- insist { events[1]["message"] } == "world3"
135
+ after :each do
136
+ FileUtils.rm_rf(sincedb_path)
137
+ end
130
138
 
131
- File.open(tmpfile_path, "a") do |fd|
132
- fd.puts("foo")
133
- fd.puts("bar")
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
- events = input(conf) do |pipeline, queue|
138
- 3.times.collect { queue.pop }
139
- end
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
- insist { events[0]["message"] } == "foo"
142
- insist { events[1]["message"] } == "bar"
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
- it "should not overwrite existing path and host fields" do
147
- tmpfile_path = Stud::Temporary.pathname
148
- sincedb_path = Stud::Temporary.pathname
149
-
150
- conf = <<-CONFIG
151
- input {
152
- file {
153
- type => "blah"
154
- path => "#{tmpfile_path}"
155
- start_position => "beginning"
156
- sincedb_path => "#{sincedb_path}"
157
- delimiter => "#{FILE_DELIMITER}"
158
- codec => "json"
159
- }
160
- }
161
- CONFIG
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
- File.open(tmpfile_path, "w") do |fd|
164
- fd.puts('{"path": "my_path", "host": "my_host"}')
165
- fd.puts('{"my_field": "my_val"}')
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
- events = input(conf) do |pipeline, queue|
169
- 2.times.collect { queue.pop }
170
- end
202
+ context "when close_older config is specified" do
203
+ let(:line) { "line1.1-of-a" }
171
204
 
172
- insist { events[0]["path"] } == "my_path"
173
- insist { events[0]["host"] } == "my_host"
205
+ subject { described_class.new(conf) }
174
206
 
175
- insist { events[1]["path"] } == "#{tmpfile_path}"
176
- insist { events[1]["host"] } == "#{Socket.gethostname.force_encoding(Encoding::UTF_8)}"
177
- end
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
- context "when sincedb_path is an existing directory" do
180
- let(:tmpfile_path) { Stud::Temporary.pathname }
181
- let(:sincedb_path) { Stud::Temporary.directory }
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
- after :each do
185
- FileUtils.rm_rf(sincedb_path)
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
- it "should raise exception" do
189
- expect { subject.register }.to raise_error(ArgumentError)
190
- end
191
- end
236
+ context "when ignore_older config is specified" do
237
+ let(:line) { "line1.1-of-a" }
192
238
 
193
- context "when wildcard path and a multiline codec is specified" do
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
- let(:writer_proc) do
212
- -> do
241
+ before do
213
242
  File.open("#{tmpdir_path}/a.log", "a") do |fd|
214
- fd.puts("line1.1-of-a")
215
- fd.puts(" line1.2-of-a")
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
- File.open("#{tmpdir_path}/z.log", "a") do |fd|
221
- fd.puts("line1.1-of-z")
222
- fd.puts(" line1.2-of-z")
223
- fd.puts(" line1.3-of-z")
224
- fd.puts("line2.1-of-z")
225
- end
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
- after do
230
- FileUtils.rm_rf(tmpdir_path)
231
- end
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
- let(:event_count) { 2 }
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
- it "collects separate multiple line events from each file" do
236
- writer_proc.call
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
- events = input(conf) do |pipeline, queue|
239
- queue.size.times.collect { queue.pop }
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
- expect(events.size).to eq(event_count)
366
+ subject { described_class.new(conf) }
243
367
 
244
- e1_message = events[0]["message"]
245
- e2_message = events[1]["message"]
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
- # can't assume File A will be read first
248
- if e1_message.start_with?('line1.1-of-z')
249
- expect(e1_message).to eq("line1.1-of-z#{FILE_DELIMITER} line1.2-of-z#{FILE_DELIMITER} line1.3-of-z")
250
- expect(e2_message).to eq("line1.1-of-a#{FILE_DELIMITER} line1.2-of-a#{FILE_DELIMITER} line1.3-of-a")
251
- else
252
- expect(e1_message).to eq("line1.1-of-a#{FILE_DELIMITER} line1.2-of-a#{FILE_DELIMITER} line1.3-of-a")
253
- expect(e2_message).to eq("line1.1-of-z#{FILE_DELIMITER} line1.2-of-z#{FILE_DELIMITER} line1.3-of-z")
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
@@ -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.3
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-17 00:00:00.000000000 Z
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.beta2
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.beta2
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.6.5
67
+ version: 0.7.0
68
68
  - - ~>
69
69
  - !ruby/object:Gem::Version
70
- version: '0.6'
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.6.5
75
+ version: 0.7.0
76
76
  - - ~>
77
77
  - !ruby/object:Gem::Version
78
- version: '0.6'
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.3
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.3
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.5
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