logstash-input-file 4.1.3 → 4.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/JAR_VERSION +1 -1
  4. data/README.md +0 -3
  5. data/docs/index.asciidoc +26 -16
  6. data/lib/filewatch/bootstrap.rb +10 -21
  7. data/lib/filewatch/discoverer.rb +35 -28
  8. data/lib/filewatch/observing_base.rb +2 -1
  9. data/lib/filewatch/read_mode/handlers/base.rb +19 -6
  10. data/lib/filewatch/read_mode/handlers/read_file.rb +43 -32
  11. data/lib/filewatch/read_mode/handlers/read_zip_file.rb +8 -3
  12. data/lib/filewatch/read_mode/processor.rb +8 -8
  13. data/lib/filewatch/settings.rb +3 -3
  14. data/lib/filewatch/sincedb_collection.rb +56 -42
  15. data/lib/filewatch/sincedb_value.rb +6 -0
  16. data/lib/filewatch/stat/generic.rb +34 -0
  17. data/lib/filewatch/stat/windows_path.rb +32 -0
  18. data/lib/filewatch/tail_mode/handlers/base.rb +40 -22
  19. data/lib/filewatch/tail_mode/handlers/create.rb +1 -2
  20. data/lib/filewatch/tail_mode/handlers/create_initial.rb +2 -1
  21. data/lib/filewatch/tail_mode/handlers/delete.rb +13 -1
  22. data/lib/filewatch/tail_mode/handlers/grow.rb +5 -2
  23. data/lib/filewatch/tail_mode/handlers/shrink.rb +7 -4
  24. data/lib/filewatch/tail_mode/handlers/unignore.rb +4 -2
  25. data/lib/filewatch/tail_mode/processor.rb +147 -58
  26. data/lib/filewatch/watch.rb +15 -35
  27. data/lib/filewatch/watched_file.rb +237 -41
  28. data/lib/filewatch/watched_files_collection.rb +2 -2
  29. data/lib/filewatch/winhelper.rb +167 -25
  30. data/lib/jars/filewatch-1.0.1.jar +0 -0
  31. data/lib/logstash/inputs/file.rb +9 -2
  32. data/logstash-input-file.gemspec +9 -2
  33. data/spec/file_ext/file_ext_windows_spec.rb +36 -0
  34. data/spec/filewatch/read_mode_handlers_read_file_spec.rb +2 -2
  35. data/spec/filewatch/reading_spec.rb +100 -57
  36. data/spec/filewatch/rotate_spec.rb +451 -0
  37. data/spec/filewatch/spec_helper.rb +33 -10
  38. data/spec/filewatch/tailing_spec.rb +273 -153
  39. data/spec/filewatch/watched_file_spec.rb +3 -3
  40. data/spec/filewatch/watched_files_collection_spec.rb +3 -3
  41. data/spec/filewatch/winhelper_spec.rb +4 -5
  42. data/spec/helpers/logging_level_helper.rb +8 -0
  43. data/spec/helpers/rspec_wait_handler_helper.rb +38 -0
  44. data/spec/helpers/spec_helper.rb +7 -1
  45. data/spec/inputs/file_read_spec.rb +54 -24
  46. data/spec/inputs/file_tail_spec.rb +244 -284
  47. metadata +13 -3
  48. data/lib/jars/filewatch-1.0.0.jar +0 -0
@@ -206,7 +206,7 @@ class File < LogStash::Inputs::Base
206
206
  # 1MB from each active file. See the option `max_open_files` for more info.
207
207
  # The default set internally is very large, 4611686018427387903. By default
208
208
  # the file is read to the end before moving to the next active file.
209
- config :file_chunk_count, :validate => :number, :default => FileWatch::FIXNUM_MAX
209
+ config :file_chunk_count, :validate => :number, :default => FileWatch::MAX_ITERATIONS
210
210
 
211
211
  # Which attribute of a discovered file should be used to sort the discovered files.
212
212
  # Files can be sort by modified date or full path alphabetic.
@@ -312,8 +312,14 @@ class File < LogStash::Inputs::Base
312
312
  end
313
313
  end
314
314
  @codec = LogStash::Codecs::IdentityMapCodec.new(@codec)
315
+ @completely_stopped = Concurrent::AtomicBoolean.new
315
316
  end # def register
316
317
 
318
+ def completely_stopped?
319
+ # to synchronise after(:each) blocks in tests that remove the sincedb file before atomic_write completes
320
+ @completely_stopped.true?
321
+ end
322
+
317
323
  def listener_for(path)
318
324
  # path is the identity
319
325
  FileListener.new(path, self)
@@ -333,6 +339,7 @@ class File < LogStash::Inputs::Base
333
339
  @watcher.subscribe(self) # halts here until quit is called
334
340
  # last action of the subscribe call is to write the sincedb
335
341
  exit_flush
342
+ @completely_stopped.make_true
336
343
  end # def run
337
344
 
338
345
  def post_process_this(event)
@@ -354,7 +361,7 @@ class File < LogStash::Inputs::Base
354
361
  end
355
362
 
356
363
  def stop
357
- if @watcher
364
+ unless @watcher.nil?
358
365
  @codec.close
