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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/JAR_VERSION +1 -1
- data/README.md +0 -3
- data/docs/index.asciidoc +26 -16
- data/lib/filewatch/bootstrap.rb +10 -21
- data/lib/filewatch/discoverer.rb +35 -28
- data/lib/filewatch/observing_base.rb +2 -1
- data/lib/filewatch/read_mode/handlers/base.rb +19 -6
- data/lib/filewatch/read_mode/handlers/read_file.rb +43 -32
- data/lib/filewatch/read_mode/handlers/read_zip_file.rb +8 -3
- data/lib/filewatch/read_mode/processor.rb +8 -8
- data/lib/filewatch/settings.rb +3 -3
- data/lib/filewatch/sincedb_collection.rb +56 -42
- data/lib/filewatch/sincedb_value.rb +6 -0
- data/lib/filewatch/stat/generic.rb +34 -0
- data/lib/filewatch/stat/windows_path.rb +32 -0
- data/lib/filewatch/tail_mode/handlers/base.rb +40 -22
- data/lib/filewatch/tail_mode/handlers/create.rb +1 -2
- data/lib/filewatch/tail_mode/handlers/create_initial.rb +2 -1
- data/lib/filewatch/tail_mode/handlers/delete.rb +13 -1
- data/lib/filewatch/tail_mode/handlers/grow.rb +5 -2
- data/lib/filewatch/tail_mode/handlers/shrink.rb +7 -4
- data/lib/filewatch/tail_mode/handlers/unignore.rb +4 -2
- data/lib/filewatch/tail_mode/processor.rb +147 -58
- data/lib/filewatch/watch.rb +15 -35
- data/lib/filewatch/watched_file.rb +237 -41
- data/lib/filewatch/watched_files_collection.rb +2 -2
- data/lib/filewatch/winhelper.rb +167 -25
- data/lib/jars/filewatch-1.0.1.jar +0 -0
- data/lib/logstash/inputs/file.rb +9 -2
- data/logstash-input-file.gemspec +9 -2
- data/spec/file_ext/file_ext_windows_spec.rb +36 -0
- data/spec/filewatch/read_mode_handlers_read_file_spec.rb +2 -2
- data/spec/filewatch/reading_spec.rb +100 -57
- data/spec/filewatch/rotate_spec.rb +451 -0
- data/spec/filewatch/spec_helper.rb +33 -10
- data/spec/filewatch/tailing_spec.rb +273 -153
- data/spec/filewatch/watched_file_spec.rb +3 -3
- data/spec/filewatch/watched_files_collection_spec.rb +3 -3
- data/spec/filewatch/winhelper_spec.rb +4 -5
- data/spec/helpers/logging_level_helper.rb +8 -0
- data/spec/helpers/rspec_wait_handler_helper.rb +38 -0
- data/spec/helpers/spec_helper.rb +7 -1
- data/spec/inputs/file_read_spec.rb +54 -24
- data/spec/inputs/file_tail_spec.rb +244 -284
- metadata +13 -3
- data/lib/jars/filewatch-1.0.0.jar +0 -0
Binary file
|
data/lib/logstash/inputs/file.rb
CHANGED
@@ -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::
|
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
|
-
|
364
|
+
unless @watcher.nil?
|
358
365
|
@codec.close
|
359
366
|
@watcher.quit
|
360
367
|
end
|
data/logstash-input-file.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
|
3
3
|
s.name = 'logstash-input-file'
|
4
|
-
s.version = '4.1.
|
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
|
-
|
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
|
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(
|
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(:
|
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(:
|
45
|
-
|
46
|
-
|
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
|
-
|
50
|
-
actions.activate
|
51
|
-
reading.watch_this(watch_dir)
|
55
|
+
actions.activate_quietly
|
52
56
|
reading.subscribe(observer)
|
53
|
-
|
54
|
-
expect(
|
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(:
|
62
|
-
|
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
|
-
|
66
|
-
actions.activate
|
67
|
-
reading.watch_this(watch_dir)
|
80
|
+
actions.activate_quietly
|
68
81
|
reading.subscribe(observer)
|
69
|
-
|
70
|
-
expect(
|
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
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
103
|
-
actions.activate
|
104
|
-
reading.watch_this(watch_dir)
|
137
|
+
actions.activate_quietly
|
105
138
|
reading.subscribe(observer)
|
106
|
-
|
107
|
-
expect(
|
108
|
-
expect(
|
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.
|
127
|
-
reading.watch_this(watch_dir)
|
169
|
+
actions.activate_quietly
|
128
170
|
reading.subscribe(observer)
|
129
|
-
|
130
|
-
expect(
|
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.
|
141
|
-
reading.watch_this(watch_dir)
|
183
|
+
actions.activate_quietly
|
142
184
|
reading.subscribe(observer)
|
143
|
-
|
144
|
-
expect(
|
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.
|
155
|
-
reading.watch_this(watch_dir)
|
197
|
+
actions.activate_quietly
|
156
198
|
reading.subscribe(observer)
|
157
|
-
|
158
|
-
expect(
|
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
|