fluentd 1.13.3 → 1.16.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/{bug_report.yaml → bug_report.yml} +2 -0
  3. data/.github/ISSUE_TEMPLATE/config.yml +2 -2
  4. data/.github/ISSUE_TEMPLATE/{feature_request.yaml → feature_request.yml} +1 -0
  5. data/.github/workflows/stale-actions.yml +11 -9
  6. data/.github/workflows/test.yml +32 -0
  7. data/CHANGELOG.md +490 -10
  8. data/CONTRIBUTING.md +2 -2
  9. data/MAINTAINERS.md +7 -5
  10. data/README.md +3 -23
  11. data/Rakefile +1 -1
  12. data/SECURITY.md +14 -0
  13. data/fluentd.gemspec +7 -8
  14. data/lib/fluent/command/cat.rb +13 -3
  15. data/lib/fluent/command/ctl.rb +6 -3
  16. data/lib/fluent/command/fluentd.rb +73 -65
  17. data/lib/fluent/command/plugin_config_formatter.rb +1 -1
  18. data/lib/fluent/compat/output.rb +9 -6
  19. data/lib/fluent/config/dsl.rb +1 -1
  20. data/lib/fluent/config/error.rb +12 -0
  21. data/lib/fluent/config/literal_parser.rb +2 -2
  22. data/lib/fluent/config/parser.rb +1 -1
  23. data/lib/fluent/config/v1_parser.rb +3 -3
  24. data/lib/fluent/config/yaml_parser/fluent_value.rb +47 -0
  25. data/lib/fluent/config/yaml_parser/loader.rb +108 -0
  26. data/lib/fluent/config/yaml_parser/parser.rb +166 -0
  27. data/lib/fluent/config/yaml_parser/section_builder.rb +107 -0
  28. data/lib/fluent/config/yaml_parser.rb +56 -0
  29. data/lib/fluent/config.rb +14 -1
  30. data/lib/fluent/counter/server.rb +1 -1
  31. data/lib/fluent/counter/validator.rb +3 -3
  32. data/lib/fluent/daemon.rb +2 -4
  33. data/lib/fluent/engine.rb +1 -1
  34. data/lib/fluent/env.rb +4 -0
  35. data/lib/fluent/error.rb +3 -0
  36. data/lib/fluent/event.rb +8 -4
  37. data/lib/fluent/event_router.rb +47 -2
  38. data/lib/fluent/file_wrapper.rb +137 -0
  39. data/lib/fluent/log/console_adapter.rb +66 -0
  40. data/lib/fluent/log.rb +44 -5
  41. data/lib/fluent/match.rb +1 -1
  42. data/lib/fluent/msgpack_factory.rb +6 -1
  43. data/lib/fluent/oj_options.rb +1 -2
  44. data/lib/fluent/plugin/bare_output.rb +49 -8
  45. data/lib/fluent/plugin/base.rb +26 -9
  46. data/lib/fluent/plugin/buf_file.rb +34 -5
  47. data/lib/fluent/plugin/buf_file_single.rb +32 -3
  48. data/lib/fluent/plugin/buffer/file_chunk.rb +1 -1
  49. data/lib/fluent/plugin/buffer.rb +216 -70
  50. data/lib/fluent/plugin/filter.rb +35 -1
  51. data/lib/fluent/plugin/filter_record_transformer.rb +1 -1
  52. data/lib/fluent/plugin/in_forward.rb +2 -2
  53. data/lib/fluent/plugin/in_http.rb +39 -10
  54. data/lib/fluent/plugin/in_monitor_agent.rb +4 -2
  55. data/lib/fluent/plugin/in_sample.rb +1 -1
  56. data/lib/fluent/plugin/in_syslog.rb +13 -1
  57. data/lib/fluent/plugin/in_tail/group_watch.rb +204 -0
  58. data/lib/fluent/plugin/in_tail/position_file.rb +33 -33
  59. data/lib/fluent/plugin/in_tail.rb +216 -84
  60. data/lib/fluent/plugin/in_tcp.rb +47 -2
  61. data/lib/fluent/plugin/input.rb +39 -1
  62. data/lib/fluent/plugin/metrics.rb +119 -0
  63. data/lib/fluent/plugin/metrics_local.rb +96 -0
  64. data/lib/fluent/plugin/multi_output.rb +43 -6
  65. data/lib/fluent/plugin/out_copy.rb +1 -1
  66. data/lib/fluent/plugin/out_exec_filter.rb +2 -2
  67. data/lib/fluent/plugin/out_file.rb +20 -2
  68. data/lib/fluent/plugin/out_forward/ack_handler.rb +19 -4
  69. data/lib/fluent/plugin/out_forward/socket_cache.rb +2 -0
  70. data/lib/fluent/plugin/out_forward.rb +17 -9
  71. data/lib/fluent/plugin/out_secondary_file.rb +39 -22
  72. data/lib/fluent/plugin/output.rb +167 -78
  73. data/lib/fluent/plugin/parser.rb +3 -4
  74. data/lib/fluent/plugin/parser_apache2.rb +1 -1
  75. data/lib/fluent/plugin/parser_json.rb +1 -1
  76. data/lib/fluent/plugin/parser_syslog.rb +1 -1
  77. data/lib/fluent/plugin/storage_local.rb +3 -5
  78. data/lib/fluent/plugin.rb +10 -1
  79. data/lib/fluent/plugin_helper/child_process.rb +3 -0
  80. data/lib/fluent/plugin_helper/event_emitter.rb +8 -1
  81. data/lib/fluent/plugin_helper/event_loop.rb +2 -2
  82. data/lib/fluent/plugin_helper/http_server/server.rb +2 -1
  83. data/lib/fluent/plugin_helper/metrics.rb +129 -0
  84. data/lib/fluent/plugin_helper/record_accessor.rb +1 -1
  85. data/lib/fluent/plugin_helper/retry_state.rb +14 -4
  86. data/lib/fluent/plugin_helper/server.rb +35 -6
  87. data/lib/fluent/plugin_helper/service_discovery.rb +2 -2
  88. data/lib/fluent/plugin_helper/socket.rb +13 -2
  89. data/lib/fluent/plugin_helper/thread.rb +3 -3
  90. data/lib/fluent/plugin_helper.rb +1 -0
  91. data/lib/fluent/plugin_id.rb +3 -2
  92. data/lib/fluent/registry.rb +2 -1
  93. data/lib/fluent/root_agent.rb +6 -0
  94. data/lib/fluent/rpc.rb +4 -3
  95. data/lib/fluent/supervisor.rb +283 -259
  96. data/lib/fluent/system_config.rb +13 -3
  97. data/lib/fluent/test/driver/base.rb +11 -5
  98. data/lib/fluent/test/driver/filter.rb +4 -0
  99. data/lib/fluent/test/startup_shutdown.rb +6 -8
  100. data/lib/fluent/time.rb +21 -20
  101. data/lib/fluent/version.rb +1 -1
  102. data/lib/fluent/win32api.rb +38 -0
  103. data/lib/fluent/winsvc.rb +5 -8
  104. data/templates/new_gem/test/helper.rb.erb +0 -1
  105. data/test/command/test_cat.rb +31 -2
  106. data/test/command/test_ctl.rb +1 -2
  107. data/test/command/test_fluentd.rb +209 -24
  108. data/test/command/test_plugin_config_formatter.rb +0 -1
  109. data/test/compat/test_parser.rb +6 -6
  110. data/test/config/test_system_config.rb +13 -11
  111. data/test/config/test_types.rb +1 -1
  112. data/test/log/test_console_adapter.rb +110 -0
  113. data/test/plugin/in_tail/test_io_handler.rb +26 -8
  114. data/test/plugin/in_tail/test_position_file.rb +48 -59
  115. data/test/plugin/out_forward/test_ack_handler.rb +39 -0
  116. data/test/plugin/out_forward/test_socket_cache.rb +26 -1
  117. data/test/plugin/test_bare_output.rb +14 -1
  118. data/test/plugin/test_base.rb +133 -1
  119. data/test/plugin/test_buf_file.rb +62 -23
  120. data/test/plugin/test_buf_file_single.rb +65 -0
  121. data/test/plugin/test_buffer.rb +267 -3
  122. data/test/plugin/test_buffer_chunk.rb +11 -0
  123. data/test/plugin/test_filter.rb +12 -1
  124. data/test/plugin/test_filter_parser.rb +1 -1
  125. data/test/plugin/test_filter_stdout.rb +2 -2
  126. data/test/plugin/test_in_forward.rb +9 -11
  127. data/test/plugin/test_in_http.rb +65 -3
  128. data/test/plugin/test_in_monitor_agent.rb +216 -11
  129. data/test/plugin/test_in_object_space.rb +9 -3
  130. data/test/plugin/test_in_syslog.rb +35 -0
  131. data/test/plugin/test_in_tail.rb +1393 -385
  132. data/test/plugin/test_in_tcp.rb +87 -2
  133. data/test/plugin/test_in_udp.rb +28 -0
  134. data/test/plugin/test_in_unix.rb +2 -2
  135. data/test/plugin/test_input.rb +12 -1
  136. data/test/plugin/test_metrics.rb +294 -0
  137. data/test/plugin/test_metrics_local.rb +96 -0
  138. data/test/plugin/test_multi_output.rb +25 -1
  139. data/test/plugin/test_out_exec.rb +6 -4
  140. data/test/plugin/test_out_exec_filter.rb +6 -2
  141. data/test/plugin/test_out_file.rb +34 -17
  142. data/test/plugin/test_out_forward.rb +78 -77
  143. data/test/plugin/test_out_http.rb +1 -0
  144. data/test/plugin/test_out_stdout.rb +2 -2
  145. data/test/plugin/test_output.rb +297 -12
  146. data/test/plugin/test_output_as_buffered.rb +44 -44
  147. data/test/plugin/test_output_as_buffered_compress.rb +32 -18
  148. data/test/plugin/test_output_as_buffered_retries.rb +54 -7
  149. data/test/plugin/test_output_as_buffered_secondary.rb +4 -4
  150. data/test/plugin/test_parser_regexp.rb +1 -6
  151. data/test/plugin/test_parser_syslog.rb +1 -1
  152. data/test/plugin_helper/test_cert_option.rb +1 -1
  153. data/test/plugin_helper/test_child_process.rb +38 -16
  154. data/test/plugin_helper/test_event_emitter.rb +29 -0
  155. data/test/plugin_helper/test_http_server_helper.rb +1 -1
  156. data/test/plugin_helper/test_metrics.rb +137 -0
  157. data/test/plugin_helper/test_retry_state.rb +602 -38
  158. data/test/plugin_helper/test_server.rb +78 -6
  159. data/test/plugin_helper/test_timer.rb +2 -2
  160. data/test/test_config.rb +191 -24
  161. data/test/test_event_router.rb +17 -0
  162. data/test/test_file_wrapper.rb +53 -0
  163. data/test/test_formatter.rb +24 -21
  164. data/test/test_log.rb +122 -40
  165. data/test/test_msgpack_factory.rb +32 -0
  166. data/test/test_plugin_classes.rb +102 -0
  167. data/test/test_root_agent.rb +30 -1
  168. data/test/test_supervisor.rb +477 -257
  169. data/test/test_time_parser.rb +22 -0
  170. metadata +55 -34
  171. data/.drone.yml +0 -35
  172. data/.github/workflows/issue-auto-closer.yml +0 -12
  173. data/.github/workflows/linux-test.yaml +0 -36
  174. data/.github/workflows/macos-test.yaml +0 -30
  175. data/.github/workflows/windows-test.yaml +0 -46
  176. data/.gitlab-ci.yml +0 -103
  177. data/lib/fluent/plugin/file_wrapper.rb +0 -187
  178. data/test/plugin/test_file_wrapper.rb +0 -126
  179. data/test/test_logger_initializer.rb +0 -46
@@ -3,6 +3,7 @@ require 'fluent/test/driver/input'
3
3
  require 'fluent/plugin/in_tail'
4
4
  require 'fluent/plugin/buffer'
5
5
  require 'fluent/system_config'
6
+ require 'fluent/file_wrapper'
6
7
  require 'net/http'
7
8
  require 'flexmock/test_unit'
8
9
  require 'timecop'
@@ -12,15 +13,21 @@ require 'securerandom'
12
13
  class TailInputTest < Test::Unit::TestCase
13
14
  include FlexMock::TestCase
14
15
 
16
+ def tmp_dir
17
+ File.join(File.dirname(__FILE__), "..", "tmp", "tail#{ENV['TEST_ENV_NUMBER']}", SecureRandom.hex(10))
18
+ end
19
+
15
20
  def setup
16
21
  Fluent::Test.setup
17
- cleanup_directory(TMP_DIR)
22
+ @tmp_dir = tmp_dir
23
+ cleanup_directory(@tmp_dir)
18
24
  end
19
25
 
20
26
  def teardown
21
27
  super
22
- cleanup_directory(TMP_DIR)
28
+ cleanup_directory(@tmp_dir)
23
29
  Fluent::Engine.stop
30
+ Timecop.return
24
31
  end
25
32
 
26
33
  def cleanup_directory(path)
@@ -29,89 +36,48 @@ class TailInputTest < Test::Unit::TestCase
29
36
  return
30
37
  end
31
38
 
32
- if Fluent.windows?
33
- Dir.glob("*", base: path).each do |name|
34
- begin
35
- cleanup_file(File.join(path, name))
36
- rescue
37
- # expect test driver block release already owned file handle.
38
- end
39
- end
40
- else
41
- begin
42
- FileUtils.rm_f(path, secure:true)
43
- rescue ArgumentError
44
- FileUtils.rm_f(path) # For Ruby 2.6 or before.
45
- end
46
- if File.exist?(path)
47
- FileUtils.remove_entry_secure(path, true)
48
- end
49
- end
50
- FileUtils.mkdir_p(path)
39
+ FileUtils.remove_entry_secure(path, true)
51
40
  end
52
41
 
53
42
  def cleanup_file(path)
54
- if Fluent.windows?
55
- # On Windows, when the file or directory is removed and created
56
- # frequently, there is a case that creating file or directory will
57
- # fail. This situation is caused by pending file or directory
58
- # deletion which is mentioned on win32 API document [1]
59
- # As a workaround, execute rename and remove method.
60
- #
61
- # [1] https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#files
62
- #
63
- file = File.join(Dir.tmpdir, SecureRandom.hex(10))
64
- begin
65
- FileUtils.mv(path, file)
66
- FileUtils.rm_rf(file, secure: true)
67
- rescue ArgumentError
68
- FileUtils.rm_rf(file) # For Ruby 2.6 or before.
69
- end
70
- if File.exist?(file)
71
- # ensure files are closed for Windows, on which deleted files
72
- # are still visible from filesystem
73
- GC.start(full_mark: true, immediate_mark: true, immediate_sweep: true)
74
- FileUtils.remove_entry_secure(file, true)
75
- end
76
- else
77
- begin
78
- FileUtils.rm_f(path, secure: true)
79
- rescue ArgumentError
80
- FileUtils.rm_f(path) # For Ruby 2.6 or before.
81
- end
82
- if File.exist?(path)
83
- FileUtils.remove_entry_secure(path, true)
84
- end
85
- end
43
+ FileUtils.remove_entry_secure(path, true)
86
44
  end
87
45
 
88
46
  def create_target_info(path)
89
47
  Fluent::Plugin::TailInput::TargetInfo.new(path, Fluent::FileWrapper.stat(path).ino)
90
48
  end
91
49
 
92
- TMP_DIR = File.dirname(__FILE__) + "/../tmp/tail#{ENV['TEST_ENV_NUMBER']}"
93
-
94
- CONFIG = config_element("ROOT", "", {
95
- "path" => "#{TMP_DIR}/tail.txt",
50
+ ROOT_CONFIG = config_element("ROOT", "", {
96
51
  "tag" => "t1",
97
52
  "rotate_wait" => "2s",
98
53
  "refresh_interval" => "1s"
99
- })
100
- COMMON_CONFIG = CONFIG + config_element("", "", { "pos_file" => "#{TMP_DIR}/tail.pos" })
54
+ })
55
+
56
+ def base_config
57
+ ROOT_CONFIG + config_element("", "", { "path" => "#{@tmp_dir}/tail.txt" })
58
+ end
59
+
60
+ def common_config
61
+ base_config + config_element("", "", { "pos_file" => "#{@tmp_dir}/tail.pos" })
62
+ end
63
+
64
+ def common_follow_inode_config
65
+ config_element("ROOT", "", {
66
+ "path" => "#{@tmp_dir}/tail.txt*",
67
+ "pos_file" => "#{@tmp_dir}/tail.pos",
68
+ "tag" => "t1",
69
+ "refresh_interval" => "1s",
70
+ "read_from_head" => "true",
71
+ "format" => "none",
72
+ "rotate_wait" => "1s",
73
+ "follow_inodes" => "true"
74
+ })
75
+ end
76
+
101
77
  CONFIG_READ_FROM_HEAD = config_element("", "", { "read_from_head" => true })
102
- CONFIG_ENABLE_WATCH_TIMER = config_element("", "", { "enable_watch_timer" => false })
78
+ CONFIG_DISABLE_WATCH_TIMER = config_element("", "", { "enable_watch_timer" => false })
103
79
  CONFIG_DISABLE_STAT_WATCHER = config_element("", "", { "enable_stat_watcher" => false })
104
80
  CONFIG_OPEN_ON_EVERY_UPDATE = config_element("", "", { "open_on_every_update" => true })
105
- COMMON_FOLLOW_INODE_CONFIG = config_element("ROOT", "", {
106
- "path" => "#{TMP_DIR}/tail.txt*",
107
- "pos_file" => "#{TMP_DIR}/tail.pos",
108
- "tag" => "t1",
109
- "refresh_interval" => "1s",
110
- "read_from_head" => "true",
111
- "format" => "none",
112
- "rotate_wait" => "1s",
113
- "follow_inodes" => "true"
114
- })
115
81
  SINGLE_LINE_CONFIG = config_element("", "", { "format" => "/(?<message>.*)/" })
116
82
  PARSE_SINGLE_LINE_CONFIG = config_element("", "", {}, [config_element("parse", "", { "@type" => "/(?<message>.*)/" })])
117
83
  MULTILINE_CONFIG = config_element(
@@ -144,21 +110,67 @@ class TailInputTest < Test::Unit::TestCase
144
110
  })
145
111
  ])
146
112
 
