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
@@ -24,20 +24,19 @@ require 'fluent/plugin/parser_multiline'
24
24
  require 'fluent/variable_store'
25
25
  require 'fluent/capability'
26
26
  require 'fluent/plugin/in_tail/position_file'
27
-
28
- if Fluent.windows?
29
- require_relative 'file_wrapper'
30
- else
31
- Fluent::FileWrapper = File
32
- end
27
+ require 'fluent/plugin/in_tail/group_watch'
28
+ require 'fluent/file_wrapper'
33
29
 
34
30
  module Fluent::Plugin
35
31
  class TailInput < Fluent::Plugin::Input
32
+ include GroupWatch
33
+
36
34
  Fluent::Plugin.register_input('tail', self)
37
35
 
38
36
  helpers :timer, :event_loop, :parser, :compat_parameters
39
37
 
40
38
  RESERVED_CHARS = ['/', '*', '%'].freeze
39
+ MetricsInfo = Struct.new(:opened, :closed, :rotated)
41
40
 
42
41
  class WatcherSetupError < StandardError
43
42
  def initialize(msg)
@@ -53,10 +52,13 @@ module Fluent::Plugin
53
52
  super
54
53
  @paths = []
55
54
  @tails = {}
55
+ @tails_rotate_wait = {}
56
56
  @pf_file = nil
57
57
  @pf = nil
58
58
  @ignore_list = []
59
59
  @shutdown_start_time = nil
60
+ @metrics = nil
61
+ @startup = true
60
62
  end
61
63
 
62
64
  desc 'The paths to read. Multiple paths can be specified, separated by comma.'
@@ -110,6 +112,8 @@ module Fluent::Plugin
110
112
  config_param :path_timezone, :string, default: nil
111
113
  desc 'Follow inodes instead of following file names. Guarantees more stable delivery and allows to use * in path pattern with rotating files'
112
114
  config_param :follow_inodes, :bool, default: false
115
+ desc 'Maximum length of line. The longer line is just skipped.'
116
+ config_param :max_line_size, :size, default: nil
113
117
 
114
118
  config_section :parse, required: false, multi: true, init: true, param_name: :parser_configs do
115
119
  config_argument :usage, :string, default: 'in_tail_parser'
@@ -191,6 +195,10 @@ module Fluent::Plugin
191
195
  @read_bytes_limit_per_second = min_bytes
192
196
  end
193
197
  end
198
+ opened_file_metrics = metrics_create(namespace: "fluentd", subsystem: "input", name: "files_opened_total", help_text: "Total number of opened files")
199
+ closed_file_metrics = metrics_create(namespace: "fluentd", subsystem: "input", name: "files_closed_total", help_text: "Total number of closed files")
200
+ rotated_file_metrics = metrics_create(namespace: "fluentd", subsystem: "input", name: "files_rotated_total", help_text: "Total number of rotated files")
201
+ @metrics = MetricsInfo.new(opened_file_metrics, closed_file_metrics, rotated_file_metrics)
194
202
  end
195
203
 
196
204
  def configure_tag
@@ -260,6 +268,9 @@ module Fluent::Plugin
260
268
  @shutdown_start_time = Fluent::Clock.now
261
269
  # during shutdown phase, don't close io. It should be done in close after all threads are stopped. See close.
262
270
  stop_watchers(existence_path, immediate: true, remove_watcher: false)
271
+ @tails_rotate_wait.keys.each do |tw|
272
+ detach_watcher(tw, @tails_rotate_wait[tw][:ino], false)
273
+ end
263
274
  @pf_file.close if @pf_file
264
275
 
265
276
  super
@@ -268,6 +279,7 @@ module Fluent::Plugin
268
279
  def close
269
280
  super
270
281
  # close file handles after all threads stopped (in #close of thread plugin helper)
282
+ # It may be because we need to wait IOHanlder.ready_to_shutdown()
271
283
  close_watcher_handles
272
284
  end
273
285
 
@@ -345,11 +357,11 @@ module Fluent::Plugin
345
357
 
346
358
  def existence_path
347
359
  hash = {}
