logstash-input-file 4.1.3 → 4.1.4

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