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 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