113
+ EX_ROTATE_WAIT = 0
114
+ EX_FOLLOW_INODES = false
115
+
116
+ def ex_config
117
+ config_element("", "", {
118
+ "tag" => "tail",
119
+ "path" => "test/plugin/*/%Y/%m/%Y%m%d-%H%M%S.log,test/plugin/data/log/**/*.log",
120
+ "format" => "none",
121
+ "pos_file" => "#{@tmp_dir}/tail.pos",
122
+ "read_from_head" => true,
123
+ "refresh_interval" => 30,
124
+ "rotate_wait" => "#{EX_ROTATE_WAIT}s",
125
+ "follow_inodes" => "#{EX_FOLLOW_INODES}",
126
+ })
127
+ end
128
+
129
+ def tailing_group_pattern
130
+ "/#{@tmp_dir}\/(?<podname>[a-z0-9]([-a-z0-9]*[a-z0-9])?(\/[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)_(?<namespace>[^_]+)_(?<container>.+)-(?<docker_id>[a-z0-9]{6})\.log$/"
131
+ end
132
+
133
+ DEBUG_LOG_LEVEL = config_element("", "", {
134
+ "@log_level" => "debug"
135
+ })
136
+
137
+ def create_group_directive(pattern, rate_period, *rules)
138
+ config_element("", "", {}, [
139
+ config_element("group", "", {
140
+ "pattern" => pattern,
141
+ "rate_period" => rate_period
142
+ }, rules)
143
+ ])
144
+ end
145
+
146
+ def create_rule_directive(match_named_captures, limit)
147
+ params = {
148
+ "limit" => limit,
149
+ "match" => match_named_captures,
150
+ }
151
+
152
+ config_element("rule", "", params)
153
+ end
154
+
155
+ def create_path_element(path)
156
+ config_element("source", "", { "path" => "#{@tmp_dir}/#{path}" })
157
+ end
158
+
147
159
  def create_driver(conf = SINGLE_LINE_CONFIG, use_common_conf = true)
148
- config = use_common_conf ? COMMON_CONFIG + conf : conf
160
+ config = use_common_conf ? common_config + conf : conf
149
161
  Fluent::Test::Driver::Input.new(Fluent::Plugin::TailInput).configure(config)
150
162
  end
151
163
 
152
164
  sub_test_case "configure" do
153
165
  test "plain single line" do
154
166
  d = create_driver
155
- assert_equal ["#{TMP_DIR}/tail.txt"], d.instance.paths
156
- assert_equal "t1", d.instance.tag
157
- assert_equal 2, d.instance.rotate_wait
158
- assert_equal "#{TMP_DIR}/tail.pos", d.instance.pos_file
159
- assert_equal 1000, d.instance.read_lines_limit
160
- assert_equal -1, d.instance.read_bytes_limit_per_second
161
- assert_equal false, d.instance.ignore_repeated_permission_error
167
+ assert_equal(["#{@tmp_dir}/tail.txt"], d.instance.paths)
168
+ assert_equal("t1", d.instance.tag)
169
+ assert_equal(2, d.instance.rotate_wait)
170
+ assert_equal("#{@tmp_dir}/tail.pos", d.instance.pos_file)
171
+ assert_equal(1000, d.instance.read_lines_limit)
172
+ assert_equal(-1, d.instance.read_bytes_limit_per_second)
173
+ assert_equal(false, d.instance.ignore_repeated_permission_error)
162
174
  assert_nothing_raised do
163
175
  d.instance.have_read_capability?
164
176
  end
@@ -175,13 +187,13 @@ class TailInputTest < Test::Unit::TestCase
175
187
  test "multi paths with path_delimiter" do
176
188
  c = config_element("ROOT", "", { "path" => "tail.txt|test2|tmp,dev", "tag" => "t1", "path_delimiter" => "|" })
177
189
  d = create_driver(c + PARSE_SINGLE_LINE_CONFIG, false)
178
- assert_equal ["tail.txt", "test2", "tmp,dev"], d.instance.paths
190
+ assert_equal(["tail.txt", "test2", "tmp,dev"], d.instance.paths)
179
191
  end
180
192
 
181
193
  test "multi paths with same path configured twice" do
182
194
  c = config_element("ROOT", "", { "path" => "test1.txt,test2.txt,test1.txt", "tag" => "t1", "path_delimiter" => "," })
183
195
  d = create_driver(c + PARSE_SINGLE_LINE_CONFIG, false)
184
- assert_equal ["test2.txt","test1.txt"].sort, d.instance.paths.sort
196
+ assert_equal(["test2.txt","test1.txt"].sort, d.instance.paths.sort)
185
197
  end
186
198
 
187
199
  test "multi paths with invaid path_delimiter" do
@@ -193,13 +205,13 @@ class TailInputTest < Test::Unit::TestCase
193
205
 
194
206
  test "follow_inodes w/o pos file" do
195
207
  assert_raise(Fluent::ConfigError) do
196
- create_driver(CONFIG + config_element('', '', {'follow_inodes' => 'true'}))
208
+ create_driver(base_config + config_element('', '', {'follow_inodes' => 'true'}))
197
209
  end
198
210
  end
199
211
 
200
212
  sub_test_case "log throttling per file" do
201
213
  test "w/o watcher timer is invalid" do
202
- conf = CONFIG_ENABLE_WATCH_TIMER + config_element("ROOT", "", {"read_bytes_limit_per_second" => "8k"})
214
+ conf = CONFIG_DISABLE_WATCH_TIMER + config_element("ROOT", "", {"read_bytes_limit_per_second" => "8k"})
203
215
  assert_raise(Fluent::ConfigError) do
204
216
  create_driver(conf)
205
217
  end
@@ -215,7 +227,7 @@ class TailInputTest < Test::Unit::TestCase
215
227
 
216
228
  test "both enable_watch_timer and enable_stat_watcher are false" do
217
229
  assert_raise(Fluent::ConfigError) do
218
- create_driver(CONFIG_ENABLE_WATCH_TIMER + CONFIG_DISABLE_STAT_WATCHER + PARSE_SINGLE_LINE_CONFIG)
230
+ create_driver(CONFIG_DISABLE_WATCH_TIMER + CONFIG_DISABLE_STAT_WATCHER + PARSE_SINGLE_LINE_CONFIG)
219
231
  end
220
232
  end
221
233
 
@@ -223,7 +235,7 @@ class TailInputTest < Test::Unit::TestCase
223
235
  test "valid" do
224
236
  conf = SINGLE_LINE_CONFIG + config_element("", "", { "encoding" => "utf-8" })
225
237
  d = create_driver(conf)
226
- assert_equal Encoding::UTF_8, d.instance.encoding
238
+ assert_equal(Encoding::UTF_8, d.instance.encoding)
227
239
  end
228
240
 
229
241
  test "invalid" do
@@ -263,12 +275,171 @@ class TailInputTest < Test::Unit::TestCase
263
275
  end
264
276
  end
265
277
 
278
+ sub_test_case "configure group" do
279
+ test "<rule> required" do
280
+ conf = create_group_directive('.', '1m') + SINGLE_LINE_CONFIG
281
+ assert_raise(Fluent::ConfigError) do
282
+ create_driver(conf)
283
+ end
284
+ end
285
+
286
+ test "valid configuration" do
287
+ rule1 = create_rule_directive({
288
+ "namespace"=> "/namespace-a/",
289
+ "podname"=> "/podname-[b|c]/"
290
+ }, 100)
291
+ rule2 = create_rule_directive({
292
+ "namespace"=> "/namespace-[d|e]/",
293
+ "podname"=> "/podname-f/",
294
+ }, 50)
295
+ rule3 = create_rule_directive({
296
+ "podname"=> "/podname-g/",
297
+ }, -1)
298
+ rule4 = create_rule_directive({
299
+ "namespace"=> "/namespace-h/",
300
+ }, 0)
301
+
302
+ conf = create_group_directive(tailing_group_pattern, '1m', rule1, rule2, rule3, rule4) + SINGLE_LINE_CONFIG
303
+ assert_nothing_raised do
304
+ create_driver(conf)
305
+ end
306
+ end
307
+
308
+ test "limit should be greater than DEFAULT_LIMIT (-1)" do
309
+ rule1 = create_rule_directive({
310
+ "namespace"=> "/namespace-a/",
311
+ "podname"=> "/podname-[b|c]/",
312
+ }, -100)
313
+ rule2 = create_rule_directive({
314
+ "namespace"=> "/namespace-[d|e]/",
315
+ "podname"=> "/podname-f/",
316
+ }, 50)
317
+ conf = create_group_directive(tailing_group_pattern, '1m', rule1, rule2) + SINGLE_LINE_CONFIG
318
+ assert_raise(RuntimeError) do
319
+ create_driver(conf)
320
+ end
321
+ end
322
+ end
323
+
324
+ sub_test_case "group rules line limit resolution" do
325
+ test "valid" do
326
+ rule1 = create_rule_directive({
327
+ "namespace"=> "/namespace-a/",
328
+ "podname"=> "/podname-[b|c]/",
329
+ }, 50)
330
+ rule2 = create_rule_directive({
331
+ "podname"=> "/podname-[b|c]/",
332
+ }, 400)
333
+ rule3 = create_rule_directive({
334
+ "namespace"=> "/namespace-a/",
335
+ }, 100)
336
+
337
+ conf = create_group_directive(tailing_group_pattern, '1m', rule3, rule1, rule2) + SINGLE_LINE_CONFIG
338
+ assert_nothing_raised do
339
+ d = create_driver(conf)
340
+ instance = d.instance
341
+
342
+ metadata = {
343
+ "namespace"=> "namespace-a",
344
+ "podname"=> "podname-b",
345
+ }
346
+ assert_equal(50, instance.find_group(metadata).limit)
347
+
348
+ metadata = {
349
+ "namespace" => "namespace-a",
350
+ "podname" => "podname-c",
351
+ }
352
+ assert_equal(50, instance.find_group(metadata).limit)
353
+
354
+ metadata = {
355
+ "namespace" => "namespace-a",
356
+ "podname" => "podname-d",
357
+ }
358
+ assert_equal(100, instance.find_group(metadata).limit)
359
+
360
+ metadata = {
361
+ "namespace" => "namespace-f",
362
+ "podname" => "podname-b",
363
+ }
364
+ assert_equal(400, instance.find_group(metadata).limit)
365
+
366
+ metadata = {
367
+ "podname" => "podname-c",
368
+ }
369
+ assert_equal(400, instance.find_group(metadata).limit)
370
+
371
+ assert_equal(-1, instance.find_group({}).limit)
372
+ end
373
+ end
374
+ end
375
+
376
+ sub_test_case "files should be placed in groups" do
377
+ test "invalid regex pattern places files in default group" do
378
+ rule1 = create_rule_directive({}, 100) ## limits default groups
379
+ conf = ROOT_CONFIG + DEBUG_LOG_LEVEL + create_group_directive(tailing_group_pattern, '1m', rule1) + create_path_element("test*.txt") + SINGLE_LINE_CONFIG
380
+
381
+ d = create_driver(conf, false)
382
+ Fluent::FileWrapper.open("#{@tmp_dir}/test1.txt", 'w')
383
+ Fluent::FileWrapper.open("#{@tmp_dir}/test2.txt", 'w')
384
+ Fluent::FileWrapper.open("#{@tmp_dir}/test3.txt", 'w')
385
+
386
+ d.run do
387
+ ## checking default group_watcher's paths
388
+ instance = d.instance
389
+ key = instance.default_group_key
390
+
391
+ assert_equal(3, instance.log.logs.count{|a| a.match?("Cannot find group from metadata, Adding file in the default group\n")})
392
+ assert_equal(3, instance.group_watchers[key].size)
393
+ assert_true(instance.group_watchers[key].include? File.join(@tmp_dir, 'test1.txt'))
394
+ assert_true(instance.group_watchers[key].include? File.join(@tmp_dir, 'test2.txt'))
395
+ assert_true(instance.group_watchers[key].include? File.join(@tmp_dir, 'test3.txt'))
396
+ end
397
+ end
398
+
399
+ test "valid regex pattern places file in their respective groups" do
400
+ rule1 = create_rule_directive({
401
+ "namespace"=> "/test-namespace1/",
402
+ "podname"=> "/test-podname1/",
403
+ }, 100)
404
+ rule2 = create_rule_directive({
405
+ "namespace"=> "/test-namespace1/",
406
+ }, 200)
407
+ rule3 = create_rule_directive({
408
+ "podname"=> "/test-podname2/",
409
+ }, 300)
410
+ rule4 = create_rule_directive({}, 400)
411
+
412
+ path_element = create_path_element("test-podname*.log")
413
+
414
+ conf = ROOT_CONFIG + create_group_directive(tailing_group_pattern, '1m', rule4, rule3, rule2, rule1) + path_element + SINGLE_LINE_CONFIG
415
+ d = create_driver(conf, false)
416
+
417
+ file1 = File.join(@tmp_dir, "test-podname1_test-namespace1_test-container-15fabq.log")
418
+ file2 = File.join(@tmp_dir, "test-podname3_test-namespace1_test-container-15fabq.log")
419
+ file3 = File.join(@tmp_dir, "test-podname2_test-namespace2_test-container-15fabq.log")
420
+ file4 = File.join(@tmp_dir, "test-podname4_test-namespace3_test-container-15fabq.log")
421
+
422
+ d.run do
423
+ Fluent::FileWrapper.open(file1, 'w')
424
+ Fluent::FileWrapper.open(file2, 'w')
425
+ Fluent::FileWrapper.open(file3, 'w')
426
+ Fluent::FileWrapper.open(file4, 'w')
427
+
428
+ instance = d.instance
429
+ assert_equal(100, instance.find_group_from_metadata(file1).limit)
430
+ assert_equal(200, instance.find_group_from_metadata(file2).limit)
431
+ assert_equal(300, instance.find_group_from_metadata(file3).limit)
432
+ assert_equal(400, instance.find_group_from_metadata(file4).limit)
433
+ end
434
+ end
435
+ end
436
+
266
437
  sub_test_case "singleline" do
267
438
  data(flat: SINGLE_LINE_CONFIG,
268
439
  parse: PARSE_SINGLE_LINE_CONFIG)
269
440
  def test_emit(data)
270
441
  config = data
271
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
442
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
272
443
  f.puts "test1"
273
444
  f.puts "test2"
274
445
  }
@@ -276,7 +447,7 @@ class TailInputTest < Test::Unit::TestCase
276
447
  d = create_driver(config)
277
448
 
278
449
  d.run(expect_emits: 1) do
279
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f|
450
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f|
280
451
  f.puts "test3\ntest4"
281
452
  }
282
453
  end
@@ -292,11 +463,11 @@ class TailInputTest < Test::Unit::TestCase
292
463
 
293
464
  def test_emit_with_emit_unmatched_lines_true
294
465
  config = config_element("", "", { "format" => "/^(?<message>test.*)/", "emit_unmatched_lines" => true })
295
- File.open("#{TMP_DIR}/tail.txt", "wb") { |f| }
466
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") { |f| }
296
467
 
297
468
  d = create_driver(config)
298
469
  d.run(expect_emits: 1) do
299
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f|
470
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f|
300
471
  f.puts "test line 1"
301
472
  f.puts "test line 2"
302
473
  f.puts "bad line 1"
@@ -328,7 +499,7 @@ class TailInputTest < Test::Unit::TestCase
328
499
  msg = 'test' * 2000 # in_tail reads 8192 bytes at once.
329
500
 
330
501
  d.run(expect_emits: num_events, timeout: 2) do
331
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f|
502
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f|
332
503
  f.puts msg
333
504
  f.puts msg
334
505
  }
@@ -343,7 +514,7 @@ class TailInputTest < Test::Unit::TestCase
343
514
 
344
515
  sub_test_case "log throttling per file" do
345
516
  teardown do
346
- cleanup_file("#{TMP_DIR}/tail.txt")
517
+ cleanup_file("#{@tmp_dir}/tail.txt")
347
518
  end
348
519
 
349
520
  sub_test_case "reads_bytes_per_second w/o throttled" do
@@ -374,7 +545,7 @@ class TailInputTest < Test::Unit::TestCase
374
545
 
375
546
  d = create_driver(config)
376
547
  d.run(expect_emits: 2) do
377
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f|
548
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f|
378
549
  100.times do
379
550
  f.puts msg
380
551
  end
@@ -397,7 +568,7 @@ class TailInputTest < Test::Unit::TestCase
397
568
  start_time = Fluent::Clock.now
398
569
  d = create_driver(config)
399
570
  d.run(expect_emits: 2) do
400
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f|
571
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f|
401
572
  8000.times do
402
573
  f.puts msg
403
574
  end
@@ -436,7 +607,7 @@ class TailInputTest < Test::Unit::TestCase
436
607
  io_handler
437
608
  end
438
609
 
439
- File.open("#{TMP_DIR}/tail.txt", "ab") do |f|
610
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") do |f|
440
611
  100.times do
441
612
  f.puts msg
442
613
  end
@@ -446,7 +617,7 @@ class TailInputTest < Test::Unit::TestCase
446
617
  d.run do
447
618
  start_time = Fluent::Clock.now
448
619
  while Fluent::Clock.now - start_time < 0.8 do
449
- File.open("#{TMP_DIR}/tail.txt", "ab") do |f|
620
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") do |f|
450
621
  f.puts msg
451
622
  f.flush
452
623
  end
@@ -464,7 +635,7 @@ class TailInputTest < Test::Unit::TestCase
464
635
  num_lines = 1024 * 3
465
636
  msg = "08bytes"
466
637
 
467
- File.open("#{TMP_DIR}/tail.txt", "wb") do |f|
638
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") do |f|
468
639
  f.write("#{msg}\n" * num_lines)
469
640
  end
470
641
 
@@ -481,8 +652,8 @@ class TailInputTest < Test::Unit::TestCase
481
652
  d.run(timeout: 10) do
482
653
  while d.events.size < num_lines do
483
654
  if d.events.size > 0 && !rotated
484
- cleanup_file("#{TMP_DIR}/tail.txt")
485
- FileUtils.touch("#{TMP_DIR}/tail.txt")
655
+ cleanup_file("#{@tmp_dir}/tail.txt")
656
+ FileUtils.touch("#{@tmp_dir}/tail.txt")
486
657
  rotated = true
487
658
  end
488
659
  sleep 0.3
@@ -500,7 +671,7 @@ class TailInputTest < Test::Unit::TestCase
500
671
  num_lines = 1024 * 2
501
672
  msg = "08bytes"
502
673
 
503
- File.open("#{TMP_DIR}/tail.txt", "wb") do |f|
674
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") do |f|
504
675
  f.write("#{msg}\n" * num_lines)
505
676
  end
506
677
 
@@ -527,8 +698,8 @@ class TailInputTest < Test::Unit::TestCase
527
698
  d.run(timeout: 10) do
528
699
  until detached do
529
700
  if d.events.size > 0 && !rotated
530
- cleanup_file("#{TMP_DIR}/tail.txt")
531
- FileUtils.touch("#{TMP_DIR}/tail.txt")
701
+ cleanup_file("#{@tmp_dir}/tail.txt")
702
+ FileUtils.touch("#{@tmp_dir}/tail.txt")
532
703
  rotated = true
533
704
  end
534
705
  sleep 0.3
@@ -548,7 +719,7 @@ class TailInputTest < Test::Unit::TestCase
548
719
  parse: CONFIG_READ_FROM_HEAD + PARSE_SINGLE_LINE_CONFIG)
549
720
  def test_emit_with_read_from_head(data)
550
721
  config = data
551
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
722
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
552
723
  f.puts "test1"
553
724
  f.puts "test2"
554
725
  }
@@ -556,7 +727,7 @@ class TailInputTest < Test::Unit::TestCase
556
727
  d = create_driver(config)
557
728
 
558
729
  d.run(expect_emits: 2) do
559
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f|
730
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f|
560
731
  f.puts "test3"
