fluentd 1.16.0 → 1.16.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE/{bug_report.yaml → bug_report.yml} +1 -0
- data/.github/ISSUE_TEMPLATE/{feature_request.yaml → feature_request.yml} +1 -0
- data/.github/workflows/stale-actions.yml +24 -0
- data/.github/workflows/test.yml +39 -0
- data/CHANGELOG.md +161 -0
- data/CONTRIBUTING.md +1 -1
- data/MAINTAINERS.md +3 -3
- data/README.md +0 -2
- data/SECURITY.md +5 -9
- data/fluentd.gemspec +10 -2
- data/lib/fluent/command/ctl.rb +2 -2
- data/lib/fluent/command/fluentd.rb +1 -1
- data/lib/fluent/command/plugin_config_formatter.rb +1 -1
- data/lib/fluent/config/dsl.rb +1 -1
- data/lib/fluent/config/v1_parser.rb +2 -2
- data/lib/fluent/config/yaml_parser/parser.rb +4 -0
- data/lib/fluent/counter/server.rb +1 -1
- data/lib/fluent/counter/validator.rb +3 -3
- data/lib/fluent/engine.rb +1 -1
- data/lib/fluent/event.rb +8 -4
- data/lib/fluent/log.rb +9 -0
- data/lib/fluent/match.rb +1 -1
- data/lib/fluent/msgpack_factory.rb +6 -1
- data/lib/fluent/plugin/base.rb +1 -1
- data/lib/fluent/plugin/buffer.rb +96 -87
- data/lib/fluent/plugin/filter_record_transformer.rb +1 -1
- data/lib/fluent/plugin/in_forward.rb +1 -1
- data/lib/fluent/plugin/in_http.rb +8 -8
- data/lib/fluent/plugin/in_sample.rb +1 -1
- data/lib/fluent/plugin/in_tail/position_file.rb +32 -18
- data/lib/fluent/plugin/in_tail.rb +99 -31
- data/lib/fluent/plugin/in_tcp.rb +43 -0
- data/lib/fluent/plugin/out_exec_filter.rb +2 -2
- data/lib/fluent/plugin/out_file.rb +8 -0
- data/lib/fluent/plugin/output.rb +2 -2
- data/lib/fluent/plugin/parser_json.rb +4 -12
- data/lib/fluent/plugin_helper/event_loop.rb +2 -2
- data/lib/fluent/plugin_helper/record_accessor.rb +1 -1
- data/lib/fluent/plugin_helper/server.rb +8 -0
- data/lib/fluent/plugin_helper/thread.rb +3 -3
- data/lib/fluent/plugin_id.rb +1 -1
- data/lib/fluent/supervisor.rb +2 -2
- data/lib/fluent/system_config.rb +1 -1
- data/lib/fluent/version.rb +1 -1
- data/lib/fluent/winsvc.rb +28 -3
- data/templates/new_gem/test/helper.rb.erb +0 -1
- data/test/command/test_cat.rb +2 -2
- data/test/command/test_fluentd.rb +65 -12
- data/test/config/test_system_config.rb +2 -2
- data/test/helper.rb +27 -7
- data/test/plugin/in_tail/test_io_handler.rb +13 -14
- data/test/plugin/in_tail/test_position_file.rb +37 -8
- data/test/plugin/out_forward/test_ack_handler.rb +3 -3
- data/test/plugin/out_forward/test_socket_cache.rb +3 -3
- data/test/plugin/test_base.rb +1 -1
- data/test/plugin/test_buffer.rb +110 -0
- data/test/plugin/test_buffer_chunk.rb +11 -0
- data/test/plugin/test_in_forward.rb +11 -10
- data/test/plugin/test_in_http.rb +1 -1
- data/test/plugin/test_in_monitor_agent.rb +6 -6
- data/test/plugin/test_in_syslog.rb +25 -18
- data/test/plugin/test_in_tail.rb +639 -0
- data/test/plugin/test_in_tcp.rb +75 -5
- data/test/plugin/test_in_udp.rb +35 -1
- data/test/plugin/test_in_unix.rb +2 -2
- data/test/plugin/test_multi_output.rb +1 -1
- data/test/plugin/test_out_exec_filter.rb +14 -9
- data/test/plugin/test_out_file.rb +24 -4
- data/test/plugin/test_out_forward.rb +36 -42
- data/test/plugin/test_out_stream.rb +1 -1
- data/test/plugin/test_output.rb +12 -12
- data/test/plugin/test_output_as_buffered.rb +44 -44
- data/test/plugin/test_output_as_buffered_compress.rb +32 -18
- data/test/plugin/test_output_as_buffered_retries.rb +1 -1
- data/test/plugin/test_output_as_buffered_secondary.rb +2 -2
- data/test/plugin/test_parser_json.rb +31 -0
- data/test/plugin_helper/test_child_process.rb +15 -5
- data/test/plugin_helper/test_http_server_helper.rb +1 -1
- data/test/plugin_helper/test_server.rb +110 -38
- data/test/plugin_helper/test_socket.rb +1 -1
- data/test/test_config.rb +6 -0
- data/test/test_event_router.rb +2 -2
- data/test/test_log.rb +38 -1
- data/test/test_msgpack_factory.rb +32 -0
- data/test/test_supervisor.rb +13 -0
- metadata +44 -17
- data/.github/workflows/linux-test.yaml +0 -36
- data/.github/workflows/macos-test.yaml +0 -34
- data/.github/workflows/windows-test.yaml +0 -49
data/test/plugin/test_in_tail.rb
CHANGED
|
@@ -2638,4 +2638,643 @@ class TailInputTest < Test::Unit::TestCase
|
|
|
2638
2638
|
end
|
|
2639
2639
|
end
|
|
2640
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
|
+
pos_file_inode = tail_watchers[2].pe.read_inode
|
|
3176
|
+
record_values = d.events.collect { |event| event[2]["message"] }.sort
|
|
3177
|
+
position_entries = []
|
|
3178
|
+
Fluent::FileWrapper.open("#{@tmp_dir}/tail.pos", "r") do |f|
|
|
3179
|
+
f.readlines(chomp: true).each do |line|
|
|
3180
|
+
values = line.split("\t")
|
|
3181
|
+
position_entries.append([values[0], values[1], values[2].to_i(16)])
|
|
3182
|
+
end
|
|
3183
|
+
end
|
|
3184
|
+
|
|
3185
|
+
assert_equal(
|
|
3186
|
+
{
|
|
3187
|
+
record_values: ["file1 log1", "file1 log2", "file2 log1", "file2 log2", "file3 log1", "file3 log2"],
|
|
3188
|
+
tail_watcher_paths: ["#{@tmp_dir}/tail.txt0", "#{@tmp_dir}/tail.txt0", "#{@tmp_dir}/tail.txt0"],
|
|
3189
|
+
tail_watcher_io_handler_opened_statuses: [false, false, false],
|
|
3190
|
+
position_entries: [
|
|
3191
|
+
# The recorded path is old, but it is no problem. The path is not used when using follow_inodes.
|
|
3192
|
+
["#{@tmp_dir}/tail.txt0", "0000000000000016", pos_file_inode],
|
|
3193
|
+
],
|
|
3194
|
+
},
|
|
3195
|
+
{
|
|
3196
|
+
record_values: record_values,
|
|
3197
|
+
tail_watcher_paths: tail_watchers.collect { |tw| tw.path },
|
|
3198
|
+
tail_watcher_io_handler_opened_statuses: tail_watchers.collect { |tw| tw.instance_variable_get(:@io_handler)&.opened? || false },
|
|
3199
|
+
position_entries: position_entries
|
|
3200
|
+
},
|
|
3201
|
+
)
|
|
3202
|
+
end
|
|
3203
|
+
|
|
3204
|
+
def test_next_rotation_occurs_very_fast_while_old_TW_still_waiting_rotate_wait
|
|
3205
|
+
config = config_element(
|
|
3206
|
+
"ROOT",
|
|
3207
|
+
"",
|
|
3208
|
+
{
|
|
3209
|
+
"path" => "#{@tmp_dir}/tail.txt0",
|
|
3210
|
+
"pos_file" => "#{@tmp_dir}/tail.pos",
|
|
3211
|
+
"tag" => "t1",
|
|
3212
|
+
"format" => "none",
|
|
3213
|
+
"read_from_head" => "true",
|
|
3214
|
+
"rotate_wait" => "3s",
|
|
3215
|
+
"refresh_interval" => "1h",
|
|
3216
|
+
}
|
|
3217
|
+
)
|
|
3218
|
+
d = create_driver(config, false)
|
|
3219
|
+
|
|
3220
|
+
tail_watchers = []
|
|
3221
|
+
stub.proxy(d.instance).setup_watcher do |tw|
|
|
3222
|
+
tail_watchers.append(tw)
|
|
3223
|
+
mock.proxy(tw).close.once # Note: Currently, there is no harm in duplicate calls.
|
|
3224
|
+
tw
|
|
3225
|
+
end
|
|
3226
|
+
|
|
3227
|
+
Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "wb") {|f| f.puts "file1 log1"}
|
|
3228
|
+
|
|
3229
|
+
d.run(expect_records: 6, timeout: 15) do
|
|
3230
|
+
Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "ab") {|f| f.puts "file1 log2"}
|
|
3231
|
+
|
|
3232
|
+
sleep 1.5 # Need to be larger than 1s (the interval of watch_timer)
|
|
3233
|
+
|
|
3234
|
+
FileUtils.move("#{@tmp_dir}/tail.txt0", "#{@tmp_dir}/tail.txt" + "1")
|
|
3235
|
+
Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "wb") {|f| f.puts "file2 log1"}
|
|
3236
|
+
Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "ab") {|f| f.puts "file2 log2"}
|
|
3237
|
+
|
|
3238
|
+
sleep 1.5 # Need to be larger than 1s (the interval of watch_timer)
|
|
3239
|
+
|
|
3240
|
+
# Rotate again (Old TailWatcher waiting rotate_wait also calls update_watcher)
|
|
3241
|
+
[1, 0].each do |i|
|
|
3242
|
+
FileUtils.move("#{@tmp_dir}/tail.txt#{i}", "#{@tmp_dir}/tail.txt#{i + 1}")
|
|
3243
|
+
end
|
|
3244
|
+
Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "wb") {|f| f.puts "file3 log1"}
|
|
3245
|
+
Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt0", "ab") {|f| f.puts "file3 log2"}
|
|
3246
|
+
|
|
3247
|
+
# Wait rotate_wait to confirm that TailWatcher.close is not called in duplicate.
|
|
3248
|
+
# (Note: Currently, there is no harm in duplicate calls)
|
|
3249
|
+
sleep 4
|
|
3250
|
+
end
|
|
3251
|
+
|
|
3252
|
+
pos_file_inode = tail_watchers[2].pe.read_inode
|
|
3253
|
+
record_values = d.events.collect { |event| event[2]["message"] }.sort
|
|
3254
|
+
position_entries = []
|
|
3255
|
+
Fluent::FileWrapper.open("#{@tmp_dir}/tail.pos", "r") do |f|
|
|
3256
|
+
f.readlines(chomp: true).each do |line|
|
|
3257
|
+
values = line.split("\t")
|
|
3258
|
+
position_entries.append([values[0], values[1], values[2].to_i(16)])
|
|
3259
|
+
end
|
|
3260
|
+
end
|
|
3261
|
+
|
|
3262
|
+
assert_equal(
|
|
3263
|
+
{
|
|
3264
|
+
record_values: ["file1 log1", "file1 log2", "file2 log1", "file2 log2", "file3 log1", "file3 log2"],
|
|
3265
|
+
tail_watcher_paths: ["#{@tmp_dir}/tail.txt0", "#{@tmp_dir}/tail.txt0", "#{@tmp_dir}/tail.txt0"],
|
|
3266
|
+
tail_watcher_io_handler_opened_statuses: [false, false, false],
|
|
3267
|
+
position_entries: [
|
|
3268
|
+
["#{@tmp_dir}/tail.txt0", "0000000000000016", pos_file_inode],
|
|
3269
|
+
],
|
|
3270
|
+
},
|
|
3271
|
+
{
|
|
3272
|
+
record_values: record_values,
|
|
3273
|
+
tail_watcher_paths: tail_watchers.collect { |tw| tw.path },
|
|
3274
|
+
tail_watcher_io_handler_opened_statuses: tail_watchers.collect { |tw| tw.instance_variable_get(:@io_handler)&.opened? || false },
|
|
3275
|
+
position_entries: position_entries
|
|
3276
|
+
},
|
|
3277
|
+
)
|
|
3278
|
+
end
|
|
3279
|
+
end
|
|
2641
3280
|
end
|