fluentd 1.12.0.rc1 → 1.12.0.rc2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of fluentd might be problematic. Click here for more details.

@@ -0,0 +1,57 @@
1
+ require_relative '../helper'
2
+
3
+ require 'test-unit'
4
+ require 'win32/event' if Fluent.windows?
5
+
6
+ require 'fluent/command/ctl'
7
+
8
+ class TestFluentdCtl < ::Test::Unit::TestCase
9
+ def assert_win32_event(event_name, command, pid_or_svcname)
10
+ command, event_suffix = data
11
+ event = Win32::Event.new(event_name)
12
+ ipc = Win32::Ipc.new(event.handle)
13
+ ret = Win32::Ipc::TIMEOUT
14
+
15
+ wait_thread = Thread.new do
16
+ ret = ipc.wait(1)
17
+ end
18
+ Fluent::Ctl.new([command, pid_or_svcname]).call
19
+ wait_thread.join
20
+ assert_equal(Win32::Ipc::SIGNALED, ret)
21
+ end
22
+
23
+ data("shutdown" => ["shutdown", "TERM", ""],
24
+ "restart" => ["restart", "HUP", "HUP"],
25
+ "flush" => ["flush", "USR1", "USR1"],
26
+ "reload" => ["reload", "USR2", "USR2"])
27
+ def test_commands(data)
28
+ command, signal, event_suffix = data
29
+
30
+ if Fluent.windows?
31
+ event_name = "fluentd_54321"
32
+ event_name << "_#{event_suffix}" unless event_suffix.empty?
33
+ assert_win32_event(event_name, command, "54321")
34
+ else
35
+ got_signal = false
36
+ Signal.trap(signal) do
37
+ got_signal = true
38
+ end
39
+ Fluent::Ctl.new([command, "#{$$}"]).call
40
+ assert_true(got_signal)
41
+ end
42
+ end
43
+
44
+ data("shutdown" => ["shutdown", ""],
45
+ "restart" => ["restart", "HUP"],
46
+ "flush" => ["flush", "USR1"],
47
+ "reload" => ["reload", "USR2"])
48
+ def test_commands_with_winsvcname(data)
49
+ omit "Only for Windows" unless Fluent.windows?
50
+
51
+ command, event_suffix = data
52
+ event_name = "testfluentdwinsvc"
53
+ event_name << "_#{event_suffix}" unless event_suffix.empty?
54
+
55
+ assert_win32_event(event_name, command, "testfluentdwinsvc")
56
+ end
57
+ end
@@ -56,6 +56,32 @@ class TestFluentPluginConfigFormatter < Test::Unit::TestCase
56
56
  end
57
57
  end
58
58
 
59
+ class FakeStorage < ::Fluent::Plugin::Storage
60
+ ::Fluent::Plugin.register_storage('fake', self)
61
+
62
+ def get(key)
63
+ end
64
+
65
+ def fetch(key, defval)
66
+ end
67
+
68
+ def put(key, value)
69
+ end
70
+
71
+ def delete(key)
72
+ end
73
+
74
+ def update(key, &block)
75
+ end
76
+ end
77
+
78
+ class FakeServiceDiscovery < ::Fluent::Plugin::ServiceDiscovery
79
+ ::Fluent::Plugin.register_sd('fake', self)
80
+
81
+ desc "hostname"
82
+ config_param :hostname, :string
83
+ end
84
+
59
85
  class SimpleInput < ::Fluent::Plugin::Input
60
86
  ::Fluent::Plugin.register_input("simple", self)
61
87
  helpers :inject, :compat_parameters
@@ -88,6 +114,13 @@ class TestFluentPluginConfigFormatter < Test::Unit::TestCase
88
114
  end
89
115
  end
90
116
 
117
+ class SimpleServiceDiscovery < ::Fluent::Plugin::ServiceDiscovery
118
+ ::Fluent::Plugin.register_sd('simple', self)
119
+
120
+ desc "servers"
121
+ config_param :servers, :array
122
+ end
123
+
91
124
  sub_test_case "json" do