359
366
  @watcher.quit
360
367
  end
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
 
3
3
  s.name = 'logstash-input-file'
4
- s.version = '4.1.3'
4
+ s.version = '4.1.4'
5
5
  s.licenses = ['Apache-2.0']
6
6
  s.summary = "Streams 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/logstash-plugin install gemname. This gem is not a stand-alone program"
@@ -23,7 +23,14 @@ Gem::Specification.new do |s|
23
23
  s.add_runtime_dependency "logstash-core-plugin-api", ">= 1.60", "<= 2.99"
24
24
 
25
25
  s.add_runtime_dependency 'logstash-codec-plain'
26
- s.add_runtime_dependency 'addressable'
26
+
27
+ if RUBY_VERSION.start_with?("1")
28
+ s.add_runtime_dependency 'rake', '~> 12.2.0'
29
+ s.add_runtime_dependency 'addressable', '~> 2.4.0'
30
+ else
31
+ s.add_runtime_dependency 'addressable'
32
+ end
33
+
27
34
  s.add_runtime_dependency 'logstash-codec-multiline', ['~> 3.0']
28
35
 
29
36
  s.add_development_dependency 'stud', ['~> 0.0.19']
@@ -0,0 +1,36 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative '../filewatch/spec_helper'
4
+
5
+ if LogStash::Environment.windows?
6
+ describe "basic ops" do
7
+ let(:fixture_dir) { Pathname.new(FileWatch::FIXTURE_DIR).expand_path }
8
+ let(:file_path) { fixture_dir.join('uncompressed.log') }
9
+ it "path works" do
10
+ path = file_path.to_path
11
+ identifier = Winhelper.identifier_from_path(path)
12
+ STDOUT.puts("--- >>", identifier, "------")
13
+ expect(identifier.count('-')).to eq(2)
14
+ fs_name = Winhelper.file_system_type_from_path(path)
15
+ STDOUT.puts("--- >>", fs_name, "------")
16
+ expect(fs_name).to eq("NTFS")
17
+ # identifier = Winhelper.identifier_from_path_ex(path)
18
+ # STDOUT.puts("--- >>", identifier, "------")
19
+ # expect(identifier.count('-')).to eq(2)
20
+ end
21
+
22
+ it "io works" do
23
+ file = FileWatch::FileOpener.open(file_path.to_path)
24
+ identifier = Winhelper.identifier_from_io(file)
25
+ file.close
26
+ STDOUT.puts("--- >>", identifier, "------")
27
+ expect(identifier.count('-')).to eq(2)
28
+ # fs_name = Winhelper.file_system_type_from_io(file)
29
+ # STDOUT.puts("--- >>", fs_name, "------")
30
+ # expect(fs_name).to eq("NTFS")
31
+ # identifier = Winhelper.identifier_from_path_ex(path)
32
+ # STDOUT.puts("--- >>", identifier, "------")
33
+ # expect(identifier.count('-')).to eq(2)
34
+ end
35
+ end
36
+ end
@@ -12,7 +12,7 @@ module FileWatch
12
12
  let(:sdb_collection) { SincedbCollection.new(settings) }
13
13
  let(:directory) { Pathname.new(FIXTURE_DIR) }
14
14
  let(:pathname) { directory.join('uncompressed.log') }
15
- let(:watched_file) { WatchedFile.new(pathname, pathname.stat, settings) }
15
+ let(:watched_file) { WatchedFile.new(pathname, PathStatClass.new(pathname), settings) }
16
16
  let(:processor) { ReadMode::Processor.new(settings).add_watch(watch) }
17
17
  let(:file) { DummyFileReader.new(settings.file_chunk_size, 2) }
18
18
 
@@ -20,7 +20,7 @@ module FileWatch
20
20
  let(:watch) { double("watch", :quit? => false) }
21
21
  it "calls 'sincedb_write' exactly 2 times" do
22
22
  allow(FileOpener).to receive(:open).with(watched_file.path).and_return(file)
23
- expect(sdb_collection).to receive(:sincedb_write).exactly(2).times
23
+ expect(sdb_collection).to receive(:sincedb_write).exactly(1).times
24
24
  watched_file.activate
25
25
  processor.initialize_handlers(sdb_collection, TestObserver.new)
26
26
  processor.read_file(watched_file)
@@ -30,82 +30,115 @@ module FileWatch
30
30
  end
31
31
  let(:observer) { TestObserver.new }
32
32
  let(:reading) { ObservingRead.new(opts) }
33
- let(:actions) do
34
- RSpec::Sequencing.run_after(0.45, "quit after a short time") do
35
- reading.quit
36
- end
37
- end
33
+ let(:listener1) { observer.listener_for(file_path) }
38
34
 
39
35
  after do
40
36
  FileUtils.rm_rf(directory) unless directory =~ /fixture/
41
37
  end
42
38
 
43
39
  context "when watching a directory with files" do