561
732
  f.puts "test4"
562
733
  }
@@ -570,11 +741,11 @@ class TailInputTest < Test::Unit::TestCase
570
741
  assert_equal({"message" => "test4"}, events[3][2])
571
742
  end
572
743
 
573
- data(flat: CONFIG_ENABLE_WATCH_TIMER + SINGLE_LINE_CONFIG,
574
- parse: CONFIG_ENABLE_WATCH_TIMER + PARSE_SINGLE_LINE_CONFIG)
575
- def test_emit_with_enable_watch_timer(data)
744
+ data(flat: CONFIG_DISABLE_WATCH_TIMER + SINGLE_LINE_CONFIG,
745
+ parse: CONFIG_DISABLE_WATCH_TIMER + PARSE_SINGLE_LINE_CONFIG)
746
+ def test_emit_without_watch_timer(data)
576
747
  config = data
577
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
748
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
578
749
  f.puts "test1"
579
750
  f.puts "test2"
580
751
  }
@@ -582,7 +753,7 @@ class TailInputTest < Test::Unit::TestCase
582
753
  d = create_driver(config)
583
754
 
584
755
  d.run(expect_emits: 1) do
585
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f|
756
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f|
586
757
  f.puts "test3"
587
758
  f.puts "test4"
588
759
  }
@@ -596,11 +767,43 @@ class TailInputTest < Test::Unit::TestCase
596
767
  assert_equal({"message" => "test4"}, events[1][2])
597
768
  end
598
769
 
770
+ # https://github.com/fluent/fluentd/pull/3541#discussion_r740197711
771
+ def test_watch_wildcard_path_without_watch_timer
772
+ omit "need inotify" unless Fluent.linux?
773
+
774
+ config = config_element("ROOT", "", {
775
+ "path" => "#{@tmp_dir}/tail*.txt",
776
+ "tag" => "t1",
777
+ })
778
+ config = config + CONFIG_DISABLE_WATCH_TIMER + SINGLE_LINE_CONFIG
779
+
780
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
781
+ f.puts "test1"
782
+ f.puts "test2"
783
+ }
784
+
785
+ d = create_driver(config, false)
786
+
787
+ d.run(expect_emits: 1, timeout: 1) do
788
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f|
789
+ f.puts "test3"
790
+ f.puts "test4"
791
+ }
792
+ end
793
+
794
+ assert_equal(
795
+ [
796
+ {"message" => "test3"},
797
+ {"message" => "test4"},
798
+ ],
799
+ d.events.collect { |event| event[2] })
800
+ end
801
+
599
802
  data(flat: CONFIG_DISABLE_STAT_WATCHER + SINGLE_LINE_CONFIG,
600
803
  parse: CONFIG_DISABLE_STAT_WATCHER + PARSE_SINGLE_LINE_CONFIG)
601
804
  def test_emit_with_disable_stat_watcher(data)
602
805
  config = data
603
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
806
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
604
807
  f.puts "test1"
605
808
  f.puts "test2"
606
809
  }
@@ -608,7 +811,7 @@ class TailInputTest < Test::Unit::TestCase
608
811
  d = create_driver(config)
609
812
 
610
813
  d.run(expect_emits: 1) do
611
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f|
814
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f|
612
815
  f.puts "test3"
613
816
  f.puts "test4"
614
817
  }
@@ -619,6 +822,23 @@ class TailInputTest < Test::Unit::TestCase
619
822
  assert_equal({"message" => "test3"}, events[0][2])
620
823
  assert_equal({"message" => "test4"}, events[1][2])
621
824
  end
825
+
826
+ def test_always_read_from_head_on_detecting_a_new_file
827
+ d = create_driver(SINGLE_LINE_CONFIG)
828
+
829
+ d.run(expect_emits: 1, timeout: 3) do
830
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
831
+ f.puts "test1\ntest2\n"
832
+ }
833
+ end
834
+
835
+ assert_equal(
836
+ [
837
+ {"message" => "test1"},
838
+ {"message" => "test2"},
839
+ ],
840
+ d.events.collect { |event| event[2] })
841
+ end
622
842
  end
623
843
 
624
844
  class TestWithSystem < self
@@ -633,11 +853,14 @@ class TailInputTest < Test::Unit::TestCase
633
853
 
634
854
  def setup
635
855
  omit "NTFS doesn't support UNIX like permissions" if Fluent.windows?
856
+ super
636
857
  # Store default permission
637
858
  @default_permission = system_config.instance_variable_get(:@file_permission)
638
859
  end
639
860
 
640
861
  def teardown
862
+ return if Fluent.windows?
863
+ super
641
864
  # Restore default permission
642
865
  system_config.instance_variable_set(:@file_permission, @default_permission)
643
866
  end
@@ -651,7 +874,7 @@ class TailInputTest < Test::Unit::TestCase
651
874
  system_conf = parse_system(CONFIG_SYSTEM)
652
875
  sc = Fluent::SystemConfig.new(system_conf)
653
876
  Fluent::Engine.init(sc)
654
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
877
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
655
878
  f.puts "test1"
656
879
  f.puts "test2"
657
880
  }
@@ -659,7 +882,7 @@ class TailInputTest < Test::Unit::TestCase
659
882
  d = create_driver
660
883
 
661
884
  d.run(expect_emits: 1) do
662
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f|
885
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f|
663
886
  f.puts "test3"
664
887
  f.puts "test4"
665
888
  }
@@ -679,16 +902,18 @@ class TailInputTest < Test::Unit::TestCase
679
902
  end
680
903
 
681
904
  sub_test_case "rotate file" do
905
+ def create_driver(conf = SINGLE_LINE_CONFIG)
906
+ config = common_config + conf
907
+ Fluent::Test::Driver::Input.new(Fluent::Plugin::TailInput).configure(config)
908
+ end
909
+
682
910
  data(flat: SINGLE_LINE_CONFIG,
683
911
  parse: PARSE_SINGLE_LINE_CONFIG)
684
912
  def test_rotate_file(data)
685
913
  config = data
686
914
  events = sub_test_rotate_file(config, expect_emits: 2)
687
- assert_equal(4, events.length)
688
- assert_equal({"message" => "test3"}, events[0][2])
689
- assert_equal({"message" => "test4"}, events[1][2])
690
- assert_equal({"message" => "test5"}, events[2][2])
691
- assert_equal({"message" => "test6"}, events[3][2])
915
+ assert_equal(3.upto(6).collect { |i| {"message" => "test#{i}"} },
916
+ events.collect { |event| event[2] })
692
917
  end
693
918
 
694
919
  data(flat: CONFIG_READ_FROM_HEAD + SINGLE_LINE_CONFIG,
@@ -696,13 +921,8 @@ class TailInputTest < Test::Unit::TestCase
696
921
  def test_rotate_file_with_read_from_head(data)
697
922
  config = data
698
923
  events = sub_test_rotate_file(config, expect_records: 6)
699
- assert_equal(6, events.length)
700
- assert_equal({"message" => "test1"}, events[0][2])
701
- assert_equal({"message" => "test2"}, events[1][2])
702
- assert_equal({"message" => "test3"}, events[2][2])
703
- assert_equal({"message" => "test4"}, events[3][2])
704
- assert_equal({"message" => "test5"}, events[4][2])
705
- assert_equal({"message" => "test6"}, events[5][2])
924
+ assert_equal(1.upto(6).collect { |i| {"message" => "test#{i}"} },
925
+ events.collect { |event| event[2] })
706
926
  end
707
927
 
708
928
  data(flat: CONFIG_OPEN_ON_EVERY_UPDATE + CONFIG_READ_FROM_HEAD + SINGLE_LINE_CONFIG,
@@ -710,13 +930,8 @@ class TailInputTest < Test::Unit::TestCase
710
930
  def test_rotate_file_with_open_on_every_update(data)
711
931
  config = data
712
932
  events = sub_test_rotate_file(config, expect_records: 6)
713
- assert_equal(6, events.length)
714
- assert_equal({"message" => "test1"}, events[0][2])
715
- assert_equal({"message" => "test2"}, events[1][2])
716
- assert_equal({"message" => "test3"}, events[2][2])
717
- assert_equal({"message" => "test4"}, events[3][2])
718
- assert_equal({"message" => "test5"}, events[4][2])
719
- assert_equal({"message" => "test6"}, events[5][2])
933
+ assert_equal(1.upto(6).collect { |i| {"message" => "test#{i}"} },
934
+ events.collect { |event| event[2] })
720
935
  end
721
936
 
722
937
  data(flat: SINGLE_LINE_CONFIG,
@@ -724,26 +939,21 @@ class TailInputTest < Test::Unit::TestCase
724
939
  def test_rotate_file_with_write_old(data)
725
940
  config = data
726
941
  events = sub_test_rotate_file(config, expect_emits: 3) { |rotated_file|
727
- File.open("#{TMP_DIR}/tail.txt", "wb") { |f| }
942
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") { |f| }
728
943
  rotated_file.puts "test7"
729
944
  rotated_file.puts "test8"
730
945
  rotated_file.flush
731
946
 
732
947
  sleep 1
733
- File.open("#{TMP_DIR}/tail.txt", "ab") { |f|
948
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") { |f|
734
949
  f.puts "test5"
735
950
  f.puts "test6"
736
951
  }
737
952
  }
738
953
  # This test sometimes fails and it shows a potential bug of in_tail
739
954
  # https://github.com/fluent/fluentd/issues/1434
740
- assert_equal(6, events.length)
741
- assert_equal({"message" => "test3"}, events[0][2])
742
- assert_equal({"message" => "test4"}, events[1][2])
743
- assert_equal({"message" => "test7"}, events[2][2])
744
- assert_equal({"message" => "test8"}, events[3][2])
745
- assert_equal({"message" => "test5"}, events[4][2])
746
- assert_equal({"message" => "test6"}, events[5][2])
955
+ assert_equal([3, 4, 7, 8, 5, 6].collect { |i| {"message" => "test#{i}"} },
956
+ events.collect { |event| event[2] })
747
957
  end
748
958
 
749
959
  data(flat: SINGLE_LINE_CONFIG,
@@ -755,21 +965,19 @@ class TailInputTest < Test::Unit::TestCase
755
965
  rotated_file.puts "test8"
756
966
  rotated_file.flush
757
967
  }
758
- assert_equal(4, events.length)
759
- assert_equal({"message" => "test3"}, events[0][2])
760
- assert_equal({"message" => "test4"}, events[1][2])
761
- assert_equal({"message" => "test7"}, events[2][2])
762
- assert_equal({"message" => "test8"}, events[3][2])
968
+ assert_equal([3, 4, 7, 8].collect { |i| {"message" => "test#{i}"} },
969
+ events.collect { |event| event[2] })
763
970
  end
764
971
 
765
- def sub_test_rotate_file(config = nil, expect_emits: nil, expect_records: nil, timeout: nil)
766
- file = Fluent::FileWrapper.open("#{TMP_DIR}/tail.txt", "wb")
972
+ def sub_test_rotate_file(config = nil, expect_emits: nil, expect_records: nil, timeout: 5)
973
+ file = Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb")
767
974
  file.puts "test1"
768
975
  file.puts "test2"
769
976
  file.flush
770
977
 
771
978
  d = create_driver(config)
772
979
  d.run(expect_emits: expect_emits, expect_records: expect_records, timeout: timeout) do
980
+ sleep(0.1) while d.instance.instance_variable_get(:@startup)
773
981
  size = d.emit_count
774
982
  file.puts "test3"
775
983
  file.puts "test4"
@@ -779,18 +987,18 @@ class TailInputTest < Test::Unit::TestCase
779
987
 
780
988
  if Fluent.windows?
781
989
  file.close
782
- FileUtils.mv("#{TMP_DIR}/tail.txt", "#{TMP_DIR}/tail2.txt", force: true)
783
- file = File.open("#{TMP_DIR}/tail.txt", "ab")
990
+ FileUtils.mv("#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail2.txt", force: true)
991
+ file = Fluent::FileWrapper.open("#{@tmp_dir}/tail2.txt", "ab")
784
992
  else
785
- FileUtils.mv("#{TMP_DIR}/tail.txt", "#{TMP_DIR}/tail2.txt")
993
+ FileUtils.mv("#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail2.txt")
786
994
  end
787
995
  if block_given?
788
996
  yield file
789
997
  else
790
- File.open("#{TMP_DIR}/tail.txt", "wb") { |f| }
998
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") { |f| }
791
999
  sleep 1
792
1000
 
793
- File.open("#{TMP_DIR}/tail.txt", "ab") { |f|
1001
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") { |f|
794
1002
  f.puts "test5"
795
1003
  f.puts "test6"
796
1004
  }
@@ -804,10 +1012,8 @@ class TailInputTest < Test::Unit::TestCase
804
1012
  end
805
1013
 
806
1014
  def test_truncate_file
807
- omit "Permission denied error happen on Windows. Need fix" if Fluent.windows?
808
-
809
1015
  config = SINGLE_LINE_CONFIG
810
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
1016
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
811
1017
  f.puts "test1"
812
1018
  f.puts "test2"
813
1019
  f.flush
@@ -816,12 +1022,18 @@ class TailInputTest < Test::Unit::TestCase
816
1022
  d = create_driver(config)
817
1023
 
818
1024
  d.run(expect_emits: 2) do
819
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f|
1025
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f|
820
1026
  f.puts "test3\ntest4"
821
1027
  f.flush
822
1028
  }
823
1029
  waiting(2) { sleep 0.1 until d.events.length == 2 }
824
- File.truncate("#{TMP_DIR}/tail.txt", 6)
1030
+ if Fluent.windows?
1031
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") { |f|
1032
+ f.puts("test1");
1033
+ }
1034
+ else
1035
+ File.truncate("#{@tmp_dir}/tail.txt", 6)
1036
+ end
825
1037
  end
826
1038
 
827
1039
  expected = {
@@ -840,10 +1052,8 @@ class TailInputTest < Test::Unit::TestCase
840
1052
  end
841
1053
 
842
1054
  def test_move_truncate_move_back
843
- omit "Permission denied error happen on Windows. Need fix" if Fluent.windows?
844
-
845
1055
  config = SINGLE_LINE_CONFIG
846
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
1056
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
847
1057
  f.puts "test1"
848
1058
  f.puts "test2"
849
1059
  }
@@ -852,17 +1062,23 @@ class TailInputTest < Test::Unit::TestCase
852
1062
 
853
1063
  d.run(expect_emits: 1) do
854
1064
  if Fluent.windows?
855
- FileUtils.mv("#{TMP_DIR}/tail.txt", "#{TMP_DIR}/tail2.txt", force: true)
1065
+ FileUtils.mv("#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail2.txt", force: true)
856
1066
  else
857
- FileUtils.mv("#{TMP_DIR}/tail.txt", "#{TMP_DIR}/tail2.txt")
1067
+ FileUtils.mv("#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail2.txt")
858
1068
  end
859
1069
  sleep(1)
860
- File.truncate("#{TMP_DIR}/tail2.txt", 6)
1070
+ if Fluent.windows?
1071
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail2.txt", "wb") { |f|
1072
+ f.puts("test1");
1073
+ }
1074
+ else
1075
+ File.truncate("#{@tmp_dir}/tail2.txt", 6)
1076
+ end
861
1077
  sleep(1)
862
1078
  if Fluent.windows?
863
- FileUtils.mv("#{TMP_DIR}/tail2.txt", "#{TMP_DIR}/tail.txt", force: true)
1079
+ FileUtils.mv("#{@tmp_dir}/tail2.txt", "#{@tmp_dir}/tail.txt", force: true)
864
1080
  else
865
- FileUtils.mv("#{TMP_DIR}/tail2.txt", "#{TMP_DIR}/tail.txt")
1081
+ FileUtils.mv("#{@tmp_dir}/tail2.txt", "#{@tmp_dir}/tail.txt")
866
1082
  end
867
1083
  end
868
1084
 
@@ -874,17 +1090,17 @@ class TailInputTest < Test::Unit::TestCase
874
1090
  end
875
1091
 
876
1092
  def test_lf
877
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f| }
1093
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f| }
878
1094
 
879
1095
  d = create_driver
880
1096
 
881
1097
  d.run(expect_emits: 1) do
882
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f|
1098
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f|
883
1099
  f.print "test3"
884
1100
  }
885
1101
  sleep 1
886
1102
 
887
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f|
1103
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f|
888
1104
  f.puts "test4"
889
1105
  }
890
1106
  end
@@ -895,12 +1111,12 @@ class TailInputTest < Test::Unit::TestCase
895
1111
  end
896
1112
 
897
1113
  def test_whitespace
898
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f| }
1114
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f| }
899
1115
 
900
1116
  d = create_driver
901
1117
 
902
1118
  d.run(expect_emits: 1) do
903
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f|
1119
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f|
904
1120
  f.puts " " # 4 spaces
905
1121
  f.puts " 4 spaces"
906
1122
  f.puts "4 spaces "
@@ -931,7 +1147,7 @@ class TailInputTest < Test::Unit::TestCase
931
1147
  d = create_driver(CONFIG_READ_FROM_HEAD + encoding_config)
932
1148
 
933
1149
  d.run(expect_emits: 1) do
934
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
1150
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
935
1151
  f.puts "test"
936
1152
  }
937
1153
  end
@@ -953,7 +1169,7 @@ class TailInputTest < Test::Unit::TestCase
953
1169
  utf8_message = cp932_message.encode(Encoding::UTF_8)
954
1170
 
955
1171
  d.run(expect_emits: 1) do
956
- File.open("#{TMP_DIR}/tail.txt", "w:cp932") {|f|
1172
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "w:cp932") {|f|
957
1173
  f.puts cp932_message
958
1174
  }
959
1175
  end
@@ -976,7 +1192,7 @@ class TailInputTest < Test::Unit::TestCase
976
1192
  utf8_message = utf16_message.encode(Encoding::UTF_8).strip
977
1193
 
978
1194
  d.run(expect_emits: 1) do
979
- File.open("#{TMP_DIR}/tail.txt", "w:utf-16le") { |f|
1195
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "w:utf-16le") { |f|
980
1196
  f.write utf16_message
981
1197
  }
982
1198
  end
@@ -997,7 +1213,7 @@ class TailInputTest < Test::Unit::TestCase
997
1213
  d = create_driver(conf)
998
1214
 
999
1215
  d.run(expect_emits: 1) do
1000
- File.open("#{TMP_DIR}/tail.txt", "w") { |f|
1216
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "w") { |f|
1001
1217
  f.write "te\x86st\n"
1002
1218
  }
1003
1219
  end
@@ -1012,11 +1228,11 @@ class TailInputTest < Test::Unit::TestCase
1012
1228
  parse: PARSE_MULTILINE_CONFIG)
1013
1229
  def test_multiline(data)
1014
1230
  config = data
1015
- File.open("#{TMP_DIR}/tail.txt", "wb") { |f| }
1231
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") { |f| }
1016
1232
 
1017
1233
  d = create_driver(config)
1018
1234
  d.run(expect_emits: 1) do