92
125
  data(input: [FakeInput, "input"],
93
126
  output: [FakeOutput, "output"],
@@ -196,6 +229,28 @@ TEXT
196
229
  assert_equal(expected, dumped_config)
197
230
  end
198
231
 
232
+ data("abbrev" => "sd",
233
+ "normal" => "service_discovery")
234
+ test "service_discovery simple" do |data|
235
+ plugin_type = data
236
+ dumped_config = capture_stdout do
237
+ FluentPluginConfigFormatter.new(["--format=markdown", plugin_type, "simple"]).call
238
+ end
239
+ expected = <<TEXT
240
+ * See also: [ServiceDiscovery Plugin Overview](https://docs.fluentd.org/v/1.0/servicediscovery#overview)
241
+
242
+ ## TestFluentPluginConfigFormatter::SimpleServiceDiscovery
243
+
244
+ ### servers (array) (required)
245
+
246
+ servers
247
+
248
+
249
+ TEXT
250
+ assert_equal(expected, dumped_config)
251
+ end
252
+
253
+
199
254
  test "output complex" do
200
255
  dumped_config = capture_stdout do
201
256
  FluentPluginConfigFormatter.new(["--format=markdown", "output", "complex"]).call
@@ -251,7 +306,7 @@ TEXT
251
306
  sub_test_case "arguments" do
252
307
  data do
253
308
  hash = {}
254
- ["input", "output", "filter", "parser", "formatter"].each do |type|
309
+ ["input", "output", "filter", "parser", "formatter", "storage", "service_discovery"].each do |type|
255
310
  ["txt", "json", "markdown"].each do |format|
256
311
  argv = ["--format=#{format}"]
257
312
  [
@@ -259,7 +314,7 @@ TEXT
259
314
  ["--verbose"],
260
315
  ["--compact"]
261
316
  ].each do |options|
262
- hash[(argv + options).join(" ")] = argv + options + [type, "fake"]
317
+ hash["[#{type}] " + (argv + options).join(" ")] = argv + options + [type, "fake"]
263
318
  end
264
319
  end
265
320
  end
@@ -1,5 +1,6 @@
1
1
  require_relative '../../helper'
2
2
  require 'fluent/plugin/in_tail/position_file'
3
+ require 'fluent/plugin/in_tail'
3
4
 
4
5
  require 'fileutils'
5
6
  require 'tempfile'
@@ -27,9 +28,15 @@ class IntailPositionFileTest < Test::Unit::TestCase
27
28
  f.seek(0)
28
29
  end
29
30
 
31
+ def follow_inodes_block
32
+ [true, false].each do |follow_inodes|
33
+ yield follow_inodes
34
+ end
35
+ end
36
+
30
37
  test '.load' do
31
38
  write_data(@file, TEST_CONTENT)
32
- Fluent::Plugin::TailInput::PositionFile.load(@file, logger: $log)
39
+ Fluent::Plugin::TailInput::PositionFile.load(@file, false, {}, **{logger: $log})
33
40
 
34
41
  @file.seek(0)
35
42
  lines = @file.readlines
@@ -41,7 +48,7 @@ class IntailPositionFileTest < Test::Unit::TestCase
41
48
  sub_test_case '#try_compact' do
42
49
  test 'compact invalid and convert 32 bit inode value' do
43
50
  write_data(@file, TEST_CONTENT)
44
- Fluent::Plugin::TailInput::PositionFile.new(@file, logger: $log).try_compact
51
+ Fluent::Plugin::TailInput::PositionFile.new(@file, false, {}, **{logger: $log}).try_compact
45
52
 
46
53
  @file.seek(0)
47
54
  lines = @file.readlines
@@ -55,7 +62,7 @@ class IntailPositionFileTest < Test::Unit::TestCase
55
62
  valid_path\t0000000000000002\t0000000000000001
56
63
  valid_path\t0000000000000003\t0000000000000004
57
64
  EOF
58
- Fluent::Plugin::TailInput::PositionFile.new(@file, logger: $log).try_compact
65
+ Fluent::Plugin::TailInput::PositionFile.new(@file, false, {}, **{logger: $log}).try_compact
59
66
 
60
67
  @file.seek(0)
61
68
  lines = @file.readlines
@@ -64,7 +71,7 @@ class IntailPositionFileTest < Test::Unit::TestCase
64
71
 
65
72
  test 'does not change when the file is changed' do
66
73
  write_data(@file, TEST_CONTENT)
67
- pf = Fluent::Plugin::TailInput::PositionFile.new(@file, logger: $log)
74
+ pf = Fluent::Plugin::TailInput::PositionFile.new(@file, false, {}, **{logger: $log})
68
75
 
69
76
  mock.proxy(pf).fetch_compacted_entries do |r|
70
77
  @file.write("unwatched\t#{UNWATCHED_STR}\t0000000000000000\n")
@@ -79,11 +86,16 @@ class IntailPositionFileTest < Test::Unit::TestCase
79
86
  end
80
87
 
81
88
  test 'update seek postion of remained position entry' do
82
- pf = Fluent::Plugin::TailInput::PositionFile.new(@file, logger: $log)
83
- pf['path1']
84
- pf['path2']
85
- pf['path3']
86
- pf.unwatch('path1')
89
+ pf = Fluent::Plugin::TailInput::PositionFile.new(@file, false, {}, **{logger: $log})
90
+ target_info1 = Fluent::Plugin::TailInput::TargetInfo.new('path1', -1)
91
+ target_info2 = Fluent::Plugin::TailInput::TargetInfo.new('path2', -1)
92
+ target_info3 = Fluent::Plugin::TailInput::TargetInfo.new('path3', -1)
93
+ pf[target_info1]
94
+ pf[target_info2]
95
+ pf[target_info3]
96
+
97
+ target_info1_2 = Fluent::Plugin::TailInput::TargetInfo.new('path1', 1234)
98
+ pf.unwatch(target_info1_2)
87
99
 
88
100
  pf.try_compact
89
101
 
@@ -93,8 +105,10 @@ class IntailPositionFileTest < Test::Unit::TestCase
93
105
  assert_equal "path3\t0000000000000000\t0000000000000000\n", lines[1]
94
106
  assert_equal 2, lines.size
95
107
 
96
- pf.unwatch('path2')
97
- pf.unwatch('path3')
108
+ target_info2_2 = Fluent::Plugin::TailInput::TargetInfo.new('path2', 1235)
109
+ target_info3_2 = Fluent::Plugin::TailInput::TargetInfo.new('path3', 1236)
110
+ pf.unwatch(target_info2_2)
111
+ pf.unwatch(target_info3_2)
98
112
  @file.seek(0)
99
113
  lines = @file.readlines
100
114
  assert_equal "path2\t#{UNWATCHED_STR}\t0000000000000000\n", lines[0]
@@ -106,7 +120,7 @@ class IntailPositionFileTest < Test::Unit::TestCase
106
120
  sub_test_case '#load' do
107
121
  test 'compact invalid and convert 32 bit inode value' do
108
122
  write_data(@file, TEST_CONTENT)
109
- Fluent::Plugin::TailInput::PositionFile.load(@file, logger: $log)
123
+ Fluent::Plugin::TailInput::PositionFile.load(@file, false, {}, **{logger: $log})
110
124
 
111
125
  @file.seek(0)
112
126
  lines = @file.readlines
@@ -120,7 +134,7 @@ class IntailPositionFileTest < Test::Unit::TestCase
120
134
  valid_path\t0000000000000002\t0000000000000001
121
135
  valid_path\t0000000000000003\t0000000000000004
122
136
  EOF
123
- Fluent::Plugin::TailInput::PositionFile.new(@file, logger: $log).load
137
+ Fluent::Plugin::TailInput::PositionFile.new(@file, false, {}, **{logger: $log}).load
124
138
 
125
139
  @file.seek(0)
126
140
  lines = @file.readlines
@@ -131,9 +145,10 @@ class IntailPositionFileTest < Test::Unit::TestCase
131
145
  sub_test_case '#[]' do
132
146
  test 'return entry' do
133
147
  write_data(@file, TEST_CONTENT)
134
- pf = Fluent::Plugin::TailInput::PositionFile.load(@file, logger: $log)
148
+ pf = Fluent::Plugin::TailInput::PositionFile.load(@file, false, {}, **{logger: $log})
135
149
 
136
- f = pf['valid_path']
150
+ valid_target_info = Fluent::Plugin::TailInput::TargetInfo.new('valid_path', Fluent::FileWrapper.stat(@file).ino)
151
+ f = pf[valid_target_info]
137
152
  assert_equal Fluent::Plugin::TailInput::FilePositionEntry, f.class
138
153
  assert_equal 2, f.read_pos
139
154
  assert_equal 1, f.read_inode
@@ -142,7 +157,8 @@ class IntailPositionFileTest < Test::Unit::TestCase
142
157
  lines = @file.readlines
143
158
  assert_equal 2, lines.size
144
159
 
145
- f = pf['nonexist_path']
160
+ nonexistent_target_info = Fluent::Plugin::TailInput::TargetInfo.new('nonexist_path', -1)
161
+ f = pf[nonexistent_target_info]
146
162
  assert_equal Fluent::Plugin::TailInput::FilePositionEntry, f.class
147
163
  assert_equal 0, f.read_pos
148
164
  assert_equal 0, f.read_inode
@@ -155,19 +171,19 @@ class IntailPositionFileTest < Test::Unit::TestCase
155
171
 
156
172
  test 'does not change other value position if other entry try to write' do
157
173
  write_data(@file, TEST_CONTENT)
158
- pf = Fluent::Plugin::TailInput::PositionFile.load(@file, logger: $log)
174
+ pf = Fluent::Plugin::TailInput::PositionFile.load(@file, false, {}, logger: $log)
159
175
 
160
- f = pf['nonexist_path']
176
+ f = pf[Fluent::Plugin::TailInput::TargetInfo.new('nonexist_path', -1)]
161
177
  assert_equal 0, f.read_inode
162
178
  assert_equal 0, f.read_pos
163
179
 
164
- pf['valid_path'].update(1, 2)
180
+ pf[Fluent::Plugin::TailInput::TargetInfo.new('valid_path', Fluent::FileWrapper.stat(@file).ino)].update(1, 2)
165
181
 
166
- f = pf['nonexist_path']
182
+ f = pf[Fluent::Plugin::TailInput::TargetInfo.new('nonexist_path', -1)]
167
183
  assert_equal 0, f.read_inode
168
184
  assert_equal 0, f.read_pos
169
185
 
170
- pf['nonexist_path'].update(1, 2)
186
+ pf[Fluent::Plugin::TailInput::TargetInfo.new('nonexist_path', -1)].update(1, 2)
171
187
  assert_equal 1, f.read_inode
172
188
  assert_equal 2, f.read_pos
173
189
  end
@@ -176,14 +192,18 @@ class IntailPositionFileTest < Test::Unit::TestCase
176
192
  sub_test_case '#unwatch' do
177
193
  test 'deletes entry by path' do
178
194
  write_data(@file, TEST_CONTENT)
179
- pf = Fluent::Plugin::TailInput::PositionFile.load(@file, logger: $log)
180
- p1 = pf['valid_path']
195
+ pf = Fluent::Plugin::TailInput::PositionFile.load(@file, false, {}, logger: $log)
196
+ inode1 = Fluent::FileWrapper.stat(@file).ino
197
+ target_info1 = Fluent::Plugin::TailInput::TargetInfo.new('valid_path', inode1)
198
+ p1 = pf[target_info1]
181
199
  assert_equal Fluent::Plugin::TailInput::FilePositionEntry, p1.class
182
200
 
183
- pf.unwatch('valid_path')
201
+ pf.unwatch(target_info1)
184
202
  assert_equal p1.read_pos, Fluent::Plugin::TailInput::PositionFile::UNWATCHED_POSITION
185
203
 
186
- p2 = pf['valid_path']
204
+ inode2 = Fluent::FileWrapper.stat(@file).ino
205
+ target_info2 = Fluent::Plugin::TailInput::TargetInfo.new('valid_path', inode2)
206
+ p2 = pf[target_info2]
187
207
  assert_equal Fluent::Plugin::TailInput::FilePositionEntry, p2.class
188
208
 
189
209
  assert_not_equal p1, p2
@@ -46,6 +46,7 @@ class HttpInputTest < Test::Unit::TestCase
46
46
  assert_equal 10*1024*1024, d.instance.body_size_limit
47
47
  assert_equal 5, d.instance.keepalive_timeout
48
48
  assert_equal false, d.instance.add_http_headers
49
+ assert_equal false, d.instance.add_query_params
49
50
  end
50
51
 
51
52
  def test_time
@@ -907,6 +908,30 @@ class HttpInputTest < Test::Unit::TestCase
907
908
  assert_equal ["403", "403"], res_codes
908
909
  end
909
910
 
911
+ def test_add_query_params
912
+ d = create_driver(CONFIG + "add_query_params true")
913
+ assert_equal true, d.instance.add_query_params
914
+
915
+ time = event_time("2011-01-02 13:14:15 UTC")
916
+ time_i = time.to_i
917
+ events = [
918
+ ["tag1", time, {"a"=>1, "QUERY_A"=>"b"}],
919
+ ["tag2", time, {"a"=>2, "QUERY_A"=>"b"}],
920
+ ]
921
+ res_codes = []
922
+ res_bodies = []
923
+
924
+ d.run do
925
+ events.each do |tag, _t, record|
926
+ res = post("/#{tag}?a=b", {"json"=>record.to_json, "time"=>time_i.to_s})
927
+ res_codes << res.code
928
+ end
929
+ end
930
+ assert_equal ["200", "200"], res_codes
931
+ assert_equal [], res_bodies
932
+ assert_equal events, d.events
933
+ end
934
+
910
935
  $test_in_http_connection_object_ids = []
911
936
  $test_in_http_content_types = []
912
937
  $test_in_http_content_types_flag = false
@@ -17,29 +17,62 @@ class TailInputTest < Test::Unit::TestCase
17
17
 
18
18
  def teardown
19
19
  super
20
+ cleanup_directory(TMP_DIR)
20
21
  Fluent::Engine.stop
21
22
  end
22
23
 
23
24
  def cleanup_directory(path)
24
- FileUtils.rm_rf(path, secure: true)
25
+ begin
26
+ FileUtils.rm_f(path, secure: true)
27
+ rescue ArgumentError
28
+ FileUtils.rm_f(path) # For Ruby 2.6 or before.
29
+ end
25
30
  if File.exist?(path)
26
31
  FileUtils.remove_entry_secure(path, true)
27
32
  end
28
33
  FileUtils.mkdir_p(path)
29
34
  end
30
35
 
36
+ def cleanup_file(path)
37
+ begin
38
+ FileUtils.rm_f(path, secure: true)
39
+ rescue ArgumentError
40
+ FileUtils.rm_f(path) # For Ruby 2.6 or before.
41
+ end
42
+ if File.exist?(path)
43
+ # ensure files are closed for Windows, on which deleted files
44
+ # are still visible from filesystem
45
+ GC.start(full_mark: true, immediate_mark: true, immediate_sweep: true)
46
+ FileUtils.remove_entry_secure(path, true)
47
+ end
48
+ end
49
+
50
+ def create_target_info(path)
51
+ Fluent::Plugin::TailInput::TargetInfo.new(path, Fluent::FileWrapper.stat(path).ino)
52
+ end
53
+
31
54
  TMP_DIR = File.dirname(__FILE__) + "/../tmp/tail#{ENV['TEST_ENV_NUMBER']}"
32
55
 
33
56
  CONFIG = config_element("ROOT", "", {
34
57
  "path" => "#{TMP_DIR}/tail.txt",
35
58
  "tag" => "t1",
36
- "rotate_wait" => "2s"
59
+ "rotate_wait" => "2s",
60
+ "refresh_interval" => "1s"
37
61
  })
38
62
  COMMON_CONFIG = CONFIG + config_element("", "", { "pos_file" => "#{TMP_DIR}/tail.pos" })
39
63
  CONFIG_READ_FROM_HEAD = config_element("", "", { "read_from_head" => true })
40
64
  CONFIG_ENABLE_WATCH_TIMER = config_element("", "", { "enable_watch_timer" => false })
41
65
  CONFIG_DISABLE_STAT_WATCHER = config_element("", "", { "enable_stat_watcher" => false })
42
66
  CONFIG_OPEN_ON_EVERY_UPDATE = config_element("", "", { "open_on_every_update" => true })
67
+ COMMON_FOLLOW_INODE_CONFIG = config_element("ROOT", "", {
68
+ "path" => "#{TMP_DIR}/tail.txt*",
69
+ "pos_file" => "#{TMP_DIR}/tail.pos",
70
+ "tag" => "t1",
71
+ "refresh_interval" => "1s",
72
+ "read_from_head" => "true",
73
+ "format" => "none",
74
+ "follow_inodes" => "true"
75
+ })
43
76
  SINGLE_LINE_CONFIG = config_element("", "", { "format" => "/(?<message>.*)/" })
44
77
  PARSE_SINGLE_LINE_CONFIG = config_element("", "", {}, [config_element("parse", "", { "@type" => "/(?<message>.*)/" })])
45
78
  MULTILINE_CONFIG = config_element(
@@ -118,6 +151,12 @@ class TailInputTest < Test::Unit::TestCase
118
151
  end
119
152
  end
120
153
 
154
+ test "follow_inodes w/o pos file" do
155
+ assert_raise(Fluent::ConfigError) do
156
+ create_driver(CONFIG + config_element('', '', {'follow_inodes' => 'true'}))
157
+ end
158
+ end
159
+
121
160
  test "both enable_watch_timer and enable_stat_watcher are false" do
122
161
  assert_raise(Fluent::ConfigError) do
123
162
  create_driver(CONFIG_ENABLE_WATCH_TIMER + CONFIG_DISABLE_STAT_WATCHER + PARSE_SINGLE_LINE_CONFIG)
@@ -1002,6 +1041,7 @@ class TailInputTest < Test::Unit::TestCase
1002
1041
  # * path test
1003
1042
  # TODO: Clean up tests
1004
1043
  EX_ROTATE_WAIT = 0
1044
+ EX_FOLLOW_INODES = false
1005
1045
 
1006
1046
  EX_CONFIG = config_element("", "", {
1007
1047
  "tag" => "tail",
@@ -1011,38 +1051,43 @@ class TailInputTest < Test::Unit::TestCase
1011
1051
  "read_from_head" => true,
1012
1052
  "refresh_interval" => 30,
1013
1053
  "rotate_wait" => "#{EX_ROTATE_WAIT}s",
1054
+ "follow_inodes" => "#{EX_FOLLOW_INODES}",
1014
1055
  })
1015
- EX_PATHS = [
1016
- 'test/plugin/data/2010/01/20100102-030405.log',
1017
- 'test/plugin/data/log/foo/bar.log',
1018
- 'test/plugin/data/log/test.log'
1019
- ]
1020
-
1021
1056
  def test_expand_paths
1057
+ ex_paths = [
1058
+ create_target_info('test/plugin/data/2010/01/20100102-030405.log'),
1059
+ create_target_info('test/plugin/data/log/foo/bar.log'),
1060
+ create_target_info('test/plugin/data/log/test.log')
1061
+ ]
1022
1062
  plugin = create_driver(EX_CONFIG, false).instance
1023
1063
  flexstub(Time) do |timeclass|
1024
1064
  timeclass.should_receive(:now).with_no_args.and_return(Time.new(2010, 1, 2, 3, 4, 5))
1025
- assert_equal EX_PATHS, plugin.expand_paths.sort
1065
+ assert_equal ex_paths, plugin.expand_paths.values.sort_by { |path_ino| path_ino.path }
1026
1066
  end
1027
1067
 
1028
1068
  # Test exclusion
1029
- exclude_config = EX_CONFIG + config_element("", "", { "exclude_path" => %Q(["#{EX_PATHS.last}"]) })
1069
+ exclude_config = EX_CONFIG + config_element("", "", { "exclude_path" => %Q(["#{ex_paths.last.path}"]) })
1030
1070
  plugin = create_driver(exclude_config, false).instance
1031
- assert_equal EX_PATHS - [EX_PATHS.last], plugin.expand_paths.sort
1071
+ assert_equal ex_paths - [ex_paths.last], plugin.expand_paths.values.sort_by { |path_ino| path_ino.path }
1032
1072
  end
1033
1073
 
1034
1074
  def test_expand_paths_with_duplicate_configuration
1035
1075
  expanded_paths = [
1036
- 'test/plugin/data/log/foo/bar.log',
1037
- 'test/plugin/data/log/test.log'
1076
+ create_target_info('test/plugin/data/log/foo/bar.log'),
1077
+ create_target_info('test/plugin/data/log/test.log')
1038
1078
  ]
1039
1079
  duplicate_config = EX_CONFIG.dup
1040
1080
  duplicate_config["path"]="test/plugin/data/log/**/*.log, test/plugin/data/log/**/*.log"
1041
1081
  plugin = create_driver(EX_CONFIG, false).instance
1042
- assert_equal expanded_paths, plugin.expand_paths.sort
1082
+ assert_equal expanded_paths, plugin.expand_paths.values.sort_by { |path_ino| path_ino.path }
1043
1083
  end
1044
1084
 
1045
1085
  def test_expand_paths_with_timezone
1086
+ ex_paths = [
1087
+ create_target_info('test/plugin/data/2010/01/20100102-030405.log'),
1088
+ create_target_info('test/plugin/data/log/foo/bar.log'),
1089
+ create_target_info('test/plugin/data/log/test.log')
1090
+ ]
1046
1091
  ['Asia/Taipei', '+08'].each do |tz_type|
1047
1092
  taipei_config = EX_CONFIG + config_element("", "", {"path_timezone" => tz_type})
1048
1093
  plugin = create_driver(taipei_config, false).instance
@@ -1056,8 +1101,8 @@ class TailInputTest < Test::Unit::TestCase
1056
1101
  # env : 2010-01-01 19:04:05 (UTC), tail path : 2010-01-02 03:04:05 (Asia/Taipei)
1057
1102
  timeclass.should_receive(:now).with_no_args.and_return(Time.new(2010, 1, 1, 19, 4, 5))
1058
1103
 
1059
- assert_equal EX_PATHS, plugin.expand_paths.sort
1060
- assert_equal EX_PATHS - [EX_PATHS.first], exclude_plugin.expand_paths.sort
1104
+ assert_equal ex_paths, plugin.expand_paths.values.sort_by { |path_ino| path_ino.path }
1105
+ assert_equal ex_paths - [ex_paths.first], exclude_plugin.expand_paths.values.sort_by { |path_ino| path_ino.path }
1061
1106
  end
1062
1107
  end
1063
1108
  end
@@ -1065,10 +1110,10 @@ class TailInputTest < Test::Unit::TestCase
1065
1110
 
1066
1111
  def test_log_file_without_extension
1067
1112
  expected_files = [
1068
- 'test/plugin/data/log/bar',
1069
- 'test/plugin/data/log/foo/bar.log',
1070
- 'test/plugin/data/log/foo/bar2',
1071
- 'test/plugin/data/log/test.log'
1113
+ create_target_info('test/plugin/data/log/bar'),
1114
+ create_target_info('test/plugin/data/log/foo/bar.log'),
1115
+ create_target_info('test/plugin/data/log/foo/bar2'),
1116
+ create_target_info('test/plugin/data/log/test.log')
1072
1117
  ]
1073
1118
 
1074
1119
  config = config_element("", "", {
@@ -1079,7 +1124,7 @@ class TailInputTest < Test::Unit::TestCase
1079
1124
  })
1080
1125
 
1081
1126
  plugin = create_driver(config, false).instance
1082
- assert_equal expected_files, plugin.expand_paths.sort
1127
+ assert_equal expected_files, plugin.expand_paths.values.sort_by { |path_ino| path_ino.path }
1083
1128
  end
1084
1129
 
1085
1130
  def test_unwatched_files_should_be_removed
@@ -1205,25 +1250,35 @@ class TailInputTest < Test::Unit::TestCase
1205
1250
  end
1206
1251
 
1207
1252
  def test_z_refresh_watchers
1253
+ ex_paths = [
1254
+ create_target_info('test/plugin/data/2010/01/20100102-030405.log'),
1255
+ create_target_info('test/plugin/data/log/foo/bar.log'),
1256
+ create_target_info('test/plugin/data/log/test.log'),
1257
+ ]
1208
1258
  plugin = create_driver(EX_CONFIG, false).instance
1209
1259
  sio = StringIO.new
1210
1260
  plugin.instance_eval do
1211
- @pf = Fluent::Plugin::TailInput::PositionFile.load(sio, logger: $log)
1261
+ @pf = Fluent::Plugin::TailInput::PositionFile.load(sio, EX_FOLLOW_INODES, {}, logger: $log)
1212
1262
  @loop = Coolio::Loop.new
1213
1263
  end
1214
1264
 
1215
1265
  Timecop.freeze(2010, 1, 2, 3, 4, 5) do
1216
- EX_PATHS.each do |path|
1217
- mock.proxy(Fluent::Plugin::TailInput::TailWatcher).new(path, anything, anything, true, anything, nil, anything).once
1266
+ ex_paths.each do |target_info|
1267
+ mock.proxy(Fluent::Plugin::TailInput::TailWatcher).new(target_info, anything, anything, true, false, anything, nil, anything).once
1218
1268
  end
1219
1269
 
1220
1270
  plugin.refresh_watchers
1221
1271
  end
1222
1272
 
1223
- mock.proxy(plugin).detach_watcher_after_rotate_wait(plugin.instance_variable_get(:@tails)['test/plugin/data/2010/01/20100102-030405.log'])
1273
+ path = 'test/plugin/data/2010/01/20100102-030405.log'
1274
+ target_info = Fluent::Plugin::TailInput::TargetInfo.new(path, Fluent::FileWrapper.stat(path).ino)
1275
+ mock.proxy(plugin).detach_watcher_after_rotate_wait(plugin.instance_variable_get(:@tails)[target_info], target_info.ino)
1224
1276
 
1225
1277
  Timecop.freeze(2010, 1, 2, 3, 4, 6) do
1226
- mock.proxy(Fluent::Plugin::TailInput::TailWatcher).new('test/plugin/data/2010/01/20100102-030406.log', anything, anything, true, anything, nil, anything).once
1278
+ path = "test/plugin/data/2010/01/20100102-030406.log"
1279
+ inode = Fluent::FileWrapper.stat(path).ino
1280
+ target_info = Fluent::Plugin::TailInput::TargetInfo.new(path, inode)
1281
+ mock.proxy(Fluent::Plugin::TailInput::TailWatcher).new(target_info, anything, anything, true, false, anything, nil, anything).once
1227
1282
  plugin.refresh_watchers
1228
1283
 
1229
1284
  flexstub(Fluent::Plugin::TailInput::TailWatcher) do |watcherclass|
@@ -1369,6 +1424,305 @@ class TailInputTest < Test::Unit::TestCase
1369
1424
  end
1370
1425
  end
1371
1426
 
1427
+ sub_test_case 'inode_processing' do
1428
+ def test_should_delete_file_pos_entry_for_non_existing_file_with_follow_inodes
1429
+ config = COMMON_FOLLOW_INODE_CONFIG
1430
+
1431
+ path = "#{TMP_DIR}/tail.txt"
1432
+ ino = 1
1433
+ pos = 1234
1434
+ File.open("#{TMP_DIR}/tail.pos", "wb") {|f|
1435
+ f.puts ("%s\t%016x\t%016x\n" % [path, pos, ino])
1436
+ }
1437
+
1438
+ d = create_driver(config, false)
1439
+ d.run
1440
+
1441
+ pos_file = File.open("#{TMP_DIR}/tail.pos", "r")
1442
+ pos_file.pos = 0
1443
+
1444
+ assert_raise(EOFError) do
1445
+ pos_file.readline
1446
+ end
1447
+ end
1448
+
1449
+ def test_should_write_latest_offset_after_rotate_wait
1450
+ config = COMMON_FOLLOW_INODE_CONFIG
1451
+ File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
1452
+ f.puts "test1"
1453
+ f.puts "test2"
1454
+ }
1455
+
1456
+ d = create_driver(config, false)
1457
+ d.run(expect_emits: 2, shutdown: false) do
1458
+ File.open("#{TMP_DIR}/tail.txt", "ab") {|f| f.puts "test3\n"}
1459
+ FileUtils.move("#{TMP_DIR}/tail.txt", "#{TMP_DIR}/tail.txt" + "1")
1460
+ sleep 1
1461
+ File.open("#{TMP_DIR}/tail.txt" + "1", "ab") {|f| f.puts "test4\n"}
1462
+ end
1463
+
1464
+ pos_file = File.open("#{TMP_DIR}/tail.pos", "r")
1465
+ pos_file.pos = 0
1466
+ line_parts = /^([^\t]+)\t([0-9a-fA-F]+)\t([0-9a-fA-F]+)/.match(pos_file.readline)
1467
+ waiting(5) {
1468
+ while line_parts[2].to_i(16) != 24
1469
+ sleep(0.1)
1470
+ pos_file.pos = 0
1471
+ line_parts = /^([^\t]+)\t([0-9a-fA-F]+)\t([0-9a-fA-F]+)/.match(pos_file.readline)
1472
+ end
1473
+ }
1474
+ assert_equal(24, line_parts[2].to_i(16))
1475
+ d.instance_shutdown
1476
+ end
1477
+
1478
+ def test_should_keep_and_update_existing_file_pos_entry_for_deleted_file_when_new_file_with_same_name_created
1479
+ config = config_element("", "", {"format" => "none"})
1480
+
1481
+ path = "#{TMP_DIR}/tail.txt"
1482
+ ino = 1
1483
+ pos = 1234
1484
+ File.open("#{TMP_DIR}/tail.pos", "wb") {|f|
1485
+ f.puts ("%s\t%016x\t%016x\n" % [path, pos, ino])
1486
+ }
1487
+
1488
+ d = create_driver(config)
1489
+ d.run(shutdown: false)
1490
+
1491
+ pos_file = File.open("#{TMP_DIR}/tail.pos", "r")
1492
+ pos_file.pos = 0
1493
+
1494
+ path_pos_ino = /^([^\t]+)\t([0-9a-fA-F]+)\t([0-9a-fA-F]+)/.match(pos_file.readline)
1495
+ assert_equal(path, path_pos_ino[1])
1496
+ assert_equal(pos, path_pos_ino[2].to_i(16))
1497
+ assert_equal(ino, path_pos_ino[3].to_i(16))
1498
+
1499
+ File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
1500
+ f.puts "test1"
1501
+ f.puts "test2"
1502
+ }
1503
+ Timecop.travel(Time.now + 10) do
1504
+ sleep 5
1505
+ pos_file.pos = 0
1506
+ tuple = create_target_info("#{TMP_DIR}/tail.txt")
1507
+ path_pos_ino = /^([^\t]+)\t([0-9a-fA-F]+)\t([0-9a-fA-F]+)/.match(pos_file.readline)
1508
+ assert_equal(tuple.path, path_pos_ino[1])
1509
+ assert_equal(12, path_pos_ino[2].to_i(16))
1510
+ assert_equal(tuple.ino, path_pos_ino[3].to_i(16))
1511
+ end
1512
+ d.instance_shutdown
1513
+ end
1514
+
1515
+ def test_should_mark_file_unwatched_after_limit_recently_modified_and_rotate_wait
1516
+ config = config_element("ROOT", "", {
1517
+ "path" => "#{TMP_DIR}/tail.txt*",
1518
+ "pos_file" => "#{TMP_DIR}/tail.pos",
1519
+ "tag" => "t1",
1520
+ "rotate_wait" => "1s",
1521
+ "refresh_interval" => "1s",
1522
+ "limit_recently_modified" => "1s",
1523
+ "read_from_head" => "true",
1524
+ "format" => "none",
1525
+ "follow_inodes" => "true",
1526
+ })
1527
+
1528
+ d = create_driver(config, false)
1529
+
1530
+ File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
1531
+ f.puts "test1"
1532
+ f.puts "test2"
1533
+ }
1534
+ target_info = create_target_info("#{TMP_DIR}/tail.txt")
1535
+
1536
+ d.run(expect_emits: 1, shutdown: false) do
1537
+ File.open("#{TMP_DIR}/tail.txt", "ab") {|f| f.puts "test3\n"}
1538
+ end
1539
+
1540
+
1541
+ Timecop.travel(Time.now + 10) do
1542
+ waiting(5) {
1543
+ # @pos will be reset as 0 when UNWATCHED_POSITION is specified.
1544
+ sleep 0.1 until d.instance.instance_variable_get(:@pf)[target_info].read_pos == 0
1545
+ }
1546
+ end
1547
+
1548
+ assert_equal(0, d.instance.instance_variable_get(:@pf)[target_info].read_pos)
1549
+
1550
+ d.instance_shutdown
1551
+ end
1552
+
1553
+ def test_should_read_from_head_on_file_renaming_with_star_in_pattern
1554
+ config = config_element("ROOT", "", {
1555
+ "path" => "#{TMP_DIR}/tail.txt*",
1556
+ "pos_file" => "#{TMP_DIR}/tail.pos",
1557
+ "tag" => "t1",
1558
+ "rotate_wait" => "10s",
1559
+ "refresh_interval" => "1s",
1560
+ "limit_recently_modified" => "60s",
1561
+ "read_from_head" => "true",
1562
+ "format" => "none",
1563
+ "follow_inodes" => "true"
1564
+ })
1565
+
1566
+ d = create_driver(config, false)
1567
+
1568
+ File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
1569
+ f.puts "test1"
1570
+ f.puts "test2"
1571
+ }
1572
+
1573
+ d.run(expect_emits: 2, shutdown: false) do
1574
+ File.open("#{TMP_DIR}/tail.txt", "ab") {|f| f.puts "test3\n"}
1575
+ FileUtils.move("#{TMP_DIR}/tail.txt", "#{TMP_DIR}/tail.txt1")
1576
+ end
1577
+
1578
+ events = d.events
1579
+ assert_equal(3, events.length)
1580
+ d.instance_shutdown
1581
+ end
1582
+
1583
+ def test_should_not_read_from_head_on_rotation_when_watching_inodes
1584
+ config = COMMON_FOLLOW_INODE_CONFIG
1585
+
1586
+ d = create_driver(config, false)
1587
+
1588
+ File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
1589
+ f.puts "test1"
1590
+ f.puts "test2"
1591
+ }
1592
+
1593
+ d.run(expect_emits: 1, shutdown: false) do
1594
+ File.open("#{TMP_DIR}/tail.txt", "ab") {|f| f.puts "test3\n"}
1595
+ end
1596
+
1597
+ FileUtils.move("#{TMP_DIR}/tail.txt", "#{TMP_DIR}/tail.txt1")
1598
+ Timecop.travel(Time.now + 10) do
1599
+ sleep 2
1600
+ events = d.events
1601
+ assert_equal(3, events.length)
1602
+ end
1603
+
1604
+ d.instance_shutdown
1605
+ end
1606
+
1607
+ def test_should_mark_file_unwatched_if_same_name_file_created_with_different_inode
1608
+ config = COMMON_FOLLOW_INODE_CONFIG
1609
+
1610
+ d = create_driver(config, false)
1611
+
1612
+ File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
1613
+ f.puts "test1"
1614
+ f.puts "test2"
1615
+ }
1616
+ target_info = create_target_info("#{TMP_DIR}/tail.txt")
1617
+
1618
+ d.run(expect_emits: 2, shutdown: false) do
1619
+ File.open("#{TMP_DIR}/tail.txt", "ab") {|f| f.puts "test3\n"}
1620
+ cleanup_file("#{TMP_DIR}/tail.txt")
1621
+ File.open("#{TMP_DIR}/tail.txt", "wb") {|f| f.puts "test4\n"}
1622
+ end
1623
+
1624
+ new_target_info = create_target_info("#{TMP_DIR}/tail.txt")
1625
+
1626
+ pos_file = d.instance.instance_variable_get(:@pf)
1627
+
1628
+ waiting(10) {
1629
+ # @pos will be reset as 0 when UNWATCHED_POSITION is specified.
1630
+ sleep 0.1 until pos_file[target_info].read_pos == 0
1631
+ }
1632
+ new_position = pos_file[new_target_info].read_pos
1633
+ assert_equal(6, new_position)
1634
+
1635
+ d.instance_shutdown
1636
+ end
1637
+
1638
+ def test_should_close_watcher_after_rotate_wait
1639
+ now = Time.now
1640
+ config = COMMON_FOLLOW_INODE_CONFIG + config_element('', '', {"rotate_wait" => "1s", "limit_recently_modified" => "1s"})
1641
+
1642
+ d = create_driver(config, false)
1643
+
1644
+ File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
1645
+ f.puts "test1"
1646
+ f.puts "test2"
1647
+ }
1648
+ target_info = create_target_info("#{TMP_DIR}/tail.txt")
1649
+ mock.proxy(Fluent::Plugin::TailInput::TailWatcher).new(target_info, anything, anything, true, true, anything, nil, anything).once
1650
+ d.run(shutdown: false)
1651
+ assert d.instance.instance_variable_get(:@tails)[target_info]
1652
+
1653
+ Timecop.travel(now + 10) do
1654
+ d.instance.instance_eval do
1655
+ sleep 0.1 until @tails[target_info] == nil
1656
+ end
1657
+ assert_nil d.instance.instance_variable_get(:@tails)[target_info]
1658
+ end
1659
+ d.instance_shutdown
1660
+ end
1661
+
1662
+ def test_should_create_new_watcher_for_new_file_with_same_name
1663
+ now = Time.now
1664
+ config = COMMON_FOLLOW_INODE_CONFIG + config_element('', '', {"limit_recently_modified" => "2s"})
1665
+
1666
+ d = create_driver(config, false)
1667
+
1668
+ File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
1669
+ f.puts "test1"
1670
+ f.puts "test2"
1671
+ }
1672
+ path_ino = create_target_info("#{TMP_DIR}/tail.txt")
1673
+
1674
+ d.run(expect_emits: 1, shutdown: false) do
1675
+ File.open("#{TMP_DIR}/tail.txt", "ab") {|f| f.puts "test3\n"}
1676
+ end
1677
+
1678
+ cleanup_file("#{TMP_DIR}/tail.txt")
1679
+ File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
1680
+ f.puts "test3"
1681
+ f.puts "test4"
1682
+ }
1683
+ new_path_ino = create_target_info("#{TMP_DIR}/tail.txt")
1684
+
1685
+ Timecop.travel(now + 10) do
1686
+ sleep 3
1687
+ d.instance.instance_eval do
1688
+ @tails[path_ino] == nil
1689
+ @tails[new_path_ino] != nil
1690
+ end
1691
+ end
1692
+
1693
+ events = d.events
1694
+
1695
+ assert_equal(5, events.length)
1696
+
1697
+ d.instance_shutdown
1698
+ end
1699
+
1700
+ def test_truncate_file_with_follow_inodes
1701
+ config = COMMON_FOLLOW_INODE_CONFIG
1702
+
1703
+ d = create_driver(config, false)
1704
+
1705
+ File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
1706
+ f.puts "test1"
1707
+ f.puts "test2"
1708
+ }
1709
+
1710
+ d.run(expect_emits: 3, shutdown: false) do
1711
+ File.open("#{TMP_DIR}/tail.txt", "ab") {|f| f.puts "test3\n"}
1712
+ sleep 2
1713
+ File.open("#{TMP_DIR}/tail.txt", "w+b") {|f| f.puts "test4\n"}
1714
+ end
1715
+
1716
+ events = d.events
1717
+ assert_equal(4, events.length)
1718
+ assert_equal({"message" => "test1"}, events[0][2])
1719
+ assert_equal({"message" => "test2"}, events[1][2])
1720
+ assert_equal({"message" => "test3"}, events[2][2])
1721
+ assert_equal({"message" => "test4"}, events[3][2])
1722
+ d.instance_shutdown
1723
+ end
1724
+ end
1725
+
1372
1726
  sub_test_case "tail_path" do
1373
1727
  def test_tail_path_with_singleline
1374
1728
  File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
@@ -1498,13 +1852,13 @@ class TailInputTest < Test::Unit::TestCase
1498
1852
  })
1499
1853
 
1500
1854
  expected_files = [
1501
- "#{TMP_DIR}/tail_watch1.txt",
1502
- "#{TMP_DIR}/tail_watch2.txt"
1855
+ create_target_info("#{TMP_DIR}/tail_watch1.txt"),
1856
+ create_target_info("#{TMP_DIR}/tail_watch2.txt")
1503
1857
  ]
1504
1858
 
1505
1859
  Timecop.freeze(now) do
1506
1860
  plugin = create_driver(config, false).instance
1507
- assert_equal expected_files, plugin.expand_paths.sort
1861
+ assert_equal expected_files, plugin.expand_paths.values.sort_by { |path_ino| path_ino.path }
1508
1862
  end
1509
1863
  end
1510
1864