44
- let(:directory) { Stud::Temporary.directory }
45
- let(:watch_dir) { ::File.join(directory, "*.log") }
46
- let(:file_path) { ::File.join(directory, "1.log") }
47
-
40
+ let(:actions) do
41
+ RSpec::Sequencing.run("quit after a short time") do
42
+ File.open(file_path, "wb") { |file| file.write("line1\nline2\n") }
43
+ end
44
+ .then("watch") do
45
+ reading.watch_this(watch_dir)
46
+ end
47
+ .then("wait") do
48
+ wait(2).for{listener1.calls.last}.to eq(:delete)
49
+ end
50
+ .then("quit") do
51
+ reading.quit
52
+ end
53
+ end
48
54
  it "the file is read" do
49
- File.open(file_path, "wb") { |file| file.write("line1\nline2\n") }
50
- actions.activate
51
- reading.watch_this(watch_dir)
55
+ actions.activate_quietly
52
56
  reading.subscribe(observer)
53
- expect(observer.listener_for(file_path).calls).to eq([:open, :accept, :accept, :eof, :delete])
54
- expect(observer.listener_for(file_path).lines).to eq(["line1", "line2"])
57
+ actions.assert_no_errors
58
+ expect(listener1.calls).to eq([:open, :accept, :accept, :eof, :delete])
59
+ expect(listener1.lines).to eq(["line1", "line2"])
55
60
  end
56
61
  end
57
62
 
58
63
  context "when watching a directory with files and sincedb_path is /dev/null or NUL" do
59
- let(:directory) { Stud::Temporary.directory }
60
64
  let(:sincedb_path) { File::NULL }
61
- let(:watch_dir) { ::File.join(directory, "*.log") }
62
- let(:file_path) { ::File.join(directory, "1.log") }
63
-
65
+ let(:actions) do
66
+ RSpec::Sequencing.run("quit after a short time") do
67
+ File.open(file_path, "wb") { |file| file.write("line1\nline2\n") }
68
+ end
69
+ .then("watch") do
70
+ reading.watch_this(watch_dir)
71
+ end
72
+ .then("wait") do
73
+ wait(2).for{listener1.calls.last}.to eq(:delete)
74
+ end
75
+ .then("quit") do
76
+ reading.quit
77
+ end
78
+ end
64
79
  it "the file is read" do
65
- File.open(file_path, "wb") { |file| file.write("line1\nline2\n") }
66
- actions.activate
67
- reading.watch_this(watch_dir)
80
+ actions.activate_quietly
68
81
  reading.subscribe(observer)
69
- expect(observer.listener_for(file_path).calls).to eq([:open, :accept, :accept, :eof, :delete])
70
- expect(observer.listener_for(file_path).lines).to eq(["line1", "line2"])
82
+ actions.assert_no_errors
83
+ expect(listener1.calls).to eq([:open, :accept, :accept, :eof, :delete])
84
+ expect(listener1.lines).to eq(["line1", "line2"])
71
85
  end
72
86
  end
73
87
 
74
88
  context "when watching a directory with files using striped reading" do
75
- let(:directory) { Stud::Temporary.directory }
76
- let(:watch_dir) { ::File.join(directory, "*.log") }
77
- let(:file_path1) { ::File.join(directory, "1.log") }
78
89
  let(:file_path2) { ::File.join(directory, "2.log") }
79
90
  # use a chunk size that does not align with the line boundaries
80
- let(:opts) { super.merge(:file_chunk_size => 10, :file_chunk_count => 1)}
91
+ let(:opts) { super.merge(:file_chunk_size => 10, :file_chunk_count => 1, :file_sort_by => "path")}
81
92
  let(:lines) { [] }
82
93
  let(:observer) { TestObserver.new(lines) }
83
-
94
+ let(:listener2) { observer.listener_for(file_path2) }
95
+ let(:actions) do
96
+ RSpec::Sequencing.run("create file") do
97
+ File.open(file_path, "w") { |file| file.write("string1\nstring2") }
98
+ File.open(file_path2, "w") { |file| file.write("stringA\nstringB") }
99
+ end
100
+ .then("watch") do
101
+ reading.watch_this(watch_dir)
102
+ end
103
+ .then("wait") do
104
+ wait(2).for{listener1.calls.last == :delete && listener2.calls.last == :delete}.to eq(true)
105
+ end
106
+ .then("quit") do
107
+ reading.quit
108
+ end
109
+ end
84
110
  it "the files are read seemingly in parallel" do
85
- File.open(file_path1, "w") { |file| file.write("string1\nstring2\n") }
86
- File.open(file_path2, "w") { |file| file.write("stringA\nstringB\n") }
87
- actions.activate
88
- reading.watch_this(watch_dir)
111
+ actions.activate_quietly
89
112
  reading.subscribe(observer)
90
- if lines.first == "stringA"
91
- expect(lines).to eq(%w(stringA string1 stringB string2))
92
- else
93
- expect(lines).to eq(%w(string1 stringA string2 stringB))
94
- end
113
+ actions.assert_no_errors
114
+ expect(listener1.calls).to eq([:open, :accept, :accept, :eof, :delete])
115
+ expect(listener2.calls).to eq([:open, :accept, :accept, :eof, :delete])
116
+ expect(lines).to eq(%w(string1 stringA string2 stringB))
95
117
  end
96
118
  end
97
119
 