1019
- File.open("#{TMP_DIR}/tail.txt", "ab") { |f|
1235
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") { |f|
1020
1236
  f.puts "f test1"
1021
1237
  f.puts "s test2"
1022
1238
  f.puts "f test3"
@@ -1040,11 +1256,11 @@ class TailInputTest < Test::Unit::TestCase
1040
1256
  parse: PARSE_MULTILINE_CONFIG)
1041
1257
  def test_multiline_with_emit_unmatched_lines_true(data)
1042
1258
  config = data + config_element("", "", { "emit_unmatched_lines" => true })
1043
- File.open("#{TMP_DIR}/tail.txt", "wb") { |f| }
1259
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") { |f| }
1044
1260
 
1045
1261
  d = create_driver(config)
1046
1262
  d.run(expect_emits: 1) do
1047
- File.open("#{TMP_DIR}/tail.txt", "ab") { |f|
1263
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") { |f|
1048
1264
  f.puts "f test1"
1049
1265
  f.puts "s test2"
1050
1266
  f.puts "f test3"
@@ -1070,11 +1286,11 @@ class TailInputTest < Test::Unit::TestCase
1070
1286
  parse: PARSE_MULTILINE_CONFIG_WITH_NEWLINE)
1071
1287
  def test_multiline_with_emit_unmatched_lines2(data)
1072
1288
  config = data + config_element("", "", { "emit_unmatched_lines" => true })
1073
- File.open("#{TMP_DIR}/tail.txt", "wb") { |f| }
1289
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") { |f| }
1074
1290
 
1075
1291
  d = create_driver(config)
1076
1292
  d.run(expect_emits: 0, timeout: 1) do
1077
- File.open("#{TMP_DIR}/tail.txt", "ab") { |f|
1293
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") { |f|
1078
1294
  f.puts "s test0"
1079
1295
  f.puts "f test1"
1080
1296
  f.puts "f test2"
@@ -1096,15 +1312,15 @@ class TailInputTest < Test::Unit::TestCase
1096
1312
  data(flat: MULTILINE_CONFIG,
1097
1313
  parse: PARSE_MULTILINE_CONFIG)
1098
1314
  def test_multiline_with_flush_interval(data)
1099
- File.open("#{TMP_DIR}/tail.txt", "wb") { |f| }
1315
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") { |f| }
1100
1316
 
1101
1317
  config = data + config_element("", "", { "multiline_flush_interval" => "2s" })
1102
1318
  d = create_driver(config)
1103
1319
 
1104
- assert_equal 2, d.instance.multiline_flush_interval
1320
+ assert_equal(2, d.instance.multiline_flush_interval)
1105
1321
 
1106
1322
  d.run(expect_emits: 1) do
1107
- File.open("#{TMP_DIR}/tail.txt", "ab") { |f|
1323
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") { |f|
1108
1324
  f.puts "f test1"
1109
1325
  f.puts "s test2"
1110
1326
  f.puts "f test3"
@@ -1139,7 +1355,7 @@ class TailInputTest < Test::Unit::TestCase
1139
1355
  d = create_driver(config + encoding_config)
1140
1356
 
1141
1357
  d.run(expect_emits: 1) do
1142
- File.open("#{TMP_DIR}/tail.txt", "wb") { |f|
1358
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") { |f|
1143
1359
  f.puts "s test"
1144
1360
  }
1145
1361
  end
@@ -1162,7 +1378,7 @@ class TailInputTest < Test::Unit::TestCase
1162
1378
  cp932_message = "s \x82\xCD\x82\xEB\x81\x5B\x82\xED\x81\x5B\x82\xE9\x82\xC7".force_encoding(Encoding::CP932)
1163
1379
  utf8_message = "\x82\xCD\x82\xEB\x81\x5B\x82\xED\x81\x5B\x82\xE9\x82\xC7".encode(Encoding::UTF_8, Encoding::CP932)
1164
1380
  d.run(expect_emits: 1) do
1165
- File.open("#{TMP_DIR}/tail.txt", "w:cp932") { |f|
1381
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "w:cp932") { |f|
1166
1382
  f.puts cp932_message
1167
1383
  }
1168
1384
  end
@@ -1194,11 +1410,11 @@ class TailInputTest < Test::Unit::TestCase
1194
1410
  )
1195
1411
  def test_multiline_with_multiple_formats(data)
1196
1412
  config = data
1197
- File.open("#{TMP_DIR}/tail.txt", "wb") { |f| }
1413
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") { |f| }
1198
1414
 
1199
1415
  d = create_driver(config)
1200
1416
  d.run(expect_emits: 1) do
1201
- File.open("#{TMP_DIR}/tail.txt", "ab") { |f|
1417
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") { |f|
1202
1418
  f.puts "f test1"
1203
1419
  f.puts "s test2"
1204
1420
  f.puts "f test3"
@@ -1234,8 +1450,8 @@ class TailInputTest < Test::Unit::TestCase
1234
1450
  ])
1235
1451
  )
1236
1452
  def test_multilinelog_with_multiple_paths(data)
1237
- files = ["#{TMP_DIR}/tail1.txt", "#{TMP_DIR}/tail2.txt"]
1238
- files.each { |file| File.open(file, "wb") { |f| } }
1453
+ files = ["#{@tmp_dir}/tail1.txt", "#{@tmp_dir}/tail2.txt"]
1454
+ files.each { |file| Fluent::FileWrapper.open(file, "wb") { |f| } }
1239
1455
 
1240
1456
  config = data + config_element("", "", {
1241
1457
  "path" => "#{files[0]},#{files[1]}",
@@ -1244,7 +1460,7 @@ class TailInputTest < Test::Unit::TestCase
1244
1460
  d = create_driver(config, false)
1245
1461
  d.run(expect_emits: 2) do
1246
1462
  files.each do |file|
1247
- File.open(file, 'ab') { |f|
1463
+ Fluent::FileWrapper.open(file, 'ab') { |f|
1248
1464
  f.puts "f #{file} line should be ignored"
1249
1465
  f.puts "s test1"
1250
1466
  f.puts "f test2"
@@ -1279,12 +1495,12 @@ class TailInputTest < Test::Unit::TestCase
1279
1495
  ])
1280
1496
  )
1281
1497
  def test_multiline_without_firstline(data)
1282
- File.open("#{TMP_DIR}/tail.txt", "wb") { |f| }
1498
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") { |f| }
1283
1499
 
1284
1500
  config = data
1285
1501
  d = create_driver(config)
1286
1502
  d.run(expect_emits: 1) do
1287
- File.open("#{TMP_DIR}/tail.txt", "ab") { |f|
1503
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") { |f|
1288
1504
  f.puts "foo 1"
1289
1505
  f.puts "bar 1"
1290
1506
  f.puts "baz 1"
@@ -1304,35 +1520,22 @@ class TailInputTest < Test::Unit::TestCase
1304
1520
  sub_test_case "path" do
1305
1521
  # * path test
1306
1522
  # TODO: Clean up tests
1307
- EX_ROTATE_WAIT = 0
1308
- EX_FOLLOW_INODES = false
1309
-
1310
- EX_CONFIG = config_element("", "", {
1311
- "tag" => "tail",
1312
- "path" => "test/plugin/*/%Y/%m/%Y%m%d-%H%M%S.log,test/plugin/data/log/**/*.log",
1313
- "format" => "none",
1314
- "pos_file" => "#{TMP_DIR}/tail.pos",
1315
- "read_from_head" => true,
1316
- "refresh_interval" => 30,
1317
- "rotate_wait" => "#{EX_ROTATE_WAIT}s",
1318
- "follow_inodes" => "#{EX_FOLLOW_INODES}",
1319
- })
1320
1523
  def test_expand_paths
1321
1524
  ex_paths = [
1322
1525
  create_target_info('test/plugin/data/2010/01/20100102-030405.log'),
1323
1526
  create_target_info('test/plugin/data/log/foo/bar.log'),
1324
1527
  create_target_info('test/plugin/data/log/test.log')
1325
1528
  ]
1326
- plugin = create_driver(EX_CONFIG, false).instance
1529
+ plugin = create_driver(ex_config, false).instance
1327
1530
  flexstub(Time) do |timeclass|
1328
1531
  timeclass.should_receive(:now).with_no_args.and_return(Time.new(2010, 1, 2, 3, 4, 5))
1329
- assert_equal ex_paths, plugin.expand_paths.values.sort_by { |path_ino| path_ino.path }
1532
+ assert_equal(ex_paths, plugin.expand_paths.values.sort_by { |path_ino| path_ino.path })
1330
1533
  end
1331
1534
 
1332
1535
  # Test exclusion
1333
- exclude_config = EX_CONFIG + config_element("", "", { "exclude_path" => %Q(["#{ex_paths.last.path}"]) })
1536
+ exclude_config = ex_config + config_element("", "", { "exclude_path" => %Q(["#{ex_paths.last.path}"]) })
1334
1537
  plugin = create_driver(exclude_config, false).instance
1335
- assert_equal ex_paths - [ex_paths.last], plugin.expand_paths.values.sort_by { |path_ino| path_ino.path }
1538
+ assert_equal(ex_paths - [ex_paths.last], plugin.expand_paths.values.sort_by { |path_ino| path_ino.path })
1336
1539
  end
1337
1540
 
1338
1541
  def test_expand_paths_with_duplicate_configuration
@@ -1340,10 +1543,10 @@ class TailInputTest < Test::Unit::TestCase
1340
1543
  create_target_info('test/plugin/data/log/foo/bar.log'),
1341
1544
  create_target_info('test/plugin/data/log/test.log')
1342
1545
  ]
1343
- duplicate_config = EX_CONFIG.dup
1546
+ duplicate_config = ex_config.dup
1344
1547
  duplicate_config["path"]="test/plugin/data/log/**/*.log, test/plugin/data/log/**/*.log"
1345
- plugin = create_driver(EX_CONFIG, false).instance
1346
- assert_equal expanded_paths, plugin.expand_paths.values.sort_by { |path_ino| path_ino.path }
1548
+ plugin = create_driver(ex_config, false).instance
1549
+ assert_equal(expanded_paths, plugin.expand_paths.values.sort_by { |path_ino| path_ino.path })
1347
1550
  end
1348
1551
 
1349
1552
  def test_expand_paths_with_timezone
@@ -1353,7 +1556,7 @@ class TailInputTest < Test::Unit::TestCase
1353
1556
  create_target_info('test/plugin/data/log/test.log')
1354
1557
  ]
1355
1558
  ['Asia/Taipei', '+08'].each do |tz_type|
1356
- taipei_config = EX_CONFIG + config_element("", "", {"path_timezone" => tz_type})
1559
+ taipei_config = ex_config + config_element("", "", {"path_timezone" => tz_type})
1357
1560
  plugin = create_driver(taipei_config, false).instance
1358
1561
 
1359
1562
  # Test exclude
@@ -1365,8 +1568,8 @@ class TailInputTest < Test::Unit::TestCase
1365
1568
  # env : 2010-01-01 19:04:05 (UTC), tail path : 2010-01-02 03:04:05 (Asia/Taipei)
1366
1569
  timeclass.should_receive(:now).with_no_args.and_return(Time.new(2010, 1, 1, 19, 4, 5))
1367
1570
 
1368
- assert_equal ex_paths, plugin.expand_paths.values.sort_by { |path_ino| path_ino.path }
1369
- assert_equal ex_paths - [ex_paths.first], exclude_plugin.expand_paths.values.sort_by { |path_ino| path_ino.path }
1571
+ assert_equal(ex_paths, plugin.expand_paths.values.sort_by { |path_ino| path_ino.path })
1572
+ assert_equal(ex_paths - [ex_paths.first], exclude_plugin.expand_paths.values.sort_by { |path_ino| path_ino.path })
1370
1573
  end
1371
1574
  end
1372
1575
  end
@@ -1384,35 +1587,45 @@ class TailInputTest < Test::Unit::TestCase
1384
1587
  "tag" => "tail",
1385
1588
  "path" => "test/plugin/data/log/**/*",
1386
1589
  "format" => "none",
1387
- "pos_file" => "#{TMP_DIR}/tail.pos"
1590
+ "pos_file" => "#{@tmp_dir}/tail.pos"
1388
1591
  })
1389
1592
 
1390
1593
  plugin = create_driver(config, false).instance
1391
- assert_equal expected_files, plugin.expand_paths.values.sort_by { |path_ino| path_ino.path }
1594
+ assert_equal(expected_files, plugin.expand_paths.values.sort_by { |path_ino| path_ino.path })
1392
1595
  end
1393
1596
 
1394
1597
  def test_unwatched_files_should_be_removed
1395
1598
  config = config_element("", "", {
1396
1599
  "tag" => "tail",
1397
- "path" => "#{TMP_DIR}/*.txt",
1600
+ "path" => "#{@tmp_dir}/*.txt",
1398
1601
  "format" => "none",
1399
- "pos_file" => "#{TMP_DIR}/tail.pos",
1602
+ "pos_file" => "#{@tmp_dir}/tail.pos",
1400
1603
  "read_from_head" => true,
1401
1604
  "refresh_interval" => 1,
1402
1605
  })
1403
1606
  d = create_driver(config, false)
1404
1607
  d.end_if { d.instance.instance_variable_get(:@tails).keys.size >= 1 }
1405
1608
  d.run(expect_emits: 1, shutdown: false) do
1406
- File.open("#{TMP_DIR}/tail.txt", "ab") { |f| f.puts "test3\n" }
1609
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") { |f| f.puts "test3\n" }
1407
1610
  end
1408
1611
 
1409
- cleanup_directory(TMP_DIR)
1410
- waiting(20) { sleep 0.1 until Dir.glob("#{TMP_DIR}/*.txt").size == 0 } # Ensure file is deleted on Windows
1612
+ cleanup_file("#{@tmp_dir}/tail.txt")
1613
+ waiting(20) { sleep 0.1 until Dir.glob("#{@tmp_dir}/*.txt").size == 0 } # Ensure file is deleted on Windows
1411
1614
  waiting(5) { sleep 0.1 until d.instance.instance_variable_get(:@tails).keys.size <= 0 }
1412
1615
 
1413
- assert_equal 0, d.instance.instance_variable_get(:@tails).keys.size
1414
-
1415
- d.instance_shutdown
1616
+ assert_equal(
1617
+ {
1618
+ files: [],
1619
+ tails: []
1620
+ },
1621
+ {
1622
+ files: Dir.glob("#{@tmp_dir}/*.txt"),
1623
+ tails: d.instance.instance_variable_get(:@tails).keys
1624
+ }
1625
+ )
1626
+ ensure
1627
+ d.instance_shutdown if d && d.instance
1628
+ cleanup_directory(@tmp_dir)
1416
1629
  end
1417
1630
 
1418
1631
  def count_timer_object
@@ -1462,55 +1675,55 @@ class TailInputTest < Test::Unit::TestCase
1462
1675
  "rotate_wait" => "2s"
1463
1676
  }) + PARSE_SINGLE_LINE_CONFIG, false)
1464
1677
 
1465
- assert_equal readable_paths, d.instance.expand_paths.length
1466
- assert_equal result, d.instance.have_read_capability?
1678
+ assert_equal(readable_paths, d.instance.expand_paths.length)
1679
+ assert_equal(result, d.instance.have_read_capability?)
1467
1680
  end
1468
1681
  end
1469
1682
 
1470
1683
  def test_pos_file_dir_creation
1471
1684
  config = config_element("", "", {
1472
1685
  "tag" => "tail",
1473
- "path" => "#{TMP_DIR}/*.txt",
1686
+ "path" => "#{@tmp_dir}/*.txt",
1474
1687
  "format" => "none",
1475
- "pos_file" => "#{TMP_DIR}/pos/tail.pos",
1688
+ "pos_file" => "#{@tmp_dir}/pos/tail.pos",
1476
1689
  "read_from_head" => true,
1477
1690
  "refresh_interval" => 1
1478
1691
  })
1479
1692
 
1480
- assert_path_not_exist("#{TMP_DIR}/pos")
1693
+ assert_path_not_exist("#{@tmp_dir}/pos")
1481
1694
  d = create_driver(config, false)
1482
1695
  d.run
1483
- assert_path_exist("#{TMP_DIR}/pos")
1484
- assert_equal '755', File.stat("#{TMP_DIR}/pos").mode.to_s(8)[-3, 3]
1696
+ assert_path_exist("#{@tmp_dir}/pos")
1697
+ assert_equal('755', File.stat("#{@tmp_dir}/pos").mode.to_s(8)[-3, 3])
1485
1698
  ensure
1486
- cleanup_directory(TMP_DIR)
1699
+ cleanup_directory(@tmp_dir)
1487
1700
  end
1488
1701
 
1489
1702
  def test_pos_file_dir_creation_with_system_dir_permission
1490
1703
  config = config_element("", "", {
1491
1704
  "tag" => "tail",
1492
- "path" => "#{TMP_DIR}/*.txt",
1705
+ "path" => "#{@tmp_dir}/*.txt",
1493
1706
  "format" => "none",
1494
- "pos_file" => "#{TMP_DIR}/pos/tail.pos",
1707
+ "pos_file" => "#{@tmp_dir}/pos/tail.pos",
1495
1708
  "read_from_head" => true,
1496
1709
  "refresh_interval" => 1
1497
1710
  })
1498
1711
 
1499
- assert_path_not_exist("#{TMP_DIR}/pos")
1712
+ assert_path_not_exist("#{@tmp_dir}/pos")
1500
1713
 
1501
1714
  Fluent::SystemConfig.overwrite_system_config({ "dir_permission" => "744" }) do
1502
1715
  d = create_driver(config, false)
1503
1716
  d.run
1504
1717
  end
1505
1718
 
1506
- assert_path_exist("#{TMP_DIR}/pos")
1719
+ assert_path_exist("#{@tmp_dir}/pos")
1507
1720
  if Fluent.windows?
1508
- assert_equal '755', File.stat("#{TMP_DIR}/pos").mode.to_s(8)[-3, 3]
1721
+ assert_equal('755', File.stat("#{@tmp_dir}/pos").mode.to_s(8)[-3, 3])
1509
1722
  else
1510
- assert_equal '744', File.stat("#{TMP_DIR}/pos").mode.to_s(8)[-3, 3]
1723
+ assert_equal('744', File.stat("#{@tmp_dir}/pos").mode.to_s(8)[-3, 3])
1511
1724
  end
1512
1725
  ensure
1513
- cleanup_directory(TMP_DIR)
1726
+ cleanup_directory(@tmp_dir)
1514
1727
  end
1515
1728
 
1516
1729
  def test_z_refresh_watchers
@@ -1519,30 +1732,36 @@ class TailInputTest < Test::Unit::TestCase
1519
1732
  create_target_info('test/plugin/data/log/foo/bar.log'),
1520
1733
  create_target_info('test/plugin/data/log/test.log'),
1521
1734
  ]
1522
- plugin = create_driver(EX_CONFIG, false).instance
1735
+ plugin = create_driver(ex_config, false).instance
1523
1736
  sio = StringIO.new
1524
1737
  plugin.instance_eval do
1525
1738
  @pf = Fluent::Plugin::TailInput::PositionFile.load(sio, EX_FOLLOW_INODES, {}, logger: $log)
1526
1739
  @loop = Coolio::Loop.new