348
- @tails.each_key {|target_info|
360
+ @tails.each {|path, tw|
349
361
  if @follow_inodes
350
- hash[target_info.ino] = target_info
362
+ hash[tw.ino] = TargetInfo.new(tw.path, tw.ino)
351
363
  else
352
- hash[target_info.path] = target_info
364
+ hash[tw.path] = TargetInfo.new(tw.path, tw.ino)
353
365
  end
354
366
  }
355
367
  hash
@@ -364,18 +376,59 @@ module Fluent::Plugin
364
376
  target_paths_hash = expand_paths
365
377
  existence_paths_hash = existence_path
366
378
 
367
- log.debug { "tailing paths: target = #{target_paths.join(",")} | existing = #{existence_paths.join(",")}" }
379
+ log.debug {
380
+ target_paths_str = target_paths_hash.collect { |key, target_info| target_info.path }.join(",")
381
+ existence_paths_str = existence_paths_hash.collect { |key, target_info| target_info.path }.join(",")
382
+ "tailing paths: target = #{target_paths_str} | existing = #{existence_paths_str}"
383
+ }
384
+
385
+ if !@follow_inodes
386
+ need_unwatch_in_stop_watchers = true
387
+ else
388
+ # When using @follow_inodes, need this to unwatch the rotated old inode when it disappears.
389
+ # After `update_watcher` detaches an old TailWatcher, the inode is lost from the `@tails`.
390
+ # So that inode can't be contained in `removed_hash`, and can't be unwatched by `stop_watchers`.
391
+ #
392
+ # This logic may work for `@follow_inodes false` too.
393
+ # Just limiting the case to suppress the impact to existing logics.
394
+ @pf&.unwatch_removed_targets(target_paths_hash)
395
+ need_unwatch_in_stop_watchers = false
396
+ end
368
397
 
369
- unwatched_hash = existence_paths_hash.reject {|key, value| target_paths_hash.key?(key)}
398
+ removed_hash = existence_paths_hash.reject {|key, value| target_paths_hash.key?(key)}
370
399
  added_hash = target_paths_hash.reject {|key, value| existence_paths_hash.key?(key)}
371
400
 
372
- stop_watchers(unwatched_hash, immediate: false, unwatched: true) unless unwatched_hash.empty?
401
+ # If an exisiting TailWatcher already follows a target path with the different inode,
402
+ # it means that the TailWatcher following the rotated file still exists. In this case,
403
+ # `refresh_watcher` can't start the new TailWatcher for the new current file. So, we
404
+ # should output a warning log in order to prevent silent collection stops.
405
+ # (Such as https://github.com/fluent/fluentd/pull/4327)
406
+ # (Usually, such a TailWatcher should be removed from `@tails` in `update_watcher`.)
407
+ # (The similar warning may work for `@follow_inodes true` too. Just limiting the case
408
+ # to suppress the impact to existing logics.)
409
+ unless @follow_inodes
410
+ target_paths_hash.each do |path, target|
411
+ next unless @tails.key?(path)
412
+ # We can't use `existence_paths_hash[path].ino` because it is from `TailWatcher.ino`,
413
+ # which is very unstable parameter. (It can be `nil` or old).
414
+ # So, we need to use `TailWatcher.pe.read_inode`.
415
+ existing_watcher_inode = @tails[path].pe.read_inode
416
+ if existing_watcher_inode != target.ino
417
+ log.warn "Could not follow a file (inode: #{target.ino}) because an existing watcher for that filepath follows a different inode: #{existing_watcher_inode} (e.g. keeps watching a already rotated file). If you keep getting this message, please restart Fluentd.",
418
+ filepath: target.path
419
+ end
420
+ end
421
+ end
422
+
423
+ stop_watchers(removed_hash, unwatched: need_unwatch_in_stop_watchers) unless removed_hash.empty?
373
424
  start_watchers(added_hash) unless added_hash.empty?
425
+ @startup = false if @startup
374
426
  end
375
427
 
376
428
  def setup_watcher(target_info, pe)
377
429
  line_buffer_timer_flusher = @multiline_mode ? TailWatcher::LineBufferTimerFlusher.new(log, @multiline_flush_interval, &method(:flush_buffer)) : nil
378
- tw = TailWatcher.new(target_info, pe, log, @read_from_head, @follow_inodes, method(:update_watcher), line_buffer_timer_flusher, method(:io_handler))
430
+ read_from_head = !@startup || @read_from_head
431
+ tw = TailWatcher.new(target_info, pe, log, read_from_head, @follow_inodes, method(:update_watcher), line_buffer_timer_flusher, method(:io_handler), @metrics)
379
432
 
380
433
  if @enable_watch_timer
381
434
  tt = TimerTrigger.new(1, log) { tw.on_notify }
@@ -383,7 +436,7 @@ module Fluent::Plugin
383
436
  end
384
437
 
385
438
  if @enable_stat_watcher
386
- tt = StatWatcher.new(path, log) { tw.on_notify }
439
+ tt = StatWatcher.new(target_info.path, log) { tw.on_notify }
387
440
  tw.register_watcher(tt)
388
441
  end
389
442
 
@@ -391,6 +444,8 @@ module Fluent::Plugin
391
444
  event_loop_attach(watcher)
392
445
  end
393
446
 
447
+ tw.group_watcher = add_path_to_group_watcher(target_info.path)
448
+
394
449
  tw
395
450
  rescue => e
396
451
  if tw
@@ -405,36 +460,31 @@ module Fluent::Plugin
405
460
  end
406
461
 
407
462
  def construct_watcher(target_info)
463
+ path = target_info.path
464
+
465
+ # The file might be rotated or removed after collecting paths, so check inode again here.
466
+ begin
467
+ target_info.ino = Fluent::FileWrapper.stat(path).ino
468
+ rescue Errno::ENOENT, Errno::EACCES
469
+ $log.warn "stat() for #{path} failed. Continuing without tailing it."
470
+ return
471
+ end
472
+
408
473
  pe = nil
409
474
  if @pf
410
475
  pe = @pf[target_info]
411
- if @read_from_head && pe.read_inode.zero?
412
- begin
413
- pe.update(Fluent::FileWrapper.stat(target_info.path).ino, 0)
414
- rescue Errno::ENOENT, Errno::EACCES
415
- $log.warn "stat() for #{target_info.path} failed. Continuing without tailing it."
416
- end
417
- end
476
+ pe.update(target_info.ino, 0) if @read_from_head && pe.read_inode.zero?
418
477
  end
419
478
 
420
479
  begin
421
480
  tw = setup_watcher(target_info, pe)
422
481
  rescue WatcherSetupError => e
423
- log.warn "Skip #{target_info.path} because unexpected setup error happens: #{e}"
482
+ log.warn "Skip #{path} because unexpected setup error happens: #{e}"
424
483
  return
425
484
  end
426
485
 
427
- begin
428
- target_info = TargetInfo.new(target_info.path, Fluent::FileWrapper.stat(target_info.path).ino)
429
- @tails.delete(target_info)
430
- @tails[target_info] = tw
431
- tw.on_notify
432
- rescue Errno::ENOENT, Errno::EACCES => e
433
- $log.warn "stat() for #{target_info.path} failed with #{e.class.name}. Drop tail watcher for now."
434
- # explicitly detach and unwatch watcher `tw`.
435
- tw.unwatched = true
436
- detach_watcher(tw, target_info.ino, false)
437
- end
486
+ @tails[path] = tw
487
+ tw.on_notify
438
488
  end
439
489
 
440
490
  def start_watchers(targets_info)
@@ -446,10 +496,12 @@ module Fluent::Plugin
446
496
 
447
497
  def stop_watchers(targets_info, immediate: false, unwatched: false, remove_watcher: true)
448
498
  targets_info.each_value { |target_info|
499
+ remove_path_from_group_watcher(target_info.path)
500
+
449
501
  if remove_watcher
450
- tw = @tails.delete(target_info)
502
+ tw = @tails.delete(target_info.path)
451
503
  else
452
- tw = @tails[target_info]
504
+ tw = @tails[target_info.path]
453
505
  end
454
506
  if tw
455
507
  tw.unwatched = unwatched
@@ -463,57 +515,74 @@ module Fluent::Plugin
463
515
  end
464
516
 
465
517
  def close_watcher_handles
466
- @tails.keys.each do |target_info|
467
- tw = @tails.delete(target_info)
518
+ @tails.keys.each do |path|
519
+ tw = @tails.delete(path)
468
520
  if tw
469
521
  tw.close
470
522
  end
471
523
  end
524
+ @tails_rotate_wait.keys.each do |tw|
525
+ tw.close
526
+ end
472
527
  end
473
528
 
474
529
  # refresh_watchers calls @tails.keys so we don't use stop_watcher -> start_watcher sequence for safety.
475
- def update_watcher(target_info, pe)
476
- log.info("detected rotation of #{target_info.path}; waiting #{@rotate_wait} seconds")
530
+ def update_watcher(tail_watcher, pe, new_inode)
531
+ # TODO we should use another callback for this.
532
+ # To supress impact to existing logics, limit the case to `@follow_inodes`.
533
+ # We may not need `@follow_inodes` condition.
534
+ if @follow_inodes && new_inode.nil?
535
+ # nil inode means the file disappeared, so we only need to stop it.
536
+ @tails.delete(tail_watcher.path)
537
+ # https://github.com/fluent/fluentd/pull/4237#issuecomment-1633358632
538
+ # Because of this problem, log duplication can occur during `rotate_wait`.
539
+ # Need to set `rotate_wait 0` for a workaround.
540
+ # Duplication will occur if `refresh_watcher` is called during the `rotate_wait`.
541
+ # In that case, `refresh_watcher` will add the new TailWatcher to tail the same target,
542
+ # and it causes the log duplication.
543
+ # (Other `detach_watcher_after_rotate_wait` may have the same problem.
544
+ # We need the mechanism not to add duplicated TailWathcer with detaching TailWatcher.)
545
+ detach_watcher_after_rotate_wait(tail_watcher, pe.read_inode)
546
+ return
547
+ end
548
+
549
+ path = tail_watcher.path
550
+
551
+ log.info("detected rotation of #{path}; waiting #{@rotate_wait} seconds")
477
552
 
478
553
  if @pf
479
554
  pe_inode = pe.read_inode
480
- target_info_from_position_entry = TargetInfo.new(target_info.path, pe_inode)
555
+ target_info_from_position_entry = TargetInfo.new(path, pe_inode)
481
556
  unless pe_inode == @pf[target_info_from_position_entry].read_inode
482
- log.debug "Skip update_watcher because watcher has been already updated by other inotify event"
557
+ log.warn "Skip update_watcher because watcher has been already updated by other inotify event",
558
+ path: path, inode: pe.read_inode, inode_in_pos_file: @pf[target_info_from_position_entry].read_inode
483
559
  return
484
560
  end
485
561
  end
486
562
 
487
- rotated_target_info = TargetInfo.new(target_info.path, pe.read_inode)
488
- rotated_tw = @tails[rotated_target_info]
489
- new_target_info = target_info.dup
563
+ new_target_info = TargetInfo.new(path, new_inode)
490
564
 
491
565
  if @follow_inodes
492
- new_position_entry = @pf[target_info]
493
-
566
+ new_position_entry = @pf[new_target_info]
567
+ # If `refresh_watcher` find the new file before, this will not be zero.
568
+ # In this case, only we have to do is detaching the current tail_watcher.
494
569
  if new_position_entry.read_inode == 0
495
- # When follow_inodes is true, it's not cleaned up by refresh_watcher.
496
- # So it should be unwatched here explicitly.
497
- rotated_tw.unwatched = true
498
- # Make sure to delete old key, it has a different ino while the hash key is same.
499
- @tails.delete(rotated_target_info)
500
- @tails[new_target_info] = setup_watcher(new_target_info, new_position_entry)
501
- @tails[new_target_info].on_notify
570
+ @tails[path] = setup_watcher(new_target_info, new_position_entry)
571
+ @tails[path].on_notify
502
572
  end
503
573
  else
504
- # Make sure to delete old key, it has a different ino while the hash key is same.
505
- @tails.delete(rotated_target_info)
506
- @tails[new_target_info] = setup_watcher(new_target_info, pe)
507
- @tails[new_target_info].on_notify
574
+ @tails[path] = setup_watcher(new_target_info, pe)
575
+ @tails[path].on_notify
508
576
  end
509
- detach_watcher_after_rotate_wait(rotated_tw, pe.read_inode) if rotated_tw
577
+
578
+ detach_watcher_after_rotate_wait(tail_watcher, pe.read_inode)
510
579
  end
511
580
 
512
- # TailWatcher#close is called by another thread at shutdown phase.
513
- # It causes 'can't modify string; temporarily locked' error in IOHandler
514
- # so adding close_io argument to avoid this problem.
515
- # At shutdown, IOHandler's io will be released automatically after detached the event loop
516
581
  def detach_watcher(tw, ino, close_io = true)
582
+ if @follow_inodes && tw.ino != ino
583
+ log.warn("detach_watcher could be detaching an unexpected tail_watcher with a different ino.",
584
+ path: tw.path, actual_ino_in_tw: tw.ino, expect_ino_to_close: ino)
585
+ end
517
586
  tw.watchers.each do |watcher|
518
587
  event_loop_detach(watcher)
519
588
  end
@@ -521,24 +590,29 @@ module Fluent::Plugin
521
590
 
522
591
  tw.close if close_io
523
592
 
524
- if tw.unwatched && @pf
593
+ if @pf && tw.unwatched && (@follow_inode || !@tails[tw.path])
525
594
  target_info = TargetInfo.new(tw.path, ino)
526
595
  @pf.unwatch(target_info)
527
596
  end
528
597
  end
529
598
 
599
+ def throttling_is_enabled?(tw)
600
+ return true if @read_bytes_limit_per_second > 0
601
+ return true if tw.group_watcher && tw.group_watcher.limit >= 0
602
+ false
603
+ end
604
+
530
605
  def detach_watcher_after_rotate_wait(tw, ino)
531
606
  # Call event_loop_attach/event_loop_detach is high-cost for short-live object.
532
607
  # If this has a problem with large number of files, use @_event_loop directly instead of timer_execute.
533
608
  if @open_on_every_update
534
609
  # Detach now because it's already closed, waiting it doesn't make sense.
535
610
  detach_watcher(tw, ino)
536
- elsif @read_bytes_limit_per_second < 0
537
- # throttling isn't enabled, just wait @rotate_wait
538
- timer_execute(:in_tail_close_watcher, @rotate_wait, repeat: false) do
539
- detach_watcher(tw, ino)
540
- end
541
- else
611
+ end
612
+
613
+ return if @tails_rotate_wait[tw]
614
+
615
+ if throttling_is_enabled?(tw)
542
616
  # When the throttling feature is enabled, it might not reach EOF yet.
543
617
  # Should ensure to read all contents before closing it, with keeping throttling.
544
618
  start_time_to_wait = Fluent::Clock.now
@@ -546,9 +620,18 @@ module Fluent::Plugin
546
620
  elapsed = Fluent::Clock.now - start_time_to_wait
547
621
  if tw.eof? && elapsed >= @rotate_wait
548
622
  timer.detach
623
+ @tails_rotate_wait.delete(tw)
549
624
  detach_watcher(tw, ino)
550
625
  end
551
626
  end
627
+ @tails_rotate_wait[tw] = { ino: ino, timer: timer }
628
+ else
629
+ # when the throttling feature isn't enabled, just wait @rotate_wait
630
+ timer = timer_execute(:in_tail_close_watcher, @rotate_wait, repeat: false) do
631
+ @tails_rotate_wait.delete(tw)
632
+ detach_watcher(tw, ino)
633
+ end
634
+ @tails_rotate_wait[tw] = { ino: ino, timer: timer }
552
635
  end
553
636
  end
554
637
 
@@ -581,6 +664,14 @@ module Fluent::Plugin
581
664
 
582
665
  # @return true if no error or unrecoverable error happens in emit action. false if got BufferOverflowError
583
666
  def receive_lines(lines, tail_watcher)
667
+ lines = lines.reject do |line|
668
+ skip_line = @max_line_size ? line.bytesize > @max_line_size : false
669
+ if skip_line
670
+ log.warn "received line length is longer than #{@max_line_size}"
671
+ log.debug "skipped line: #{line.chomp}"
672
+ end
673
+ skip_line
674
+ end
584
675
  es = @receive_handler.call(lines, tail_watcher)
585
676
  unless es.empty?
586
677
  tag = if @tag_prefix || @tag_suffix
@@ -670,6 +761,19 @@ module Fluent::Plugin
670
761
  es
671
762
  end
672
763
 
764
+ def statistics
765
+ stats = super
766
+
767
+ stats = {
768
+ 'input' => stats["input"].merge({
769
+ 'opened_file_count' => @metrics.opened.get,
770
+ 'closed_file_count' => @metrics.closed.get,
771
+ 'rotated_file_count' => @metrics.rotated.get,
772
+ })
773
+ }
774
+ stats
775
+ end
776
+
673
777
  private
674
778
 
675
779
  def io_handler(watcher, path)
@@ -682,6 +786,7 @@ module Fluent::Plugin
682
786
  open_on_every_update: @open_on_every_update,
683
787
  from_encoding: @from_encoding,
684
788
  encoding: @encoding,
789
+ metrics: @metrics,
685
790
  &method(:receive_lines)
686
791
  )
687
792
  end
@@ -717,7 +822,7 @@ module Fluent::Plugin
717
822
  end
718
823
 
719
824
  class TailWatcher
720
- def initialize(target_info, pe, log, read_from_head, follow_inodes, update_watcher, line_buffer_timer_flusher, io_handler_build)
825
+ def initialize(target_info, pe, log, read_from_head, follow_inodes, update_watcher, line_buffer_timer_flusher, io_handler_build, metrics)
721
826
  @path = target_info.path
722
827
  @ino = target_info.ino
723
828
  @pe = pe || MemoryPositionEntry.new
@@ -729,6 +834,7 @@ module Fluent::Plugin
729
834
  @line_buffer_timer_flusher = line_buffer_timer_flusher
730
835
  @io_handler = nil
731
836
  @io_handler_build = io_handler_build
837
+ @metrics = metrics
732
838
  @watchers = []
733
839
  end
734
840
 
@@ -737,9 +843,10 @@ module Fluent::Plugin
737
843
  attr_reader :line_buffer_timer_flusher
738
844
  attr_accessor :unwatched # This is used for removing position entry from PositionFile
739
845
  attr_reader :watchers
846
+ attr_accessor :group_watcher
740
847
 
741
848
  def tag
742
- @parsed_tag ||= @path.tr('/', '.').gsub(/\.+/, '.').gsub(/^\./, '')
849
+ @parsed_tag ||= @path.tr('/', '.').squeeze('.').gsub(/^\./, '')
743
850
  end
744
851
 
745
852
  def register_watcher(watcher)
@@ -762,7 +869,7 @@ module Fluent::Plugin
762
869
  end
763
870
 
764
871
  def eof?
765
- @io_handler.eof?
872
+ @io_handler.nil? || @io_handler.eof?
766
873
  end
767
874
 
768
875
  def on_notify
@@ -835,26 +942,27 @@ module Fluent::Plugin
835
942
 
836
943
  if watcher_needs_update
837
944
  if @follow_inodes
838
- # No need to update a watcher if stat is nil (file not present), because moving to inodes will create
839
- # new watcher, and old watcher will be closed by stop_watcher in refresh_watchers method
840
- # don't want to swap state because we need latest read offset in pos file even after rotate_wait
841
- if stat
842
- target_info = TargetInfo.new(@path, stat.ino)
843
- @update_watcher.call(target_info, @pe)
844
- end
945
+ # If stat is nil (file not present), NEED to stop and discard this watcher.
946
+ # When the file is disappeared but is resurrected soon, then `#refresh_watcher`
947
+ # can't recognize this TailWatcher needs to be stopped.
948
+ # This can happens when the file is rotated.
949
+ # If a notify comes before the new file for the path is created during rotation,
950
+ # then it appears as if the file was resurrected once it disappeared.
951
+ # Don't want to swap state because we need latest read offset in pos file even after rotate_wait
952
+ @update_watcher.call(self, @pe, stat&.ino)
845
953
  else
846
954
  # Permit to handle if stat is nil (file not present).
847
955
  # If a file is mv-ed and a new file is created during
848
956
  # calling `#refresh_watchers`s, and `#refresh_watchers` won't run `#start_watchers`
849
957
  # and `#stop_watchers()` for the path because `target_paths_hash`
850
958
  # always contains the path.
851
- target_info = TargetInfo.new(@path, stat ? stat.ino : nil)
852
- @update_watcher.call(target_info, swap_state(@pe))
959
+ @update_watcher.call(self, swap_state(@pe), stat&.ino)
853
960
  end
854
961
  else
855
962
  @log.info "detected rotation of #{@path}"
856
963
  @io_handler = io_handler
857
964
  end
965
+ @metrics.rotated.inc
858
966
  end
859
967
  end
860
968
 
@@ -934,7 +1042,7 @@ module Fluent::Plugin
934
1042
 
935
1043
  attr_accessor :shutdown_timeout
936
1044
 
937
- def initialize(watcher, path:, read_lines_limit:, read_bytes_limit_per_second:, log:, open_on_every_update:, from_encoding: nil, encoding: nil, &receive_lines)
1045
+ def initialize(watcher, path:, read_lines_limit:, read_bytes_limit_per_second:, log:, open_on_every_update:, from_encoding: nil, encoding: nil, metrics:, &receive_lines)
938
1046
  @watcher = watcher
939
1047
  @path = path
940
1048
  @read_lines_limit = read_lines_limit
@@ -953,10 +1061,15 @@ module Fluent::Plugin
953
1061
  @shutdown_timeout = SHUTDOWN_TIMEOUT
954
1062
  @shutdown_mutex = Mutex.new
955
1063
  @eof = false
1064
+ @metrics = metrics
956
1065
 
957
1066
  @log.info "following tail of #{@path}"
958
1067
  end
959
1068
 
1069
+ def group_watcher
1070
+ @watcher.group_watcher
1071
+ end
1072
+
960
1073
  def on_notify
961
1074
  @notify_mutex.synchronize { handle_notify }
962
1075
  end
@@ -972,6 +1085,7 @@ module Fluent::Plugin
972
1085
  if @io && !@io.closed?
973
1086
  @io.close
974
1087
  @io = nil
1088
+ @metrics.closed.inc
975
1089
  end
976
1090
  end
977
1091
 
@@ -1013,6 +1127,7 @@ module Fluent::Plugin
1013
1127
 
1014
1128
  def handle_notify
1015
1129
  return if limit_bytes_per_second_reached?
1130
+ return if group_watcher&.limit_lines_reached?(@path)
1016
1131
 
1017
1132
  with_io do |io|
1018
1133
  begin
@@ -1022,17 +1137,26 @@ module Fluent::Plugin
1022
1137
  begin
1023
1138
  while true
1024
1139
  @start_reading_time ||= Fluent::Clock.now
1140
+ group_watcher&.update_reading_time(@path)
1141
+
1025
1142
  data = io.readpartial(BYTES_TO_READ, @iobuf)
1026
1143
  @eof = false
1027
1144
  @number_bytes_read += data.bytesize
1028
1145
  @fifo << data
1146
+
1147
+ n_lines_before_read = @lines.size
1029
1148
  @fifo.read_lines(@lines)
1149
+ group_watcher&.update_lines_read(@path, @lines.size - n_lines_before_read)
1030
1150
 
1031
- if limit_bytes_per_second_reached? || should_shutdown_now?
1151
+ group_watcher_limit = group_watcher&.limit_lines_reached?(@path)
1152
+ @log.debug "Reading Limit exceeded #{@path} #{group_watcher.number_lines_read}" if group_watcher_limit
1153
+
1154
+ if group_watcher_limit || limit_bytes_per_second_reached? || should_shutdown_now?
1032
1155
  # Just get out from tailing loop.
1033
1156
  read_more = false
1034
1157
  break
1035
1158
  end
1159
+
1036
1160
  if @lines.size >= @read_lines_limit
1037
1161
  # not to use too much memory in case the file is very large
1038
1162
  read_more = true
@@ -1059,11 +1183,15 @@ module Fluent::Plugin
1059
1183
  def open
1060
1184
  io = Fluent::FileWrapper.open(@path)
1061
1185
  io.seek(@watcher.pe.read_pos + @fifo.bytesize)
1186
+ @metrics.opened.inc
1062
1187
  io
1063
1188
  rescue RangeError
1064
1189
  io.close if io
1065
1190
  raise WatcherSetupError, "seek error with #{@path}: file position = #{@watcher.pe.read_pos.to_s(16)}, reading bytesize = #{@fifo.bytesize.to_s(16)}"
1066
- rescue Errno::ENOENT, Errno::EACCES
1191
+ rescue Errno::EACCES => e
1192
+ @log.warn "#{e}"
1193
+ nil
1194
+ rescue Errno::ENOENT
1067
1195
  nil
1068
1196
  end
1069
1197
 
@@ -1108,6 +1236,10 @@ module Fluent::Plugin
1108
1236
  def opened?
1109
1237
  false
1110
1238
  end
1239
+
1240
+ def eof?
1241
+ true
1242
+ end
1111
1243
  end
1112
1244
 
1113
1245
  class RotateHandler