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