1740
+ opened_file_metrics = Fluent::Plugin::LocalMetrics.new
1741
+ opened_file_metrics.configure(config_element('metrics', '', {}))
1742
+ closed_file_metrics = Fluent::Plugin::LocalMetrics.new
1743
+ closed_file_metrics.configure(config_element('metrics', '', {}))
1744
+ rotated_file_metrics = Fluent::Plugin::LocalMetrics.new
1745
+ rotated_file_metrics.configure(config_element('metrics', '', {}))
1746
+ @metrics = Fluent::Plugin::TailInput::MetricsInfo.new(opened_file_metrics, closed_file_metrics, rotated_file_metrics)
1527
1747
  end
1528
1748
 
1529
1749
  Timecop.freeze(2010, 1, 2, 3, 4, 5) do
1530
1750
  ex_paths.each do |target_info|
1531
- mock.proxy(Fluent::Plugin::TailInput::TailWatcher).new(target_info, anything, anything, true, false, anything, nil, anything).once
1751
+ mock.proxy(Fluent::Plugin::TailInput::TailWatcher).new(target_info, anything, anything, true, false, anything, nil, anything, anything).once
1532
1752
  end
1533
1753
 
1534
1754
  plugin.refresh_watchers
1535
1755
  end
1536
1756
 
1537
1757
  path = 'test/plugin/data/2010/01/20100102-030405.log'
1538
- target_info = Fluent::Plugin::TailInput::TargetInfo.new(path, Fluent::FileWrapper.stat(path).ino)
1539
- mock.proxy(plugin).detach_watcher_after_rotate_wait(plugin.instance_variable_get(:@tails)[target_info], target_info.ino)
1758
+ mock.proxy(plugin).detach_watcher_after_rotate_wait(plugin.instance_variable_get(:@tails)[path], Fluent::FileWrapper.stat(path).ino)
1540
1759
 
1541
1760
  Timecop.freeze(2010, 1, 2, 3, 4, 6) do
1542
1761
  path = "test/plugin/data/2010/01/20100102-030406.log"
1543
1762
  inode = Fluent::FileWrapper.stat(path).ino
1544
1763
  target_info = Fluent::Plugin::TailInput::TargetInfo.new(path, inode)
1545
- mock.proxy(Fluent::Plugin::TailInput::TailWatcher).new(target_info, anything, anything, true, false, anything, nil, anything).once
1764
+ mock.proxy(Fluent::Plugin::TailInput::TailWatcher).new(target_info, anything, anything, true, false, anything, nil, anything, anything).once
1546
1765
  plugin.refresh_watchers
1547
1766
 
1548
1767
  flexstub(Fluent::Plugin::TailInput::TailWatcher) do |watcherclass|
@@ -1556,9 +1775,9 @@ class TailInputTest < Test::Unit::TestCase
1556
1775
  test 'type of pos_file_compaction_interval is time' do
1557
1776
  tail = {
1558
1777
  "tag" => "tail",
1559
- "path" => "#{TMP_DIR}/*.txt",
1778
+ "path" => "#{@tmp_dir}/*.txt",
1560
1779
  "format" => "none",
1561
- "pos_file" => "#{TMP_DIR}/pos/tail.pos",
1780
+ "pos_file" => "#{@tmp_dir}/pos/tail.pos",
1562
1781
  "refresh_interval" => 1,
1563
1782
  "read_from_head" => true,
1564
1783
  'pos_file_compaction_interval' => '24h',
@@ -1575,7 +1794,7 @@ class TailInputTest < Test::Unit::TestCase
1575
1794
  DummyWatcher = Struct.new("DummyWatcher", :tag)
1576
1795
 
1577
1796
  def test_tag
1578
- d = create_driver(EX_CONFIG, false)
1797
+ d = create_driver(ex_config, false)
1579
1798
  d.run {}
1580
1799
  plugin = d.instance
1581
1800
  mock(plugin.router).emit_stream('tail', anything).once
@@ -1651,12 +1870,47 @@ class TailInputTest < Test::Unit::TestCase
1651
1870
  mock(plugin.router).emit_stream('pre.foo.bar.log.post', anything).once
1652
1871
  plugin.receive_lines(['foo', 'bar'], DummyWatcher.new('foo.bar.log'))
1653
1872
  end
1873
+
1874
+ data(
1875
+ small: ["128", 128],
1876
+ KiB: ["1k", 1024]
1877
+ )
1878
+ test 'max_line_size' do |(label, size)|
1879
+ config = config_element("", "", {
1880
+ "tag" => "max_line_size",
1881
+ "path" => "#{@tmp_dir}/with_long_lines.txt",
1882
+ "format" => "none",
1883
+ "read_from_head" => true,
1884
+ "max_line_size" => label,
1885
+ "log_level" => "debug"
1886
+ })
1887
+ Fluent::FileWrapper.open("#{@tmp_dir}/with_long_lines.txt", "w+") do |f|
1888
+ f.puts "foo"
1889
+ f.puts "x" * size # 'x' * size + \n > @max_line_size
1890
+ f.puts "bar"
1891
+ end
1892
+ d = create_driver(config, false)
1893
+ timestamp = Time.parse("Mon Nov 29 11:22:33 UTC 2021")
1894
+ Timecop.freeze(timestamp)
1895
+ d.run(expect_records: 2)
1896
+ assert_equal([
1897
+ [{"message" => "foo"},{"message" => "bar"}],
1898
+ [
1899
+ "2021-11-29 11:22:33 +0000 [warn]: received line length is longer than #{size}\n",
1900
+ "2021-11-29 11:22:33 +0000 [debug]: skipped line: #{'x' * size}\n"
1901
+ ]
1902
+ ],
1903
+ [
1904
+ d.events.collect { |event| event.last },
1905
+ d.logs[-2..]
1906
+ ])
1907
+ end
1654
1908
  end
1655
1909
 
1656
1910
  # Ensure that no fatal exception is raised when a file is missing and that
1657
1911
  # files that do exist are still tailed as expected.
1658
1912
  def test_missing_file
1659
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
1913
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
1660
1914
  f.puts "test1"
1661
1915
  f.puts "test2"
1662
1916
  }
@@ -1665,16 +1919,16 @@ class TailInputTest < Test::Unit::TestCase
1665
1919
  # since their interactions with the filesystem differ.
1666
1920
  config1 = config_element("", "", {
1667
1921
  "tag" => "t1",
1668
- "path" => "#{TMP_DIR}/non_existent_file.txt,#{TMP_DIR}/tail.txt",
1922
+ "path" => "#{@tmp_dir}/non_existent_file.txt,#{@tmp_dir}/tail.txt",
1669
1923
  "format" => "none",
1670
1924
  "rotate_wait" => "2s",
1671
- "pos_file" => "#{TMP_DIR}/tail.pos"
1925
+ "pos_file" => "#{@tmp_dir}/tail.pos"
1672
1926
  })
1673
1927
  config2 = config1 + config_element("", "", { "read_from_head" => true })
1674
1928
  [config1, config2].each do |config|
1675
1929
  d = create_driver(config, false)
1676
1930
  d.run(expect_emits: 1) do
1677
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f|
1931
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f|
1678
1932
  f.puts "test3"
1679
1933
  f.puts "test4"
1680
1934
  }
@@ -1690,19 +1944,19 @@ class TailInputTest < Test::Unit::TestCase
1690
1944
 
1691
1945
  sub_test_case 'inode_processing' do
1692
1946
  def test_should_delete_file_pos_entry_for_non_existing_file_with_follow_inodes
1693
- config = COMMON_FOLLOW_INODE_CONFIG
1947
+ config = common_follow_inode_config
1694
1948
 
1695
- path = "#{TMP_DIR}/tail.txt"
1949
+ path = "#{@tmp_dir}/tail.txt"
1696
1950
  ino = 1
1697
1951
  pos = 1234
1698
- File.open("#{TMP_DIR}/tail.pos", "wb") {|f|
1952
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.pos", "wb") {|f|
1699
1953
  f.puts ("%s\t%016x\t%016x\n" % [path, pos, ino])
1700
1954
  }
1701
1955
 
1702
1956
  d = create_driver(config, false)
1703
1957
  d.run
1704
1958
 
1705
- pos_file = File.open("#{TMP_DIR}/tail.pos", "r")
1959
+ pos_file = Fluent::FileWrapper.open("#{@tmp_dir}/tail.pos", "r")
1706
1960
  pos_file.pos = 0
1707
1961
 
1708
1962
  assert_raise(EOFError) do
@@ -1711,21 +1965,21 @@ class TailInputTest < Test::Unit::TestCase
1711
1965
  end
1712
1966
 
1713
1967
  def test_should_write_latest_offset_after_rotate_wait
1714
- config = COMMON_FOLLOW_INODE_CONFIG
1715
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
1968
+ config = common_follow_inode_config
1969
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
1716
1970
  f.puts "test1"
1717
1971
  f.puts "test2"
1718
1972
  }
1719
1973
 
1720
1974
  d = create_driver(config, false)
1721
1975
  d.run(expect_emits: 2, shutdown: false) do
1722
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f| f.puts "test3\n"}
1723
- FileUtils.move("#{TMP_DIR}/tail.txt", "#{TMP_DIR}/tail.txt" + "1")
1976
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f| f.puts "test3\n"}
1977
+ FileUtils.move("#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt" + "1")
1724
1978
  sleep 1
1725
- File.open("#{TMP_DIR}/tail.txt" + "1", "ab") {|f| f.puts "test4\n"}
1979
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt" + "1", "ab") {|f| f.puts "test4\n"}
1726
1980
  end
1727
1981
 
1728
- pos_file = File.open("#{TMP_DIR}/tail.pos", "r")
1982
+ pos_file = Fluent::FileWrapper.open("#{@tmp_dir}/tail.pos", "r")
1729
1983
  pos_file.pos = 0
1730
1984
  line_parts = /^([^\t]+)\t([0-9a-fA-F]+)\t([0-9a-fA-F]+)/.match(pos_file.readline)
1731
1985
  waiting(5) {
@@ -1739,47 +1993,28 @@ class TailInputTest < Test::Unit::TestCase
1739
1993
  d.instance_shutdown
1740
1994
  end
1741
1995
 
1742
- def test_should_keep_and_update_existing_file_pos_entry_for_deleted_file_when_new_file_with_same_name_created
1996
+ def test_should_remove_deleted_file
1743
1997
  config = config_element("", "", {"format" => "none"})
1744
1998
 
1745
- path = "#{TMP_DIR}/tail.txt"
1999
+ path = "#{@tmp_dir}/tail.txt"
1746
2000
  ino = 1
1747
2001
  pos = 1234
1748
- File.open("#{TMP_DIR}/tail.pos", "wb") {|f|
2002
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.pos", "wb") {|f|
1749
2003
  f.puts ("%s\t%016x\t%016x\n" % [path, pos, ino])
1750
2004
  }
1751
2005
 
1752
2006
  d = create_driver(config)
1753
- d.run(shutdown: false)
1754
-
1755
- pos_file = File.open("#{TMP_DIR}/tail.pos", "r")
1756
- pos_file.pos = 0
1757
-
1758
- path_pos_ino = /^([^\t]+)\t([0-9a-fA-F]+)\t([0-9a-fA-F]+)/.match(pos_file.readline)
1759
- assert_equal(path, path_pos_ino[1])
1760
- assert_equal(pos, path_pos_ino[2].to_i(16))
1761
- assert_equal(ino, path_pos_ino[3].to_i(16))
1762
-
1763
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
1764
- f.puts "test1"
1765
- f.puts "test2"
1766
- }
1767
- Timecop.travel(Time.now + 10) do
1768
- sleep 5
2007
+ d.run do
2008
+ pos_file = Fluent::FileWrapper.open("#{@tmp_dir}/tail.pos", "r")
1769
2009
  pos_file.pos = 0
1770
- tuple = create_target_info("#{TMP_DIR}/tail.txt")
1771
- path_pos_ino = /^([^\t]+)\t([0-9a-fA-F]+)\t([0-9a-fA-F]+)/.match(pos_file.readline)
1772
- assert_equal(tuple.path, path_pos_ino[1])
1773
- assert_equal(12, path_pos_ino[2].to_i(16))
1774
- assert_equal(tuple.ino, path_pos_ino[3].to_i(16))
2010
+ assert_equal([], pos_file.readlines)
1775
2011
  end
1776
- d.instance_shutdown
1777
2012
  end
1778
2013
 
1779
2014
  def test_should_mark_file_unwatched_after_limit_recently_modified_and_rotate_wait
1780
2015
  config = config_element("ROOT", "", {
1781
- "path" => "#{TMP_DIR}/tail.txt*",
1782
- "pos_file" => "#{TMP_DIR}/tail.pos",
2016
+ "path" => "#{@tmp_dir}/tail.txt*",
2017
+ "pos_file" => "#{@tmp_dir}/tail.pos",
1783
2018
  "tag" => "t1",
1784
2019
  "rotate_wait" => "1s",
1785
2020
  "refresh_interval" => "1s",
@@ -1791,14 +2026,14 @@ class TailInputTest < Test::Unit::TestCase
1791
2026
 
1792
2027
  d = create_driver(config, false)
1793
2028
 
1794
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
2029
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
1795
2030
  f.puts "test1"
1796
2031
  f.puts "test2"
1797
2032
  }
1798
- target_info = create_target_info("#{TMP_DIR}/tail.txt")
2033
+ target_info = create_target_info("#{@tmp_dir}/tail.txt")
1799
2034
 
1800
2035
  d.run(expect_emits: 1, shutdown: false) do
1801
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f| f.puts "test3\n"}
2036
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f| f.puts "test3\n"}
1802
2037
  end
1803
2038
 
1804
2039
 
@@ -1816,8 +2051,8 @@ class TailInputTest < Test::Unit::TestCase
1816
2051
 
1817
2052
  def test_should_read_from_head_on_file_renaming_with_star_in_pattern
1818
2053
  config = config_element("ROOT", "", {
1819
- "path" => "#{TMP_DIR}/tail.txt*",
1820
- "pos_file" => "#{TMP_DIR}/tail.pos",
2054
+ "path" => "#{@tmp_dir}/tail.txt*",
2055
+ "pos_file" => "#{@tmp_dir}/tail.pos",
1821
2056
  "tag" => "t1",
1822
2057
  "rotate_wait" => "10s",
1823
2058
  "refresh_interval" => "1s",
@@ -1829,14 +2064,14 @@ class TailInputTest < Test::Unit::TestCase
1829
2064
 
1830
2065
  d = create_driver(config, false)
1831
2066
 
1832
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
2067
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
1833
2068
  f.puts "test1"
1834
2069
  f.puts "test2"
1835
2070
  }
1836
2071
 
1837
2072
  d.run(expect_emits: 2, shutdown: false) do
1838
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f| f.puts "test3\n"}
1839
- FileUtils.move("#{TMP_DIR}/tail.txt", "#{TMP_DIR}/tail.txt1")
2073
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f| f.puts "test3\n"}
2074
+ FileUtils.move("#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt1")
1840
2075
  end
1841
2076
 
1842
2077
  events = d.events
@@ -1845,20 +2080,20 @@ class TailInputTest < Test::Unit::TestCase
1845
2080
  end
1846
2081
 
1847
2082
  def test_should_not_read_from_head_on_rotation_when_watching_inodes
1848
- config = COMMON_FOLLOW_INODE_CONFIG
2083
+ config = common_follow_inode_config
1849
2084
 
1850
2085
  d = create_driver(config, false)
1851
2086
 
1852
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
2087
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
1853
2088
  f.puts "test1"
1854
2089
  f.puts "test2"
1855
2090
  }
1856
2091
 
1857
2092
  d.run(expect_emits: 1, shutdown: false) do
1858
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f| f.puts "test3\n"}
2093
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f| f.puts "test3\n"}
1859
2094
  end
1860
2095
 
1861
- FileUtils.move("#{TMP_DIR}/tail.txt", "#{TMP_DIR}/tail.txt1")
2096
+ FileUtils.move("#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt1")
1862
2097
  Timecop.travel(Time.now + 10) do
1863
2098
  sleep 2
1864
2099
  events = d.events
@@ -1869,23 +2104,23 @@ class TailInputTest < Test::Unit::TestCase
1869
2104
  end
1870
2105
 
1871
2106
  def test_should_mark_file_unwatched_if_same_name_file_created_with_different_inode
1872
- config = COMMON_FOLLOW_INODE_CONFIG
2107
+ config = common_follow_inode_config
1873
2108
 
1874
2109
  d = create_driver(config, false)
1875
2110
 
1876
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
2111
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
1877
2112
  f.puts "test1"
1878
2113
  f.puts "test2"
1879
2114
  }
1880
- target_info = create_target_info("#{TMP_DIR}/tail.txt")
2115
+ target_info = create_target_info("#{@tmp_dir}/tail.txt")
1881
2116
 
1882
2117
  d.run(expect_emits: 2, shutdown: false) do
1883
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f| f.puts "test3\n"}
1884
- cleanup_file("#{TMP_DIR}/tail.txt")
1885
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f| f.puts "test4\n"}
2118
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f| f.puts "test3\n"}
2119
+ cleanup_file("#{@tmp_dir}/tail.txt")
2120
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f| f.puts "test4\n"}
1886
2121
  end
1887
2122
 
1888
- new_target_info = create_target_info("#{TMP_DIR}/tail.txt")
2123
+ new_target_info = create_target_info("#{@tmp_dir}/tail.txt")
1889
2124
 
1890
2125
  pos_file = d.instance.instance_variable_get(:@pf)
1891
2126
 
@@ -1901,56 +2136,65 @@ class TailInputTest < Test::Unit::TestCase
1901
2136
 
1902
2137
  def test_should_close_watcher_after_rotate_wait
1903
2138
  now = Time.now
1904
- config = COMMON_FOLLOW_INODE_CONFIG + config_element('', '', {"rotate_wait" => "1s", "limit_recently_modified" => "1s"})
2139
+ config = common_follow_inode_config + config_element('', '', {"rotate_wait" => "1s", "limit_recently_modified" => "1s"})
1905
2140
 
1906
2141
  d = create_driver(config, false)
2142
+ d.instance.instance_eval do
2143
+ opened_file_metrics = Fluent::Plugin::LocalMetrics.new
2144
+ opened_file_metrics.configure(config_element('metrics', '', {}))
2145
+ closed_file_metrics = Fluent::Plugin::LocalMetrics.new
2146
+ closed_file_metrics.configure(config_element('metrics', '', {}))
2147
+ rotated_file_metrics = Fluent::Plugin::LocalMetrics.new
2148
+ rotated_file_metrics.configure(config_element('metrics', '', {}))
2149
+ @metrics = Fluent::Plugin::TailInput::MetricsInfo.new(opened_file_metrics, closed_file_metrics, rotated_file_metrics)
2150
+ end
1907
2151
 
1908
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
2152
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
1909
2153
  f.puts "test1"
1910
2154
  f.puts "test2"
1911
2155
  }