98
120
  context "when a non default delimiter is specified and it is not in the content" do
99
121
  let(:opts) { super.merge(:delimiter => "\nø") }
100
-
122
+ let(:actions) do
123
+ RSpec::Sequencing.run("create file") do
124
+ File.open(file_path, "wb") { |file| file.write("line1\nline2") }
125
+ end
126
+ .then("watch") do
127
+ reading.watch_this(watch_dir)
128
+ end
129
+ .then("wait") do
130
+ wait(2).for{listener1.calls.last}.to eq(:delete)
131
+ end
132
+ .then("quit") do
133
+ reading.quit
134
+ end
135
+ end
101
136
  it "the file is opened, data is read, but no lines are found initially, at EOF the whole file becomes the line" do
102
- File.open(file_path, "wb") { |file| file.write("line1\nline2") }
103
- actions.activate
104
- reading.watch_this(watch_dir)
137
+ actions.activate_quietly
105
138
  reading.subscribe(observer)
106
- listener = observer.listener_for(file_path)
107
- expect(listener.calls).to eq([:open, :accept, :eof, :delete])
108
- expect(listener.lines).to eq(["line1\nline2"])
139
+ actions.assert_no_errors
140
+ expect(listener1.calls).to eq([:open, :accept, :eof, :delete])
141
+ expect(listener1.lines).to eq(["line1\nline2"])
109
142
  sincedb_record_fields = File.read(sincedb_path).split(" ")
110
143
  position_field_index = 3
111
144
  # tailing, no delimiter, we are expecting one, if it grows we read from the start.
@@ -116,18 +149,28 @@ module FileWatch
116
149
 
117
150
  describe "reading fixtures" do
118
151
  let(:directory) { FIXTURE_DIR }
119
-
152
+ let(:actions) do
153
+ RSpec::Sequencing.run("watch") do
154
+ reading.watch_this(watch_dir)
155
+ end
156
+ .then("wait") do
157
+ wait(1).for{listener1.calls.last}.to eq(:delete)
158
+ end
159
+ .then("quit") do
160
+ reading.quit
161
+ end
162
+ end
120
163
  context "for an uncompressed file" do
121
164
  let(:watch_dir) { ::File.join(directory, "unc*.log") }
122
165
  let(:file_path) { ::File.join(directory, 'uncompressed.log') }
123
166
 
124
167
  it "the file is read" do
125
168
  FileWatch.make_fixture_current(file_path)
126
- actions.activate
127
- reading.watch_this(watch_dir)
169
+ actions.activate_quietly
128
170
  reading.subscribe(observer)
129
- expect(observer.listener_for(file_path).calls).to eq([:open, :accept, :accept, :eof, :delete])
130
- expect(observer.listener_for(file_path).lines.size).to eq(2)
171
+ actions.assert_no_errors
172
+ expect(listener1.calls).to eq([:open, :accept, :accept, :eof, :delete])
173
+ expect(listener1.lines.size).to eq(2)
131
174
  end
132
175
  end
133
176
 
@@ -137,11 +180,11 @@ module FileWatch
137
180
 
138
181
  it "the file is read" do
139
182
  FileWatch.make_fixture_current(file_path)
140
- actions.activate
141
- reading.watch_this(watch_dir)
183
+ actions.activate_quietly
142
184
  reading.subscribe(observer)
143
- expect(observer.listener_for(file_path).calls).to eq([:open, :accept, :accept, :eof, :delete])
144
- expect(observer.listener_for(file_path).lines.size).to eq(2)
185
+ actions.assert_no_errors
186
+ expect(listener1.calls).to eq([:open, :accept, :accept, :eof, :delete])
187
+ expect(listener1.lines.size).to eq(2)
145
188
  end
146
189
  end
147
190
 
@@ -151,11 +194,11 @@ module FileWatch
151
194
 
152
195
  it "the file is read" do
153
196
  FileWatch.make_fixture_current(file_path)
154
- actions.activate
155
- reading.watch_this(watch_dir)
197
+ actions.activate_quietly
156
198
  reading.subscribe(observer)
157
- expect(observer.listener_for(file_path).calls).to eq([:open, :accept, :accept, :eof, :delete])
158
- expect(observer.listener_for(file_path).lines.size).to eq(2)
199
+ actions.assert_no_errors
200
+ expect(listener1.calls).to eq([:open, :accept, :accept, :eof, :delete])
201
+ expect(listener1.lines.size).to eq(2)
159
202
  end
160
203
  end
161
204
  end
