fluentd 1.13.3 → 1.16.5

Sign up to get free protection for your applications and to get access to all the features.
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