1912
- target_info = create_target_info("#{TMP_DIR}/tail.txt")
1913
- mock.proxy(Fluent::Plugin::TailInput::TailWatcher).new(target_info, anything, anything, true, true, anything, nil, anything).once
2156
+ target_info = create_target_info("#{@tmp_dir}/tail.txt")
2157
+ mock.proxy(Fluent::Plugin::TailInput::TailWatcher).new(target_info, anything, anything, true, true, anything, nil, anything, anything).once
1914
2158
  d.run(shutdown: false)
1915
- assert d.instance.instance_variable_get(:@tails)[target_info]
2159
+ assert d.instance.instance_variable_get(:@tails)[target_info.path]
1916
2160
 
1917
2161
  Timecop.travel(now + 10) do
1918
2162
  d.instance.instance_eval do
1919
- sleep 0.1 until @tails[target_info] == nil
2163
+ sleep 0.1 until @tails[target_info.path] == nil
1920
2164
  end
1921
- assert_nil d.instance.instance_variable_get(:@tails)[target_info]
2165
+ assert_nil d.instance.instance_variable_get(:@tails)[target_info.path]
1922
2166
  end
1923
2167
  d.instance_shutdown
1924
2168
  end
1925
2169
 
1926
2170
  def test_should_create_new_watcher_for_new_file_with_same_name
1927
2171
  now = Time.now
1928
- config = COMMON_FOLLOW_INODE_CONFIG + config_element('', '', {"limit_recently_modified" => "2s"})
2172
+ config = common_follow_inode_config + config_element('', '', {"limit_recently_modified" => "2s"})
1929
2173
 
1930
2174
  d = create_driver(config, false)
1931
2175
 
1932
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
2176
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
1933
2177
  f.puts "test1"
1934
2178
  f.puts "test2"
1935
2179
  }
1936
- path_ino = create_target_info("#{TMP_DIR}/tail.txt")
2180
+ path_ino = create_target_info("#{@tmp_dir}/tail.txt")
1937
2181
 
1938
2182
  d.run(expect_emits: 1, shutdown: false) do
1939
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f| f.puts "test3\n"}
2183
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f| f.puts "test3\n"}
1940
2184
  end
1941
2185
 
1942
- cleanup_file("#{TMP_DIR}/tail.txt")
1943
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
2186
+ cleanup_file("#{@tmp_dir}/tail.txt")
2187
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
1944
2188
  f.puts "test3"
1945
2189
  f.puts "test4"
1946
2190
  }
1947
- new_path_ino = create_target_info("#{TMP_DIR}/tail.txt")
2191
+ new_path_ino = create_target_info("#{@tmp_dir}/tail.txt")
1948
2192
 
1949
2193
  Timecop.travel(now + 10) do
1950
2194
  sleep 3
1951
2195
  d.instance.instance_eval do
1952
- @tails[path_ino] == nil
1953
- @tails[new_path_ino] != nil
2196
+ @tails[path_ino.path] == nil
2197
+ @tails[new_path_ino.path] != nil
1954
2198
  end
1955
2199
  end
1956
2200
 
@@ -1962,19 +2206,19 @@ class TailInputTest < Test::Unit::TestCase
1962
2206
  end
1963
2207
 
1964
2208
  def test_truncate_file_with_follow_inodes
1965
- config = COMMON_FOLLOW_INODE_CONFIG
2209
+ config = common_follow_inode_config
1966
2210
 
1967
2211
  d = create_driver(config, false)
1968
2212
 
1969
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
2213
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
1970
2214
  f.puts "test1"
1971
2215
  f.puts "test2"
1972
2216
  }
1973
2217
 
1974
2218
  d.run(expect_emits: 3, shutdown: false) do
1975
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f| f.puts "test3\n"}
2219
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f| f.puts "test3\n"}
1976
2220
  sleep 2
1977
- File.open("#{TMP_DIR}/tail.txt", "w+b") {|f| f.puts "test4\n"}
2221
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "w+b") {|f| f.puts "test4\n"}
1978
2222
  end
1979
2223
 
1980
2224
  events = d.events
@@ -1988,15 +2232,15 @@ class TailInputTest < Test::Unit::TestCase
1988
2232
 
1989
2233
  # issue #3464
1990
2234
  def test_should_replace_target_info
1991
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
2235
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
1992
2236
  f.puts "test1\n"
1993
2237
  }
1994
- target_info = create_target_info("#{TMP_DIR}/tail.txt")
2238
+ target_info = create_target_info("#{@tmp_dir}/tail.txt")
1995
2239
  inodes = []
1996
2240
 
1997
2241
  config = config_element("ROOT", "", {
1998
- "path" => "#{TMP_DIR}/tail.txt*",
1999
- "pos_file" => "#{TMP_DIR}/tail.pos",
2242
+ "path" => "#{@tmp_dir}/tail.txt*",
2243
+ "pos_file" => "#{@tmp_dir}/tail.pos",
2000
2244
  "tag" => "t1",
2001
2245
  "refresh_interval" => "60s",
2002
2246
  "read_from_head" => "true",
@@ -2009,21 +2253,21 @@ class TailInputTest < Test::Unit::TestCase
2009
2253
  while d.events.size < 1 do
2010
2254
  sleep 0.1
2011
2255
  end
2012
- inodes = d.instance.instance_variable_get(:@tails).keys.collect do |key|
2013
- key.ino
2256
+ inodes = d.instance.instance_variable_get(:@tails).values.collect do |tw|
2257
+ tw.ino
2014
2258
  end
2015
2259
  assert_equal([target_info.ino], inodes)
2016
2260
 
2017
- cleanup_file("#{TMP_DIR}/tail.txt")
2018
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f| f.puts "test2\n"}
2261
+ cleanup_file("#{@tmp_dir}/tail.txt")
2262
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f| f.puts "test2\n"}
2019
2263
 
2020
2264
  while d.events.size < 2 do
2021
2265
  sleep 0.1
2022
2266
  end
2023
- inodes = d.instance.instance_variable_get(:@tails).keys.collect do |key|
2024
- key.ino
2267
+ inodes = d.instance.instance_variable_get(:@tails).values.collect do |tw|
2268
+ tw.ino
2025
2269
  end
2026
- new_target_info = create_target_info("#{TMP_DIR}/tail.txt")
2270
+ new_target_info = create_target_info("#{@tmp_dir}/tail.txt")
2027
2271
  assert_not_equal(target_info.ino, new_target_info.ino)
2028
2272
  assert_equal([new_target_info.ino], inodes)
2029
2273
  end
@@ -2032,7 +2276,7 @@ class TailInputTest < Test::Unit::TestCase
2032
2276
 
2033
2277
  sub_test_case "tail_path" do
2034
2278
  def test_tail_path_with_singleline
2035
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
2279
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f|
2036
2280
  f.puts "test1"
2037
2281
  f.puts "test2"
2038
2282
  }
@@ -2040,7 +2284,7 @@ class TailInputTest < Test::Unit::TestCase
2040
2284
  d = create_driver(SINGLE_LINE_CONFIG + config_element("", "", { "path_key" => "path" }))
2041
2285
 
2042
2286
  d.run(expect_emits: 1) do
2043
- File.open("#{TMP_DIR}/tail.txt", "ab") {|f|
2287
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f|
2044
2288
  f.puts "test3"
2045
2289
  f.puts "test4"
2046
2290
  }
@@ -2049,12 +2293,12 @@ class TailInputTest < Test::Unit::TestCase
2049
2293
  events = d.events
2050
2294
  assert_equal(true, events.length > 0)
2051
2295
  events.each do |emit|
2052
- assert_equal("#{TMP_DIR}/tail.txt", emit[2]["path"])
2296
+ assert_equal("#{@tmp_dir}/tail.txt", emit[2]["path"])
2053
2297
  end
2054
2298
  end
2055
2299
 
2056
2300
  def test_tail_path_with_multiline_with_firstline
2057
- File.open("#{TMP_DIR}/tail.txt", "wb") { |f| }
2301
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") { |f| }
2058
2302
 
2059
2303
  config = config_element("", "", {
2060
2304
  "path_key" => "path",
@@ -2064,7 +2308,7 @@ class TailInputTest < Test::Unit::TestCase
2064
2308
  })
2065
2309
  d = create_driver(config)
2066
2310
  d.run(expect_emits: 1) do