@@ -0,0 +1,451 @@
1
+ # encoding: utf-8
2
+ require 'stud/temporary'
3
+ require_relative 'spec_helper'
4
+ require 'filewatch/observing_tail'
5
+
6
+ # simulate size based rotation ala
7
+ # See https://docs.python.org/2/library/logging.handlers.html#rotatingfilehandler
8
+ # The specified file is opened and used as the stream for logging.
9
+ # If mode is not specified, 'a' is used. If encoding is not None, it is used to
10
+ # open the file with that encoding. If delay is true, then file opening is deferred
11
+ # until the first call to emit(). By default, the file grows indefinitely.
12
+ # You can use the maxBytes and backupCount values to allow the file to rollover
13
+ # at a predetermined size. When the size is about to be exceeded, the file is
14
+ # closed and a new file is silently opened for output. Rollover occurs whenever
15
+ # the current log file is nearly maxBytes in length; if either of maxBytes or
16
+ # backupCount is zero, rollover never occurs. If backupCount is non-zero, the
17
+ # system will save old log files by appending the extensions ‘.1’, ‘.2’ etc.,
18
+ # to the filename. For example, with a backupCount of 5 and a base file name of
19
+ # app.log, you would get app.log, app.log.1, app.log.2, up to app.log.5.
20
+ # The file being written to is always app.log. When this file is filled, it is
21
+ # closed and renamed to app.log.1, and if files app.log.1, app.log.2, etc.
22
+ # exist, then they are renamed to app.log.2, app.log.3 etc. respectively.
23
+
24
+ module FileWatch
25
+ describe Watch, :unix => true do
26
+ let(:directory) { Pathname.new(Stud::Temporary.directory) }
27
+ let(:file1_path) { file_path.to_path }
28
+ let(:max) { 4095 }
29
+ let(:stat_interval) { 0.01 }
30
+ let(:discover_interval) { 15 }
31
+ let(:start_new_files_at) { :beginning }
32
+ let(:sincedb_path) { directory.join("tailing.sdb") }
33
+ let(:opts) do
34
+ {
35
+ :stat_interval => stat_interval, :start_new_files_at => start_new_files_at, :max_open_files => max,
36
+ :delimiter => "\n", :discover_interval => discover_interval, :sincedb_path => sincedb_path.to_path
37
+ }
38
+ end
39
+ let(:observer) { TestObserver.new }
40
+ let(:tailing) { ObservingTail.new(opts) }
41
+ let(:line1) { "Line 1 - Lorem ipsum dolor sit amet, consectetur adipiscing elit." }
42
+ let(:line2) { "Line 2 - Proin ut orci lobortis, congue diam in, dictum est." }
43
+ let(:line3) { "Line 3 - Sed vestibulum accumsan sollicitudin." }
44
+
45
+ before do
46
+ directory
47
+ wait(1.0).for{Dir.exist?(directory)}.to eq(true)
48
+ end
49
+
50
+ after do
51
+ FileUtils.rm_rf(directory)
52
+ wait(1.0).for{Dir.exist?(directory)}.to eq(false)
53
+ end
54
+
55
+ context "create + rename rotation: when a new logfile is renamed to a path we have seen before and the open file is fully read, renamed outside glob" do
56
+ let(:watch_dir) { directory.join("*A.log") }
57
+ let(:file_path) { directory.join("1A.log") }
58
+ subject { described_class.new(conf) }
59
+ let(:listener1) { observer.listener_for(file1_path) }
60
+ let(:listener2) { observer.listener_for(second_file.to_path) }
61
+ let(:actions) do
62
+ RSpec::Sequencing
63
+ .run_after(0.25, "create file") do
64
+ file_path.open("wb") { |file| file.write("#{line1}\n") }
65
+ end
66
+ .then_after(0.25, "write a 'unfinished' line") do
67
+ file_path.open("ab") { |file| file.write(line2) }
68
+ end
69
+ .then_after(0.25, "rotate once") do
70
+ tmpfile = directory.join("1.logtmp")
71
+ tmpfile.open("wb") { |file| file.write("\n#{line3}\n")}
72
+ file_path.rename(directory.join("1.log.1"))
73
+ FileUtils.mv(directory.join("1.logtmp").to_path, file1_path)
74
+ end
75
+ .then("wait for expectation") do
76
+ wait(2).for{listener1.calls}.to eq([:open, :accept, :accept, :accept])
77
+ end
78
+ .then("quit") do
79
+ tailing.quit
80
+ end
81
+ end
82
+
83
+ it "content from both inodes are sent via the same stream" do
84
+ actions.activate_quietly
85
+ tailing.watch_this(watch_dir.to_path)
86
+ tailing.subscribe(observer)
87
+ actions.assert_no_errors
88
+ lines = listener1.lines
89
+ expect(lines[0]).to eq(line1)
90
+ expect(lines[1]).to eq(line2)
91
+ expect(lines[2]).to eq(line3)
92
+ end
93
+ end
94
+
95
+ context "create + rename rotation: a multiple file rename cascade" do
96
+ let(:watch_dir) { directory.join("*B.log") }
97
+ let(:file_path) { directory.join("1B.log") }
98
+ subject { described_class.new(conf) }
99
+ let(:second_file) { directory.join("2B.log") }
100
+ let(:third_file) { directory.join("3B.log") }
101
+ let(:listener1) { observer.listener_for(file1_path) }
102
+ let(:listener2) { observer.listener_for(second_file.to_path) }
103
+ let(:listener3) { observer.listener_for(third_file.to_path) }
104
+ let(:actions) do
105
+ RSpec::Sequencing
106
+ .run_after(0.25, "create file") do
107
+ file_path.open("wb") { |file| file.write("#{line1}\n") }
108
+ end
109
+ .then_after(0.25, "rotate 1 - line1(66) is in 2B.log, line2(61) is in 1B.log") do
110
+ file_path.rename(second_file)
111
+ file_path.open("wb") { |file| file.write("#{line2}\n") }
112
+ end
113
+ .then_after(0.25, "rotate 2 - line1(66) is in 3B.log, line2(61) is in 2B.log, line3(47) is in 1B.log") do
114
+ second_file.rename(third_file)
115
+ file_path.rename(second_file)
116
+ file_path.open("wb") { |file| file.write("#{line3}\n") }
117
+ end
118
+ .then("wait for expectations to be met") do
119
+ wait(0.75).for{listener1.lines.size == 3 && listener3.lines.empty? && listener2.lines.empty?}.to eq(true)
120
+ end
121
+ .then("quit") do
122
+ tailing.quit
123
+ end
124
+ end
125
+
126
+ it "content from both inodes are sent via the same stream" do
127
+ actions.activate_quietly
128
+ tailing.watch_this(watch_dir.to_path)
129
+ tailing.subscribe(observer)
130
+ actions.assert_no_errors
131
+ expect(listener1.lines[0]).to eq(line1)
132
+ expect(listener1.lines[1]).to eq(line2)
133
+ expect(listener1.lines[2]).to eq(line3)
134
+ end
135
+ end
136
+
137
+ context "create + rename rotation: a two file rename cascade in slow motion" do
138
+ let(:watch_dir) { directory.join("*C.log") }
139
+ let(:file_path) { directory.join("1C.log") }
140
+ let(:stat_interval) { 0.01 }
141
+ subject { described_class.new(conf) }
142
+ let(:second_file) { directory.join("2C.log") }
143
+ let(:listener1) { observer.listener_for(file1_path) }
144
+ let(:listener2) { observer.listener_for(second_file.to_path) }
145
+ let(:actions) do
146
+ RSpec::Sequencing
147
+ .run_after(0.25, "create original - write line 1, 66 bytes") do
148
+ file_path.open("wb") { |file| file.write("#{line1}\n") }
149
+ end
150
+ .then_after(0.25, "rename to 2.log") do
151
+ file_path.rename(second_file)
152
+ end
153
+ .then_after(0.25, "write line 2 to original, 61 bytes") do
154
+ file_path.open("wb") { |file| file.write("#{line2}\n") }
155
+ end
156
+ .then_after(0.25, "rename to 2.log again") do
157
+ file_path.rename(second_file)
158
+ end
159
+ .then_after(0.25, "write line 3 to original, 47 bytes") do
160
+ file_path.open("wb") { |file| file.write("#{line3}\n") }
161
+ end
162
+ .then("wait for expectations to be met") do
163
+ wait(1).for{listener1.lines.size == 3 && listener2.lines.empty?}.to eq(true)
164
+ end
165
+ .then("quit") do
166
+ tailing.quit
167
+ end
168
+ end
169
+
170
+ it "content from both inodes are sent via the same stream AND content from the rotated file is not read again" do
171
+ actions.activate_quietly
172
+ tailing.watch_this(watch_dir.to_path)
173
+ tailing.subscribe(observer)
174
+ actions.assert_no_errors
175
+ expect(listener1.lines[0]).to eq(line1)
176
+ expect(listener1.lines[1]).to eq(line2)
177
+ expect(listener1.lines[2]).to eq(line3)
178
+ end
179
+ end
180
+
181
+ context "create + rename rotation: a two file rename cascade in normal speed" do
182
+ let(:watch_dir) { directory.join("*D.log") }
183
+ let(:file_path) { directory.join("1D.log") }
184
+ subject { described_class.new(conf) }
185
+ let(:second_file) { directory.join("2D.log") }
186
+ let(:listener1) { observer.listener_for(file1_path) }
187
+ let(:listener2) { observer.listener_for(second_file.to_path) }
188
+ let(:actions) do
189
+ RSpec::Sequencing
190
+ .run_after(0.25, "create original - write line 1, 66 bytes") do
191
+ file_path.open("wb") { |file| file.write("#{line1}\n") }
192
+ end
193
+ .then_after(0.25, "rename to 2.log") do
194
+ file_path.rename(second_file)
195
+ file_path.open("wb") { |file| file.write("#{line2}\n") }
196
+ end
197
+ .then_after(0.25, "rename to 2.log again") do
198
+ file_path.rename(second_file)
199
+ file_path.open("wb") { |file| file.write("#{line3}\n") }
200
+ end
201
+ .then("wait for expectations to be met") do
202
+ wait(0.5).for{listener1.lines.size == 3 && listener2.lines.empty?}.to eq(true)
203
+ end
204
+ .then("quit") do
205
+ tailing.quit
206
+ end
207
+ end
208
+
209
+ it "content from both inodes are sent via the same stream AND content from the rotated file is not read again" do
210
+ actions.activate_quietly
211
+ tailing.watch_this(watch_dir.to_path)
212
+ tailing.subscribe(observer)
213
+ actions.assert_no_errors
214
+ expect(listener1.lines[0]).to eq(line1)
215
+ expect(listener1.lines[1]).to eq(line2)
216
+ expect(listener1.lines[2]).to eq(line3)
217
+ end
218
+ end
219
+
220
+ context "create + rename rotation: when a new logfile is renamed to a path we have seen before but not all content from the previous the file is read" do
221
+ let(:opts) { super.merge(
222
+ :file_chunk_size => line1.bytesize.succ,
223
+ :file_chunk_count => 1
224
+ ) }
225
+ let(:watch_dir) { directory.join("*E.log") }
226
+ let(:file_path) { directory.join("1E.log") }
227
+ subject { described_class.new(conf) }
228
+ let(:listener1) { observer.listener_for(file1_path) }
229
+ let(:actions) do
230
+ RSpec::Sequencing
231
+ .run_after(0.25, "create file") do
232
+ file_path.open("wb") do |file|
233
+ 65.times{file.puts(line1)}
234
+ end
235
+ end
236
+ .then_after(0.25, "rotate") do
237
+ tmpfile = directory.join("1E.logtmp")
238
+ tmpfile.open("wb") { |file| file.puts(line1)}
239
+ file_path.rename(directory.join("1E.log.1"))
240
+ tmpfile.rename(directory.join("1E.log"))
241
+ end
242
+ .then("wait for expectations to be met") do
243
+ wait(0.5).for{listener1.lines.size}.to eq(66)
244
+ end
245
+ .then("quit") do
246
+ tailing.quit
247
+ end
248
+ end
249
+
250
+ it "content from both inodes are sent via the same stream" do
251
+ actions.activate_quietly
252
+ tailing.watch_this(watch_dir.to_path)
253
+ tailing.subscribe(observer)
254
+ actions.assert_no_errors
255
+ expected_calls = ([:accept] * 66).unshift(:open)
256
+ expect(listener1.lines.uniq).to eq([line1])
257
+ expect(listener1.calls).to eq(expected_calls)
258
+ expect(sincedb_path.readlines.size).to eq(2)
259
+ end
260
+ end
261
+
262
+ context "copy + truncate rotation: when a logfile is copied to a new path and truncated and the open file is fully read" do
263
+ let(:watch_dir) { directory.join("*F.log") }
264
+ let(:file_path) { directory.join("1F.log") }
265
+ subject { described_class.new(conf) }
266
+ let(:listener1) { observer.listener_for(file1_path) }
267
+ let(:actions) do
268
+ RSpec::Sequencing
269
+ .run_after(0.25, "create file") do
270
+ file_path.open("wb") { |file| file.puts(line1); file.puts(line2) }
271
+ end
272
+ .then_after(0.25, "rotate") do
273
+ FileUtils.cp(file1_path, directory.join("1F.log.1").to_path)
274
+ file_path.truncate(0)
275
+ end
276
+ .then_after(0.25, "write to truncated file") do
277
+ file_path.open("wb") { |file| file.puts(line3) }
278
+ end
279
+ .then("wait for expectations to be met") do
280
+ wait(0.5).for{listener1.lines.size}.to eq(3)
281
+ end
282
+ .then("quit") do
283
+ tailing.quit
284
+ end
285
+ end
286
+
287
+ it "content is read correctly" do
288
+ actions.activate_quietly
289
+ tailing.watch_this(watch_dir.to_path)
290
+ tailing.subscribe(observer)
291
+ actions.assert_no_errors
292
+ expect(listener1.lines).to eq([line1, line2, line3])
293
+ expect(listener1.calls).to eq([:open, :accept, :accept, :accept])
294
+ end
295
+ end
296
+
297
+ context "copy + truncate rotation: when a logfile is copied to a new path and truncated before the open file is fully read" do
298
+ let(:opts) { super.merge(
299
+ :file_chunk_size => line1.bytesize.succ,
300
+ :file_chunk_count => 1
301
+ ) }
302
+ let(:watch_dir) { directory.join("*G.log") }
303
+ let(:file_path) { directory.join("1G.log") }
304
+ subject { described_class.new(conf) }
305
+ let(:listener1) { observer.listener_for(file1_path) }
306
+ let(:actions) do
307
+ RSpec::Sequencing
308
+ .run_after(0.25, "create file") do
309
+ file_path.open("wb") { |file| 65.times{file.puts(line1)} }
310
+ end
311
+ .then_after(0.25, "rotate") do
312
+ FileUtils.cp(file1_path, directory.join("1G.log.1").to_path)
313
+ file_path.truncate(0)
314
+ end
315
+ .then_after(0.25, "write to truncated file") do
316
+ file_path.open("wb") { |file| file.puts(line3) }
317
+ end
318
+ .then("wait for expectations to be met") do
319
+ wait(0.5).for{listener1.lines.last}.to eq(line3)
320
+ end
321
+ .then("quit") do
322
+ tailing.quit
323
+ end
324
+ end
325
+
326
+ it "unread content before the truncate is lost" do
327
+ actions.activate_quietly
328
+ tailing.watch_this(watch_dir.to_path)
329
+ tailing.subscribe(observer)
330
+ actions.assert_no_errors
331
+ expect(listener1.lines.size).to be < 66
332
+ end
333
+ end
334
+
335
+ context "? rotation: when an active file is renamed inside the glob and the reading does not lag" do
336
+ let(:watch_dir) { directory.join("*H.log") }
337
+ let(:file_path) { directory.join("1H.log") }
338
+ let(:file2) { directory.join("2H.log") }
339
+ subject { described_class.new(conf) }
340
+ let(:listener1) { observer.listener_for(file1_path) }
341
+ let(:listener2) { observer.listener_for(file2.to_path) }
342
+ let(:actions) do
343
+ RSpec::Sequencing
344
+ .run_after(0.25, "create file") do
345
+ file_path.open("wb") { |file| file.puts(line1); file.puts(line2) }
346
+ end
347
+ .then_after(0.25, "rename") do
348
+ FileUtils.mv(file1_path, file2.to_path)
349
+ end
350
+ .then_after(0.25, "write to renamed file") do
351
+ file2.open("ab") { |file| file.puts(line3) }
352
+ end
353
+ .then("wait for expectations to be met") do
354
+ wait(0.75).for{listener1.lines.size + listener2.lines.size}.to eq(3)
355
+ end
356
+ .then("quit") do
357
+ tailing.quit
358
+ end
359
+ end
360
+
361
+ it "content is read correctly, the renamed file is not reread from scratch" do
362
+ actions.activate_quietly
363
+ tailing.watch_this(watch_dir.to_path)
364
+ tailing.subscribe(observer)
365
+ actions.assert_no_errors
366
+ expect(listener1.lines).to eq([line1, line2])
367
+ expect(listener2.lines).to eq([line3])
368
+ end
369
+ end
370
+
371
+ context "? rotation: when an active file is renamed inside the glob and the reading lags behind" do
372
+ let(:opts) { super.merge(
373
+ :file_chunk_size => line1.bytesize.succ,
374
+ :file_chunk_count => 2
375
+ ) }
376
+ let(:watch_dir) { directory.join("*I.log") }
377
+ let(:file_path) { directory.join("1I.log") }
378
+ let(:file2) { directory.join("2I.log") }
379
+ subject { described_class.new(conf) }
380
+ let(:listener1) { observer.listener_for(file1_path) }
381
+ let(:listener2) { observer.listener_for(file2.to_path) }
382
+ let(:actions) do
383
+ RSpec::Sequencing
384
+ .run_after(0.25, "create file") do
385
+ file_path.open("wb") { |file| 65.times{file.puts(line1)} }
386
+ end
387
+ .then_after(0.25, "rename") do
388
+ FileUtils.mv(file1_path, file2.to_path)
389
+ end
390
+ .then_after(0.25, "write to renamed file") do
391
+ file2.open("ab") { |file| file.puts(line3) }
392
+ end
393
+ .then("wait for expectations to be met") do
394
+ wait(1.25).for{listener1.lines.size + listener2.lines.size}.to eq(66)
395
+ end
396
+ .then("quit") do
397
+ tailing.quit
398
+ end
399
+ end
400
+
401
+ it "content is read correctly, the renamed file is not reread from scratch" do
402
+ actions.activate_quietly
403
+ tailing.watch_this(watch_dir.to_path)
404
+ tailing.subscribe(observer)
405
+ actions.assert_no_errors
406
+ expect(listener2.lines.last).to eq(line3)
407
+ end
408
+ end
409
+
410
+ context "? rotation: when a not active file is rotated outside the glob before the file is read" do
411
+ let(:opts) { super.merge(
412
+ :close_older => 3600,
413
+ :max_open_files => 1,
414
+ :file_sort_by => "path"
415
+ ) }
416
+ let(:watch_dir) { directory.join("*J.log") }
417
+ let(:file_path) { directory.join("1J.log") }
418
+ let(:file2) { directory.join("2J.log") }
419
+ let(:file3) { directory.join("2J.log.1") }
420
+ let(:listener1) { observer.listener_for(file1_path) }
421
+ let(:listener2) { observer.listener_for(file2.to_path) }
422
+ let(:listener3) { observer.listener_for(file3.to_path) }
423
+ subject { described_class.new(conf) }
424
+ let(:actions) do
425
+ RSpec::Sequencing
426
+ .run_after(0.25, "create file") do
427
+ file_path.open("wb") { |file| 65.times{file.puts(line1)} }
428
+ file2.open("wb") { |file| 65.times{file.puts(line1)} }
429
+ end
430
+ .then_after(0.25, "rename") do
431
+ FileUtils.mv(file2.to_path, file3.to_path)
432
+ end
433
+ .then("wait for expectations to be met") do
434
+ wait(1.25).for{listener1.lines.size}.to eq(65)
435
+ end
436
+ .then("quit") do
437
+ tailing.quit
438
+ end
439
+ end
440
+
441
+ it "file 1 content is read correctly, the renamed file 2 is not read at all" do
442
+ actions.activate_quietly
443
+ tailing.watch_this(watch_dir.to_path)
444
+ tailing.subscribe(observer)
445
+ actions.assert_no_errors
446
+ expect(listener2.lines.size).to eq(0)
447
+ expect(listener3.lines.size).to eq(0)
448
+ end
449
+ end
450
+ end
451
+ end