2067
- File.open("#{TMP_DIR}/tail.txt", "ab") { |f|
2311
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") { |f|
2068
2312
  f.puts "f test1"
2069
2313
  f.puts "s test2"
2070
2314
  f.puts "f test3"
@@ -2079,12 +2323,12 @@ class TailInputTest < Test::Unit::TestCase
2079
2323
  events = d.events
2080
2324
  assert_equal(4, events.length)
2081
2325
  events.each do |emit|
2082
- assert_equal("#{TMP_DIR}/tail.txt", emit[2]["path"])
2326
+ assert_equal("#{@tmp_dir}/tail.txt", emit[2]["path"])
2083
2327
  end
2084
2328
  end
2085
2329
 
2086
2330
  def test_tail_path_with_multiline_without_firstline
2087
- File.open("#{TMP_DIR}/tail.txt", "wb") { |f| }
2331
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") { |f| }
2088
2332
 
2089
2333
  config = config_element("", "", {
2090
2334
  "path_key" => "path",
@@ -2095,7 +2339,7 @@ class TailInputTest < Test::Unit::TestCase
2095
2339
  })
2096
2340
  d = create_driver(config)
2097
2341
  d.run(expect_emits: 1) do
2098
- File.open("#{TMP_DIR}/tail.txt", "ab") { |f|
2342
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") { |f|
2099
2343
  f.puts "foo 1"
2100
2344
  f.puts "bar 1"
2101
2345
  f.puts "baz 1"
@@ -2105,7 +2349,7 @@ class TailInputTest < Test::Unit::TestCase
2105
2349
  events = d.events
2106
2350
  assert(events.length > 0)
2107
2351
  events.each do |emit|
2108
- assert_equal("#{TMP_DIR}/tail.txt", emit[2]["path"])
2352
+ assert_equal("#{@tmp_dir}/tail.txt", emit[2]["path"])
2109
2353
  end
2110
2354
  end
2111
2355
 
@@ -2113,8 +2357,8 @@ class TailInputTest < Test::Unit::TestCase
2113
2357
  if ENV["APPVEYOR"] && Fluent.windows?
2114
2358
  omit "This testcase is unstable on AppVeyor."
2115
2359
  end
2116
- files = ["#{TMP_DIR}/tail1.txt", "#{TMP_DIR}/tail2.txt"]
2117
- files.each { |file| File.open(file, "wb") { |f| } }
2360
+ files = ["#{@tmp_dir}/tail1.txt", "#{@tmp_dir}/tail2.txt"]
2361
+ files.each { |file| Fluent::FileWrapper.open(file, "wb") { |f| } }
2118
2362
 
2119
2363
  config = config_element("", "", {
2120
2364
  "path" => "#{files[0]},#{files[1]}",
@@ -2127,7 +2371,7 @@ class TailInputTest < Test::Unit::TestCase
2127
2371
  d = create_driver(config, false)
2128
2372
  d.run(expect_emits: 2) do
2129
2373
  files.each do |file|
2130
- File.open(file, 'ab') { |f|
2374
+ Fluent::FileWrapper.open(file, 'ab') { |f|
2131
2375
  f.puts "f #{file} line should be ignored"
2132
2376
  f.puts "s test1"
2133
2377
  f.puts "f test2"
@@ -2147,30 +2391,30 @@ class TailInputTest < Test::Unit::TestCase
2147
2391
 
2148
2392
  def test_limit_recently_modified
2149
2393
  now = Time.new(2010, 1, 2, 3, 4, 5)
2150
- FileUtils.touch("#{TMP_DIR}/tail_unwatch.txt", mtime: (now - 3601))
2151
- FileUtils.touch("#{TMP_DIR}/tail_watch1.txt", mtime: (now - 3600))
2152
- FileUtils.touch("#{TMP_DIR}/tail_watch2.txt", mtime: now)
2394
+ FileUtils.touch("#{@tmp_dir}/tail_unwatch.txt", mtime: (now - 3601))
2395
+ FileUtils.touch("#{@tmp_dir}/tail_watch1.txt", mtime: (now - 3600))
2396
+ FileUtils.touch("#{@tmp_dir}/tail_watch2.txt", mtime: now)
2153
2397
 
2154
2398
  config = config_element('', '', {
2155
2399
  'tag' => 'tail',
2156
- 'path' => "#{TMP_DIR}/*.txt",
2400
+ 'path' => "#{@tmp_dir}/*.txt",
2157
2401
  'format' => 'none',
2158
2402
  'limit_recently_modified' => '3600s'
2159
2403
  })
2160
2404
 
2161
2405
  expected_files = [
2162
- create_target_info("#{TMP_DIR}/tail_watch1.txt"),
2163
- create_target_info("#{TMP_DIR}/tail_watch2.txt")
2406
+ create_target_info("#{@tmp_dir}/tail_watch1.txt"),
2407
+ create_target_info("#{@tmp_dir}/tail_watch2.txt")
2164
2408
  ]
2165
2409
 
2166
2410
  Timecop.freeze(now) do
2167
2411
  plugin = create_driver(config, false).instance
2168
- assert_equal expected_files, plugin.expand_paths.values.sort_by { |path_ino| path_ino.path }
2412
+ assert_equal(expected_files, plugin.expand_paths.values.sort_by { |path_ino| path_ino.path })
2169
2413
  end
2170
2414
  end
2171
2415
 
2172
2416
  def test_skip_refresh_on_startup
2173
- FileUtils.touch("#{TMP_DIR}/tail.txt")
2417
+ FileUtils.touch("#{@tmp_dir}/tail.txt")
2174
2418
  config = config_element('', '', {
2175
2419
  'format' => 'none',
2176
2420
  'refresh_interval' => 1,
@@ -2178,36 +2422,42 @@ class TailInputTest < Test::Unit::TestCase
2178
2422
  })
2179
2423
  d = create_driver(config)
2180
2424
  d.run(shutdown: false) {}
2181
- assert_equal 0, d.instance.instance_variable_get(:@tails).keys.size
2425
+ assert_equal(0, d.instance.instance_variable_get(:@tails).keys.size)
2182
2426
  # detect a file at first execution of in_tail_refresh_watchers timer
2183
2427
  waiting(5) { sleep 0.1 until d.instance.instance_variable_get(:@tails).keys.size == 1 }
2184
2428
  d.instance_shutdown
2185
2429
  end
2186
2430
 
2187
2431
  def test_ENOENT_error_after_setup_watcher
2188
- path = "#{TMP_DIR}/tail.txt"
2432
+ path = "#{@tmp_dir}/tail.txt"
2189
2433
  FileUtils.touch(path)
2190
2434
  config = config_element('', '', {
2191
2435
  'format' => 'none',
2192
2436
  })
2193
2437
  d = create_driver(config)
2194
- mock.proxy(d.instance).setup_watcher(anything, anything) do |tw|
2195
- cleanup_file(path)
2196
- tw
2197
- end
2438
+ file_deleted = false
2439
+ mock.proxy(d.instance).existence_path do |hash|
2440
+ unless file_deleted
2441
+ cleanup_file(path)
2442
+ file_deleted = true
2443
+ end
2444
+ hash
2445
+ end.twice
2198
2446
  assert_nothing_raised do
2199
2447
  d.run(shutdown: false) {}
2200
2448
  end
2201
- d.instance_shutdown
2202
- assert($log.out.logs.any?{|log| log.include?("stat() for #{path} failed with Errno::ENOENT. Drop tail watcher for now.\n") })
2449
+ assert($log.out.logs.any?{|log| log.include?("stat() for #{path} failed. Continuing without tailing it.\n") },
2450
+ $log.out.logs.join("\n"))
2451
+ ensure
2452
+ d.instance_shutdown if d && d.instance
2203
2453
  end
2204
2454
 
2205
2455
  def test_EACCES_error_after_setup_watcher
2206
2456
  omit "Cannot test with root user" if Process::UID.eid == 0
2207
- path = "#{TMP_DIR}/noaccess/tail.txt"
2457
+ path = "#{@tmp_dir}/noaccess/tail.txt"
2208
2458
  begin
2209
- FileUtils.mkdir_p("#{TMP_DIR}/noaccess")
2210
- FileUtils.chmod(0755, "#{TMP_DIR}/noaccess")
2459
+ FileUtils.mkdir_p("#{@tmp_dir}/noaccess")
2460
+ FileUtils.chmod(0755, "#{@tmp_dir}/noaccess")
2211
2461
  FileUtils.touch(path)
2212
2462
  config = config_element('', '', {
2213
2463
  'tag' => "tail",
@@ -2215,25 +2465,26 @@ class TailInputTest < Test::Unit::TestCase
2215
2465
  'format' => 'none',
2216
2466
  })
2217
2467
  d = create_driver(config, false)
2218
- mock.proxy(d.instance).setup_watcher(anything, anything) do |tw|
2219
- FileUtils.chmod(0000, "#{TMP_DIR}/noaccess")
2220
- tw
2221
- end
2468
+ mock.proxy(d.instance).existence_path do |hash|
2469
+ FileUtils.chmod(0000, "#{@tmp_dir}/noaccess")
2470
+ hash
2471
+ end.twice
2222
2472
  assert_nothing_raised do
2223
2473
  d.run(shutdown: false) {}
2224
2474
  end
2225
- d.instance_shutdown
2226
- assert($log.out.logs.any?{|log| log.include?("stat() for #{path} failed with Errno::EACCES. Drop tail watcher for now.\n") })
2475
+ assert($log.out.logs.any?{|log| log.include?("stat() for #{path} failed. Continuing without tailing it.\n") },
2476
+ $log.out.logs.join("\n"))
2227
2477
  end
2228
2478
  ensure
2229
- if File.exist?("#{TMP_DIR}/noaccess")
2230
- FileUtils.chmod(0755, "#{TMP_DIR}/noaccess")
2231
- FileUtils.rm_rf("#{TMP_DIR}/noaccess")
2479
+ d.instance_shutdown if d && d.instance
2480
+ if File.exist?("#{@tmp_dir}/noaccess")
2481
+ FileUtils.chmod(0755, "#{@tmp_dir}/noaccess")
2482
+ FileUtils.rm_rf("#{@tmp_dir}/noaccess")
2232
2483
  end
2233
2484
  end unless Fluent.windows?
2234
2485
 
2235
2486
  def test_EACCES
2236
- path = "#{TMP_DIR}/tail.txt"
2487
+ path = "#{@tmp_dir}/tail.txt"
2237
2488
  FileUtils.touch(path)
2238
2489
  config = config_element('', '', {
2239
2490
  'format' => 'none',
@@ -2245,13 +2496,14 @@ class TailInputTest < Test::Unit::TestCase
2245
2496
  assert_nothing_raised do
2246
2497
  d.run(shutdown: false) {}
2247
2498
  end
2248
- d.instance_shutdown
2249
2499
  assert($log.out.logs.any?{|log| log.include?("expand_paths: stat() for #{path} failed with Errno::EACCES. Skip file.\n") })
2500
+ ensure
2501
+ d.instance_shutdown if d && d.instance
2250
2502
  end
2251
2503
 
2252
2504
  def test_shutdown_timeout
2253
- path = "#{TMP_DIR}/tail.txt"
2254
- File.open("#{TMP_DIR}/tail.txt", "wb") do |f|
2505
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") do |f|
2506
+ # Should be large enough to take too long time to consume
2255
2507
  (1024 * 1024 * 5).times do
2256
2508
  f.puts "{\"test\":\"fizzbuzz\"}"
2257
2509
  end
@@ -2263,18 +2515,774 @@ class TailInputTest < Test::Unit::TestCase
2263
2515
  'format' => 'json',
2264
2516
  'skip_refresh_on_startup' => true,
2265
2517
  })
2518
+ shutdown_start_time = 0
2519
+
2266
2520
  d = create_driver(config)
2267
2521
  mock.proxy(d.instance).io_handler(anything, anything) do |io_handler|
2522
+ mock.proxy(io_handler).ready_to_shutdown(anything) do
2523
+ shutdown_start_time = Fluent::Clock.now
2524
+ end
2268
2525
  io_handler.shutdown_timeout = 0.5
2269
2526
  io_handler
2270
2527
  end
2271
2528
 
2272
- start_time = Fluent::Clock.now
2273
2529
  assert_nothing_raised do
2274
2530
  d.run(expect_emits: 1)
2275
2531
  end
2276
2532
 
2277
- elapsed = Fluent::Clock.now - start_time
2278
- assert_true(elapsed > 0.5 && elapsed < 2.5)
2533
+ elapsed = Fluent::Clock.now - shutdown_start_time
2534
+ assert_true(elapsed > 0.5 && elapsed < 2.0,
2535
+ "elapsed time: #{elapsed}")
2536
+ end
2537
+
2538
+ sub_test_case "throttling logs at in_tail level" do
2539
+ data("file test1.log no_limit 5120 text: msg" => ["test1.log", 5120, "msg"],
2540
+ "file test2.log no_limit 1024 text: test" => ["test2.log", 1024, "test"])
2541
+ def test_lines_collected_with_no_throttling(data)
2542
+ file, num_lines, msg = data
2543
+
2544
+ pattern = "/^#{@tmp_dir}\/(?<file>.+)\.log$/"
2545
+ rule = create_rule_directive({
2546
+ "file" => "/test.*/",
2547
+ }, -1)
2548
+ group = create_group_directive(pattern, "1s", rule)
2549
+ path_element = create_path_element(file)
2550
+
2551
+ conf = ROOT_CONFIG + group + path_element + CONFIG_READ_FROM_HEAD + SINGLE_LINE_CONFIG
2552
+
2553
+ Fluent::FileWrapper.open("#{@tmp_dir}/#{file}", 'wb') do |f|
2554
+ num_lines.times do
2555
+ f.puts "#{msg}\n"
2556
+ end
2557
+ end
2558
+
2559
+
2560
+ d = create_driver(conf, false)
2561
+ d.run(timeout: 3) do
2562
+ start_time = Fluent::Clock.now
2563
+
2564
+ assert_equal(num_lines, d.record_count)
2565
+ assert_equal({ "message" => msg }, d.events[0][2])
2566
+
2567
+ prev_count = d.record_count
2568
+ sleep(0.1) while d.emit_count < 1
2569
+ assert_true(Fluent::Clock.now - start_time < 2)
2570
+ ## after waiting for 1 (+ jitter) secs, limit will reset
2571
+ ## Plugin will start reading but it will encounter EOF Error
2572
+ ## since no logs are left to be read
2573
+ ## Hence, d.record_count = prev_count
2574
+ tail_watcher_interval = 1.0 # hard coded value in in_tail
2575
+ safety_ratio = 1.2
2576
+ jitter = tail_watcher_interval * safety_ratio
2577
+ sleep(1.0 + jitter)
2578
+ assert_equal(0, d.record_count - prev_count)
2579
+ end
2580
+ end
2581
+
2582
+ test "lines collected with throttling" do
2583
+ file = "podname1_namespace12_container-123456.log"
2584
+ limit = 1000
2585
+ rate_period = 2
2586
+ num_lines = 3000
2587
+ msg = "a" * 8190 # Total size = 8190 bytes + 2 (\n) bytes
2588
+
2589
+ rule = create_rule_directive({
2590
+ "namespace"=> "/namespace.+/",
2591
+ "podname"=> "/podname.+/",
2592
+ }, limit)
2593
+ path_element = create_path_element(file)
2594
+ conf = ROOT_CONFIG + create_group_directive(tailing_group_pattern, "#{rate_period}s", rule) + path_element + SINGLE_LINE_CONFIG + CONFIG_READ_FROM_HEAD
2595
+
2596
+ d = create_driver(conf, false)
2597
+ file_path = "#{@tmp_dir}/#{file}"
2598
+
2599
+ Fluent::FileWrapper.open(file_path, 'wb') do |f|
2600
+ num_lines.times do
2601
+ f.puts msg
2602
+ end
2603
+ end
2604
+
2605
+ d.run(timeout: 15) do
2606
+ sleep_interval = 0.1
2607
+ tail_watcher_interval = 1.0 # hard coded value in in_tail
2608
+ safety_ratio = 1.2
2609
+ lower_jitter = sleep_interval * safety_ratio
2610
+ upper_jitter = (tail_watcher_interval + sleep_interval) * safety_ratio
2611
+ lower_interval = rate_period - lower_jitter
2612
+ upper_interval = rate_period + upper_jitter
2613
+
2614
+ emit_count = 0
2615
+ prev_count = 0
2616
+
2617
+ while emit_count < 3 do
2618
+ start_time = Fluent::Clock.now
2619
+ sleep(sleep_interval) while d.emit_count <= emit_count
2620
+ elapsed_seconds = Fluent::Clock.now - start_time
2621
+ if emit_count > 0
2622
+ assert_true(elapsed_seconds > lower_interval && elapsed_seconds < upper_interval,
2623
+ "elapsed_seconds #{elapsed_seconds} is out of allowed range:\n" +
2624
+ " lower: #{lower_interval} [sec]\n" +
2625
+ " upper: #{upper_interval} [sec]")
2626
+ end
2627
+ assert_equal(limit, d.record_count - prev_count)
2628
+ emit_count = d.emit_count
2629
+ prev_count = d.record_count
2630
+ end
2631
+
2632
+ ## When all the lines are read and rate_period seconds are over
2633
+ ## limit will reset and since there are no more logs to be read,
2634
+ ## number_lines_read will be 0
2635
+ sleep upper_interval
2636
+ gw = d.instance.find_group_from_metadata(file_path)
2637
+ assert_equal(0, gw.current_paths[file_path].number_lines_read)
2638
+ end
2639
+ end
2640
+ end
2641
+
2642
+ sub_test_case "Update watchers for rotation with follow_inodes" do
2643
+ def test_updateTW_before_refreshTW_and_detach_before_refreshTW
2644
+ config = config_element(
2645
+ "ROOT",
2646
+ "",
2647
+ {
2648
+ "path" => "#{@tmp_dir}/tail.txt*",
2649
+ "pos_file" => "#{@tmp_dir}/tail.pos",
2650
+ "tag" => "t1",
2651
+ "format" => "none",
2652
+ "read_from_head" => "true",
2653
+ "follow_inodes" => "true",
2654
+ # In order to detach the old watcher quickly.
2655
+ "rotate_wait" => "1s",
2656
+ # In order to reproduce the same condition stably, ensure that `refresh_watchers` is not
2657
+ # called by a timer.
2658
+ "refresh_interval" => "1h",
2659
+ # stat_watcher often calls `TailWatcher::on_notify` faster than creating a new log file,
2660
+ # so disable it in order to reproduce the same condition stably.
2661
+ "enable_stat_watcher" => "false",
2662
+ }
2663
+ )
2664
+ d = create_driver(config, false)
2665
+
2666
+ tail_watchers = []
2667
+ stub.proxy(d.instance).setup_watcher do |tw|
2668
+ tail_watchers.append(tw)
2669
+ tw
2670
+ end
2671
+
2672
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f| f.puts "file1 log1"}
2673
+
2674
+ d.run(expect_records: 4, timeout: 10) do
2675
+ # Rotate (If the timing is bad, `TailWatcher::on_notify` might be called between mv and new-file-creation)
2676
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f| f.puts "file1 log2"}
2677
+ FileUtils.move("#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt" + "1")
2678
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f| f.puts "file2 log1"}
2679
+
2680
+ # `watch_timer` calls `TailWatcher::on_notify`, and then `update_watcher` updates the TailWatcher:
2681
+ # TailWatcher(path: "tail.txt", inode: inode_0) => TailWatcher(path: "tail.txt", inode: inode_1)
2682
+ # The old TailWathcer is detached here since `rotate_wait` is just `1s`.
2683
+ sleep 3
2684
+
2685
+ # This reproduces the following situation:
2686
+ # Rotation => update_watcher => refresh_watchers
2687
+ # This adds a new TailWatcher: TailWatcher(path: "tail.txt1", inode: inode_0)
2688
+ d.instance.refresh_watchers
2689
+
2690
+ # Append to the new current log file.
2691
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f| f.puts "file2 log2"}
2692
+ end
2693
+
2694
+ inode_0 = tail_watchers[0].ino
2695
+ inode_1 = tail_watchers[1].ino
2696
+ record_values = d.events.collect { |event| event[2]["message"] }.sort
2697
+ position_entries = []
2698
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.pos", "r") do |f|
2699
+ f.readlines(chomp: true).each do |line|
2700
+ values = line.split("\t")
2701
+ position_entries.append([values[0], values[1], values[2].to_i(16)])
2702
+ end
2703
+ end
2704
+
2705
+ assert_equal(
2706
+ {
2707
+ record_values: ["file1 log1", "file1 log2", "file2 log1", "file2 log2"],
2708
+ tail_watcher_paths: ["#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt1"],
2709
+ tail_watcher_inodes: [inode_0, inode_1, inode_0],
2710
+ tail_watcher_io_handler_opened_statuses: [false, false, false],
2711
+ position_entries: [
2712
+ ["#{@tmp_dir}/tail.txt", "0000000000000016", inode_0],
2713
+ ["#{@tmp_dir}/tail.txt", "0000000000000016", inode_1],
2714
+ ],
2715
+ },
2716
+ {
2717
+ record_values: record_values,
2718
+ tail_watcher_paths: tail_watchers.collect { |tw| tw.path },
2719
+ tail_watcher_inodes: tail_watchers.collect { |tw| tw.ino },
2720
+ tail_watcher_io_handler_opened_statuses: tail_watchers.collect { |tw| tw.instance_variable_get(:@io_handler)&.opened? || false },
2721
+ position_entries: position_entries
2722
+ },
2723
+ )
2724
+ end
2725
+
2726
+ def test_updateTW_before_refreshTW_and_detach_after_refreshTW
2727
+ config = config_element(
2728
+ "ROOT",
2729
+ "",
2730
+ {
2731
+ "path" => "#{@tmp_dir}/tail.txt*",
2732
+ "pos_file" => "#{@tmp_dir}/tail.pos",
2733
+ "tag" => "t1",
2734
+ "format" => "none",
2735
+ "read_from_head" => "true",
2736
+ "follow_inodes" => "true",
2737
+ # In order to detach the old watcher after refresh_watchers.
2738
+ "rotate_wait" => "4s",
2739
+ # In order to reproduce the same condition stably, ensure that `refresh_watchers` is not
2740
+ # called by a timer.
2741
+ "refresh_interval" => "1h",
2742
+ # stat_watcher often calls `TailWatcher::on_notify` faster than creating a new log file,
2743
+ # so disable it in order to reproduce the same condition stably.
2744
+ "enable_stat_watcher" => "false",
2745
+ }
2746
+ )
2747
+ d = create_driver(config, false)
2748
+
2749
+ tail_watchers = []
2750
+ stub.proxy(d.instance).setup_watcher do |tw|
2751
+ tail_watchers.append(tw)
2752
+ tw
2753
+ end
2754
+
2755
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f| f.puts "file1 log1"}
2756
+
2757
+ d.run(expect_records: 4, timeout: 10) do
2758
+ # Rotate (If the timing is bad, `TailWatcher::on_notify` might be called between mv and new-file-creation)
2759
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f| f.puts "file1 log2"}
2760
+ FileUtils.move("#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt" + "1")
2761
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f| f.puts "file2 log1"}
2762
+
2763
+ # `watch_timer` calls `TailWatcher::on_notify`, and then `update_watcher` updates the TailWatcher:
2764
+ # TailWatcher(path: "tail.txt", inode: inode_0) => TailWatcher(path: "tail.txt", inode: inode_1)
2765
+ sleep 2
2766
+
2767
+ # This reproduces the following situation:
2768
+ # Rotation => update_watcher => refresh_watchers
2769
+ # This adds a new TailWatcher: TailWatcher(path: "tail.txt1", inode: inode_0)
2770
+ d.instance.refresh_watchers
2771
+
2772
+ # The old TailWathcer is detached here since `rotate_wait` is `4s`.
2773
+ sleep 3
2774
+
2775
+ # Append to the new current log file.
2776
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f| f.puts "file2 log2"}
2777
+ end
2778
+
2779
+ inode_0 = tail_watchers[0].ino
2780
+ inode_1 = tail_watchers[1].ino
2781
+ record_values = d.events.collect { |event| event[2]["message"] }.sort
2782
+ position_entries = []
2783
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.pos", "r") do |f|
2784
+ f.readlines(chomp: true).each do |line|
2785
+ values = line.split("\t")
2786
+ position_entries.append([values[0], values[1], values[2].to_i(16)])
2787
+ end
2788
+ end
2789
+
2790
+ assert_equal(
2791
+ {
2792
+ record_values: ["file1 log1", "file1 log2", "file2 log1", "file2 log2"],
2793
+ tail_watcher_paths: ["#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt1"],
2794
+ tail_watcher_inodes: [inode_0, inode_1, inode_0],
2795
+ tail_watcher_io_handler_opened_statuses: [false, false, false],
2796
+ position_entries: [
2797
+ # The recorded path is old, but it is no problem. The path is not used when using follow_inodes.
2798
+ ["#{@tmp_dir}/tail.txt", "0000000000000016", inode_0],
2799
+ ["#{@tmp_dir}/tail.txt", "0000000000000016", inode_1],
2800
+ ],
2801
+ },
2802
+ {
2803
+ record_values: record_values,
2804
+ tail_watcher_paths: tail_watchers.collect { |tw| tw.path },
2805
+ tail_watcher_inodes: tail_watchers.collect { |tw| tw.ino },
2806
+ tail_watcher_io_handler_opened_statuses: tail_watchers.collect { |tw| tw.instance_variable_get(:@io_handler)&.opened? || false },
2807
+ position_entries: position_entries
2808
+ },
2809
+ )
2810
+ end
2811
+
2812
+ # The scenario where in_tail wrongly detaches TailWatcher.
2813
+ # This is reported in https://github.com/fluent/fluentd/issues/4190.
2814
+ def test_updateTW_after_refreshTW
2815
+ config = config_element(
2816
+ "ROOT",
2817
+ "",
2818
+ {
2819
+ "path" => "#{@tmp_dir}/tail.txt*",
2820
+ "pos_file" => "#{@tmp_dir}/tail.pos",
2821
+ "tag" => "t1",
2822
+ "format" => "none",
2823
+ "read_from_head" => "true",
2824
+ "follow_inodes" => "true",
2825
+ # In order to detach the old watcher quickly.
2826
+ "rotate_wait" => "1s",
2827
+ # In order to reproduce the same condition stably, ensure that `refresh_watchers` is not
2828
+ # called by a timer.
2829
+ "refresh_interval" => "1h",
2830
+ # stat_watcher often calls `TailWatcher::on_notify` faster than creating a new log file,
2831
+ # so disable it in order to reproduce the same condition stably.
2832
+ "enable_stat_watcher" => "false",
2833
+ }
2834
+ )
2835
+ d = create_driver(config, false)
2836
+
2837
+ tail_watchers = []
2838
+ stub.proxy(d.instance).setup_watcher do |tw|
2839
+ tail_watchers.append(tw)
2840
+ tw
2841
+ end
2842
+
2843
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f| f.puts "file1 log1"}
2844
+
2845
+ d.run(expect_records: 4, timeout: 10) do
2846
+ # Rotate (If the timing is bad, `TailWatcher::on_notify` might be called between mv and new-file-creation)
2847
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f| f.puts "file1 log2"}
2848
+ FileUtils.move("#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt" + "1")
2849
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f| f.puts "file2 log1"}
2850
+
2851
+ # This reproduces the following situation:
2852
+ # Rotation => refresh_watchers => update_watcher
2853
+ # This add a new TailWatcher: TailWatcher(path: "tail.txt", inode: inode_1)
2854
+ # This overwrites `@tails["tail.txt"]`
2855
+ d.instance.refresh_watchers
2856
+
2857
+ # `watch_timer` calls `TailWatcher::on_notify`, and then `update_watcher` trys to update the TailWatcher:
2858
+ # TailWatcher(path: "tail.txt", inode: inode_0) => TailWatcher(path: "tail.txt", inode: inode_1)
2859
+ # However, it is already added in `refresh_watcher`, so `update_watcher` doesn't create the new TailWatcher.
2860
+ # The old TailWathcer is detached here since `rotate_wait` is just `1s`.
2861
+ sleep 3
2862
+
2863
+ # This adds a new TailWatcher: TailWatcher(path: "tail.txt1", inode: inode_0)
2864
+ d.instance.refresh_watchers
2865
+
2866
+ # Append to the new current log file.
2867
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f| f.puts "file2 log2"}
2868
+ end
2869
+
2870
+ inode_0 = tail_watchers[0].ino
2871
+ inode_1 = tail_watchers[1].ino
2872
+ record_values = d.events.collect { |event| event[2]["message"] }.sort
2873
+ position_entries = []
2874
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.pos", "r") do |f|
2875
+ f.readlines(chomp: true).each do |line|
2876
+ values = line.split("\t")
2877
+ position_entries.append([values[0], values[1], values[2].to_i(16)])
2878
+ end
2879
+ end
2880
+
2881
+ assert_equal(
2882
+ {
2883
+ record_values: ["file1 log1", "file1 log2", "file2 log1", "file2 log2"],
2884
+ tail_watcher_paths: ["#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt1"],
2885
+ tail_watcher_inodes: [inode_0, inode_1, inode_0],
2886
+ tail_watcher_io_handler_opened_statuses: [false, false, false],
2887
+ position_entries: [
2888
+ # The recorded path is old, but it is no problem. The path is not used when using follow_inodes.
2889
+ ["#{@tmp_dir}/tail.txt", "0000000000000016", inode_0],
2890
+ ["#{@tmp_dir}/tail.txt", "0000000000000016", inode_1],
2891
+ ],
2892
+ },
2893
+ {
2894
+ record_values: record_values,
2895
+ tail_watcher_paths: tail_watchers.collect { |tw| tw.path },
2896
+ tail_watcher_inodes: tail_watchers.collect { |tw| tw.ino },
2897
+ tail_watcher_io_handler_opened_statuses: tail_watchers.collect { |tw| tw.instance_variable_get(:@io_handler)&.opened? || false },
2898
+ position_entries: position_entries
2899
+ },
2900
+ )
2901
+ end
2902
+
2903
+ def test_path_resurrection
2904
+ config = config_element(
2905
+ "ROOT",
2906
+ "",
2907
+ {
2908
+ "path" => "#{@tmp_dir}/tail.txt*",
2909
+ "pos_file" => "#{@tmp_dir}/tail.pos",
2910
+ "tag" => "t1",
2911
+ "format" => "none",
2912
+ "read_from_head" => "true",
2913
+ "follow_inodes" => "true",
2914
+ # In order to reproduce the same condition stably, ensure that `refresh_watchers` is not
2915
+ # called by a timer.
2916
+ "refresh_interval" => "1h",
2917
+ # https://github.com/fluent/fluentd/pull/4237#issuecomment-1633358632
2918
+ # Because of this problem, log duplication can occur during `rotate_wait`.
2919
+ # Need to set `rotate_wait 0` for a workaround.
2920
+ "rotate_wait" => "0s",
2921
+ }
2922
+ )
2923
+ d = create_driver(config, false)
2924
+
2925
+ tail_watchers = []
2926
+ stub.proxy(d.instance).setup_watcher do |tw|
2927
+ tail_watchers.append(tw)
2928
+ tw
2929
+ end
2930
+
2931
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f| f.puts "file1 log1"}
2932
+
2933
+ d.run(expect_records: 5, timeout: 10) do
2934
+ # Rotate
2935
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f| f.puts "file1 log2"}
2936
+ FileUtils.move("#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt" + "1")
2937
+ # TailWatcher(path: "tail.txt", inode: inode_0) detects `tail.txt` disappeared.
2938
+ # Call `update_watcher` to stop and discard self.
2939
+ # If not discarding, then it will be a orphan and cause leak and log duplication.
2940
+ #
2941
+ # This reproduces the case where the notify to TailWatcher comes before the new file for the path
2942
+ # is created during rotation.
2943
+ # (stat_watcher notifies faster than a new file is created)
2944
+ # Overall, this is a rotation operation, but from the TailWatcher, it appears as if the file
2945
+ # was resurrected once it disappeared.
2946
+ sleep 2 # On Windows and macOS, StatWatcher doesn't work, so need enough interval for TimeTrigger.
2947
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f| f.puts "file2 log1"}
2948
+
2949
+ # Add new TailWatchers
2950
+ # tail.txt: TailWatcher(path: "tail.txt", inode: inode_1)
2951
+ # tail.txt: TailWatcher(path: "tail.txt1", inode: inode_0)
2952
+ # NOTE: If not discarding the first TailWatcher on notify, this makes it a orphan because
2953
+ # this overwrites the `@tails[tail.txt]` by adding TailWatcher(path: "tail.txt", inode: inode_1)
2954
+ d.instance.refresh_watchers
2955
+
2956
+ # This does nothing.
2957
+ # NOTE: If not discarding the first TailWatcher on notify, this add
2958
+ # tail.txt1: TailWatcher(path: "tail.txt1", inode: inode_0)
2959
+ # because the previous refresh_watcher overwrites `@tails[tail.txt]` and the inode_0 is lost.
2960
+ # This would cause log duplication.
2961
+ d.instance.refresh_watchers
2962
+
2963
+ # Append to the old file
2964
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt1", "ab") {|f| f.puts "file1 log3"}
2965
+
2966
+ # Append to the new current log file.
2967
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f| f.puts "file2 log2"}
2968
+ end
2969
+
2970
+ inode_0 = Fluent::FileWrapper.stat("#{@tmp_dir}/tail.txt1").ino
2971
+ inode_1 = Fluent::FileWrapper.stat("#{@tmp_dir}/tail.txt").ino
2972
+ record_values = d.events.collect { |event| event[2]["message"] }.sort
2973
+ position_entries = []
2974
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.pos", "r") do |f|
2975
+ f.readlines(chomp: true).each do |line|
2976
+ values = line.split("\t")
2977
+ position_entries.append([values[0], values[1], values[2].to_i(16)])
2978
+ end
2979
+ end
2980
+
2981
+ assert_equal(
2982
+ {
2983
+ record_values: ["file1 log1", "file1 log2", "file1 log3", "file2 log1", "file2 log2"],
2984
+ tail_watcher_set: Set[
2985
+ {
2986
+ path: "#{@tmp_dir}/tail.txt",
2987
+ inode: inode_0,
2988
+ io_handler_opened_status: false,
2989
+ },
2990
+ {
2991
+ path: "#{@tmp_dir}/tail.txt",
2992
+ inode: inode_1,
2993
+ io_handler_opened_status: false,
2994
+ },
2995
+ {
2996
+ path: "#{@tmp_dir}/tail.txt1",
2997
+ inode: inode_0,
2998
+ io_handler_opened_status: false,
2999
+ },
3000
+ ],
3001
+ position_entries: [
3002
+ ["#{@tmp_dir}/tail.txt", "0000000000000021", inode_0],
3003
+ ["#{@tmp_dir}/tail.txt", "0000000000000016", inode_1],
3004
+ ],
3005
+ },
3006
+ {
3007
+ record_values: record_values,
3008
+ tail_watcher_set: Set.new(tail_watchers.collect { |tw|
3009
+ {
3010
+ path: tw.path,
3011
+ inode: tw.ino,
3012
+ io_handler_opened_status: tw.instance_variable_get(:@io_handler)&.opened? || false,
3013
+ }
3014
+ }),
3015
+ position_entries: position_entries,
3016
+ },
3017
+ )
3018
+ end
3019
+
3020
+ def test_next_rotation_occurs_very_fast_while_old_TW_still_waiting_rotate_wait
3021
+ config = config_element(
3022
+ "ROOT",
3023
+ "",
3024
+ {
3025
+ "path" => "#{@tmp_dir}/tail.txt*",
3026
+ "pos_file" => "#{@tmp_dir}/tail.pos",
3027
+ "tag" => "t1",
3028
+ "format" => "none",
3029
+ "read_from_head" => "true",
3030
+ "follow_inodes" => "true",
3031
+ "rotate_wait" => "3s",
3032
+ "refresh_interval" => "1h",
3033
+ # stat_watcher often calls `TailWatcher::on_notify` faster than creating a new log file,
3034
+ # so disable it in order to reproduce the same condition stably.
3035
+ "enable_stat_watcher" => "false",
3036
+ }
3037
+ )
3038
+ d = create_driver(config, false)
3039
+
3040
+ tail_watchers = []
3041
+ stub.proxy(d.instance).setup_watcher do |tw|
3042
+ tail_watchers.append(tw)
3043
+ mock.proxy(tw).close.once # Note: Currently, there is no harm in duplicate calls.
3044
+ tw
3045
+ end
3046
+
3047
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "wb") {|f| f.puts "file1 log1"}
3048
+
3049
+ d.run(expect_records: 6, timeout: 15) do
3050
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "ab") {|f| f.puts "file1 log2"}
3051
+
3052
+ sleep 1.5 # Need to be larger than 1s (the interval of watch_timer)
3053
+
3054
+ FileUtils.move("#{@tmp_dir}/tail.txt0", "#{@tmp_dir}/tail.txt" + "1")
3055
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "wb") {|f| f.puts "file2 log1"}
3056
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "ab") {|f| f.puts "file2 log2"}
3057
+
3058
+ sleep 1.5 # Need to be larger than 1s (the interval of watch_timer)
3059
+
3060
+ # Rotate again (Old TailWatcher waiting rotate_wait also calls update_watcher)
3061
+ [1, 0].each do |i|
3062
+ FileUtils.move("#{@tmp_dir}/tail.txt#{i}", "#{@tmp_dir}/tail.txt#{i + 1}")
3063
+ end
3064
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "wb") {|f| f.puts "file3 log1"}
3065
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "ab") {|f| f.puts "file3 log2"}
3066
+
3067
+ # Wait rotate_wait to confirm that TailWatcher.close is not called in duplicate.
3068
+ # (Note: Currently, there is no harm in duplicate calls)
3069
+ sleep 4
3070
+ end
3071
+
3072
+ inode_0 = tail_watchers[0]&.ino
3073
+ inode_1 = tail_watchers[1]&.ino
3074
+ inode_2 = tail_watchers[2]&.ino
3075
+ record_values = d.events.collect { |event| event[2]["message"] }.sort
3076
+ position_entries = []
3077
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.pos", "r") do |f|
3078
+ f.readlines(chomp: true).each do |line|
3079
+ values = line.split("\t")
3080
+ position_entries.append([values[0], values[1], values[2].to_i(16)])
3081
+ end
3082
+ end
3083
+
3084
+ assert_equal(
3085
+ {
3086
+ record_values: ["file1 log1", "file1 log2", "file2 log1", "file2 log2", "file3 log1", "file3 log2"],
3087
+ tail_watcher_paths: ["#{@tmp_dir}/tail.txt0", "#{@tmp_dir}/tail.txt0", "#{@tmp_dir}/tail.txt0"],
3088
+ tail_watcher_inodes: [inode_0, inode_1, inode_2],
3089
+ tail_watcher_io_handler_opened_statuses: [false, false, false],
3090
+ position_entries: [
3091
+ ["#{@tmp_dir}/tail.txt0", "0000000000000016", inode_0],
3092
+ ["#{@tmp_dir}/tail.txt0", "0000000000000016", inode_1],
3093
+ ["#{@tmp_dir}/tail.txt0", "0000000000000016", inode_2],
3094
+ ],
3095
+ },
3096
+ {
3097
+ record_values: record_values,
3098
+ tail_watcher_paths: tail_watchers.collect { |tw| tw.path },
3099
+ tail_watcher_inodes: tail_watchers.collect { |tw| tw.ino },
3100
+ tail_watcher_io_handler_opened_statuses: tail_watchers.collect { |tw| tw.instance_variable_get(:@io_handler)&.opened? || false },
3101
+ position_entries: position_entries
3102
+ },
3103
+ )
3104
+ end
3105
+ end
3106
+
3107
+ sub_test_case "Update watchers for rotation without follow_inodes" do
3108
+ # The scenario where in_tail wrongly unwatches the PositionEntry.
3109
+ # This is reported in https://github.com/fluent/fluentd/issues/3614.
3110
+ def test_refreshTW_during_rotation
3111
+ config = config_element(
3112
+ "ROOT",
3113
+ "",
3114
+ {
3115
+ "path" => "#{@tmp_dir}/tail.txt0",
3116
+ "pos_file" => "#{@tmp_dir}/tail.pos",
3117
+ "tag" => "t1",
3118
+ "format" => "none",
3119
+ "read_from_head" => "true",
3120
+ # In order to detach the old watcher quickly.
3121
+ "rotate_wait" => "3s",
3122
+ # In order to reproduce the same condition stably, ensure that `refresh_watchers` is not
3123
+ # called by a timer.
3124
+ "refresh_interval" => "1h",
3125
+ # stat_watcher often calls `TailWatcher::on_notify` faster than creating a new log file,
3126
+ # so disable it in order to reproduce the same condition stably.
3127
+ "enable_stat_watcher" => "false",
3128
+ }
3129
+ )
3130
+ d = create_driver(config, false)
3131
+
3132
+ tail_watchers = []
3133
+ stub.proxy(d.instance).setup_watcher do |tw|
3134
+ tail_watchers.append(tw)
3135
+ tw
3136
+ end
3137
+
3138
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "wb") {|f| f.puts "file1 log1"}
3139
+
3140
+ d.run(expect_records: 6, timeout: 15) do
3141
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "ab") {|f| f.puts "file1 log2"}
3142
+ FileUtils.move("#{@tmp_dir}/tail.txt0", "#{@tmp_dir}/tail.txt" + "1")
3143
+
3144
+ # This reproduces the following situation:
3145
+ # `refresh_watchers` is called during the rotation process and it detects the current file being lost.
3146
+ # Then it stops and unwatches the TailWatcher.
3147
+ d.instance.refresh_watchers
3148
+
3149
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "wb") {|f| f.puts "file2 log1"}
3150
+
3151
+ # `watch_timer` calls `TailWatcher::on_notify`, and then `update_watcher` trys to add the new TailWatcher.
3152
+ # After `rotate_wait` interval, the PositionEntry is unwatched.
3153
+ # HOWEVER, the new TailWatcher is still using that PositionEntry, so this breaks the PositionFile!!
3154
+ # That PositionEntry is removed from `PositionFile::map`, but it is still working and remaining in the real pos file.
3155
+ sleep 5
3156
+
3157
+ # Append to the new current log file.
3158
+ # The PositionEntry is updated although it does not exist in `PositionFile::map`.
3159
+ # `PositionFile::map`: empty
3160
+ # Real pos file: `.../tail.txt 0000000000000016 (inode)`
3161
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "ab") {|f| f.puts "file2 log2"}
3162
+
3163
+ # Rotate again
3164
+ [1, 0].each do |i|
3165
+ FileUtils.move("#{@tmp_dir}/tail.txt#{i}", "#{@tmp_dir}/tail.txt#{i + 1}")
3166
+ end
3167
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "wb") {|f| f.puts "file3 log1"}
3168
+
3169
+ # `watch_timer` calls `TailWatcher::on_notify`, and then `update_watcher` trys to update the TailWatcher.
3170
+ sleep 3
3171
+
3172
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "ab") {|f| f.puts "file3 log2"}
3173
+ end
3174
+
3175
+ inode_0 = tail_watchers[0]&.ino
3176
+ inode_1 = tail_watchers[1]&.ino
3177
+ inode_2 = tail_watchers[2]&.ino
3178
+ record_values = d.events.collect { |event| event[2]["message"] }.sort
3179
+ position_entries = []
3180
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.pos", "r") do |f|
3181
+ f.readlines(chomp: true).each do |line|
3182
+ values = line.split("\t")
3183
+ position_entries.append([values[0], values[1], values[2].to_i(16)])
3184
+ end
3185
+ end
3186
+
3187
+ assert_equal(
3188
+ {
3189
+ record_values: ["file1 log1", "file1 log2", "file2 log1", "file2 log2", "file3 log1", "file3 log2"],
3190
+ tail_watcher_paths: ["#{@tmp_dir}/tail.txt0", "#{@tmp_dir}/tail.txt0", "#{@tmp_dir}/tail.txt0"],
3191
+ tail_watcher_inodes: [inode_0, inode_1, inode_2],
3192
+ tail_watcher_io_handler_opened_statuses: [false, false, false],
3193
+ position_entries: [
3194
+ # The recorded path is old, but it is no problem. The path is not used when using follow_inodes.
3195
+ ["#{@tmp_dir}/tail.txt0", "0000000000000016", inode_2],
3196
+ ],
3197
+ },
3198
+ {
3199
+ record_values: record_values,
3200
+ tail_watcher_paths: tail_watchers.collect { |tw| tw.path },
3201
+ tail_watcher_inodes: tail_watchers.collect { |tw| tw.ino },
3202
+ tail_watcher_io_handler_opened_statuses: tail_watchers.collect { |tw| tw.instance_variable_get(:@io_handler)&.opened? || false },
3203
+ position_entries: position_entries
3204
+ },
3205
+ )
3206
+ end
3207
+
3208
+ def test_next_rotation_occurs_very_fast_while_old_TW_still_waiting_rotate_wait
3209
+ config = config_element(
3210
+ "ROOT",
3211
+ "",
3212
+ {
3213
+ "path" => "#{@tmp_dir}/tail.txt0",
3214
+ "pos_file" => "#{@tmp_dir}/tail.pos",
3215
+ "tag" => "t1",
3216
+ "format" => "none",
3217
+ "read_from_head" => "true",
3218
+ "rotate_wait" => "3s",
3219
+ "refresh_interval" => "1h",
3220
+ }
3221
+ )
3222
+ d = create_driver(config, false)
3223
+
3224
+ tail_watchers = []
3225
+ stub.proxy(d.instance).setup_watcher do |tw|
3226
+ tail_watchers.append(tw)
3227
+ mock.proxy(tw).close.once # Note: Currently, there is no harm in duplicate calls.
3228
+ tw
3229
+ end
3230
+
3231
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "wb") {|f| f.puts "file1 log1"}
3232
+
3233
+ d.run(expect_records: 6, timeout: 15) do
3234
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "ab") {|f| f.puts "file1 log2"}
3235
+
3236
+ sleep 1.5 # Need to be larger than 1s (the interval of watch_timer)
3237
+
3238
+ FileUtils.move("#{@tmp_dir}/tail.txt0", "#{@tmp_dir}/tail.txt" + "1")
3239
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "wb") {|f| f.puts "file2 log1"}
3240
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "ab") {|f| f.puts "file2 log2"}
3241
+
3242
+ sleep 1.5 # Need to be larger than 1s (the interval of watch_timer)
3243
+
3244
+ # Rotate again (Old TailWatcher waiting rotate_wait also calls update_watcher)
3245
+ [1, 0].each do |i|
3246
+ FileUtils.move("#{@tmp_dir}/tail.txt#{i}", "#{@tmp_dir}/tail.txt#{i + 1}")
3247
+ end
3248
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "wb") {|f| f.puts "file3 log1"}
3249
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "ab") {|f| f.puts "file3 log2"}
3250
+
3251
+ # Wait rotate_wait to confirm that TailWatcher.close is not called in duplicate.
3252
+ # (Note: Currently, there is no harm in duplicate calls)
3253
+ sleep 4
3254
+ end
3255
+
3256
+ inode_0 = tail_watchers[0]&.ino
3257
+ inode_1 = tail_watchers[1]&.ino
3258
+ inode_2 = tail_watchers[2]&.ino
3259
+ record_values = d.events.collect { |event| event[2]["message"] }.sort
3260
+ position_entries = []
3261
+ Fluent::FileWrapper.open("#{@tmp_dir}/tail.pos", "r") do |f|
3262
+ f.readlines(chomp: true).each do |line|
3263
+ values = line.split("\t")
3264
+ position_entries.append([values[0], values[1], values[2].to_i(16)])
3265
+ end
3266
+ end
3267
+
3268
+ assert_equal(
3269
+ {
3270
+ record_values: ["file1 log1", "file1 log2", "file2 log1", "file2 log2", "file3 log1", "file3 log2"],
3271
+ tail_watcher_paths: ["#{@tmp_dir}/tail.txt0", "#{@tmp_dir}/tail.txt0", "#{@tmp_dir}/tail.txt0"],
3272
+ tail_watcher_inodes: [inode_0, inode_1, inode_2],
3273
+ tail_watcher_io_handler_opened_statuses: [false, false, false],
3274
+ position_entries: [
3275
+ ["#{@tmp_dir}/tail.txt0", "0000000000000016", inode_2],
3276
+ ],
3277
+ },
3278
+ {
3279
+ record_values: record_values,
3280
+ tail_watcher_paths: tail_watchers.collect { |tw| tw.path },
3281
+ tail_watcher_inodes: tail_watchers.collect { |tw| tw.ino },
3282
+ tail_watcher_io_handler_opened_statuses: tail_watchers.collect { |tw| tw.instance_variable_get(:@io_handler)&.opened? || false },
3283
+ position_entries: position_entries
3284
+ },
3285
+ )
3286
+ end
2279
3287
  end
2280
3288
  end