Skip to content

Commit ad48f0c

Browse files
committed
add feature to detect too low collection ratio
Signed-off-by: Daijiro Fukuda <fukuda@clear-code.com>
1 parent b5e47bb commit ad48f0c

12 files changed

Lines changed: 212 additions & 14 deletions

File tree

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ gem "irb"
99
gem "rake", "~> 13.0"
1010

1111
gem "test-unit", "~> 3.0"
12+
gem 'test-unit-rr'

fluent-tail_checker.gemspec

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,5 @@ Gem::Specification.new do |spec|
3131
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
3232
spec.require_paths = ["lib"]
3333

34-
# Uncomment to register a new dependency of your gem
35-
# spec.add_dependency "example-gem", "~> 1.0"
36-
37-
# For more information and examples about making a new gem, check out our
38-
# guide at: https://bundler.io/guides/creating_gem.html
34+
spec.add_dependency "fluentd", "~> 1.0"
3935
end
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright 2025 Daijiro Fukuda
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
require "fluent/env"
18+
19+
# For compatibility with Fluentd v1.15.2 and earlier.
20+
# See https://github.com/fluent/fluentd/pull/3883
21+
if Fluent.windows?
22+
require "fluent/file_wrapper"
23+
else
24+
Fluent::FileWrapper = File
25+
end
26+
27+
module Fluent
28+
module TailChecker
29+
class CollectionRatioChecker
30+
def initialize(posfile, follow_inodes)
31+
@posfile = posfile
32+
@follow_inodes = follow_inodes
33+
@collection_ratio_threshold = 0.8
34+
end
35+
36+
def check
37+
unacceptable_collection_ratio_found = false
38+
unacceptable_collection_ratio_path_and_ratio_list = []
39+
checked_file_counts = 0
40+
41+
@posfile.watching_entries.each do |entry|
42+
size = get_file_size(entry)
43+
next if size.nil?
44+
45+
checked_file_counts += 1
46+
ratio = collection_ratio(entry.pos, size)
47+
if ratio < @collection_ratio_threshold
48+
unacceptable_collection_ratio_found = true
49+
unacceptable_collection_ratio_path_and_ratio_list.append([entry.path, ratio])
50+
end
51+
end
52+
53+
puts "Checked collection ratio of #{checked_file_counts} files."
54+
55+
if unacceptable_collection_ratio_found
56+
log_issue(unacceptable_collection_ratio_path_and_ratio_list)
57+
return false
58+
end
59+
60+
true
61+
end
62+
63+
def get_file_size(pos_entry)
64+
if @follow_inodes
65+
get_file_size_from_inode(pos_entry)
66+
else
67+
get_file_size_from_path(pos_entry)
68+
end
69+
end
70+
71+
def get_file_size_from_path(pos_entry)
72+
FileTest.size?(pos_entry.path)
73+
end
74+
75+
def get_file_size_from_inode(pos_entry)
76+
return nil unless FileTest.exist?(pos_entry.path)
77+
78+
stat = Fluent::FileWrapper.stat(pos_entry.path)
79+
# If follow_inodes is enabled, the inode of the current logfile should match the inode in the pos_file.
80+
# It may not match for the rotated logfiles because the path info in the pos_file is not updated.
81+
# So, at least, check the current logfile.
82+
# For rotated logfiles, check them if inode matches.
83+
return nil unless stat.ino == pos_entry.ino
84+
85+
stat.size
86+
end
87+
88+
def collection_ratio(pos, file_size)
89+
return 1.0 if file_size == 0
90+
91+
pos.to_f / file_size
92+
end
93+
94+
def log_issue(path_and_ratio_list)
95+
puts "Collection ratio of some files are too low. Collection of those files may have stopped or may not be keeping up."
96+
if @follow_inodes
97+
puts "This can be a known log loss issue of the follow_inodes feature that was fixed in Fluentd v1.16.2."
98+
end
99+
100+
puts "Filepaths with too low collection ratio (threshold: #{@collection_ratio_threshold}):"
101+
path_and_ratio_list.each do |path, ratio|
102+
puts " #{path} (ratio: #{ratio})"
103+
end
104+
end
105+
end
106+
end
107+
end

lib/fluent/tail_checker/duplicated_pos_checker.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,20 @@ def check
2828
duplicated_key_found = false
2929
key_set = Set.new
3030
duplicated_key_set = Set.new
31+
checked_key_counts = 0
3132

3233
@posfile.watching_entries.map do |entry|
3334
@follow_inodes ? entry.ino : entry.path
3435
end.each do |key|
36+
checked_key_counts += 1
3537
next if key_set.add?(key)
3638

3739
duplicated_key_found = true
3840
duplicated_key_set.add(key)
3941
end
4042

43+
puts "Checked duplicated pos for #{checked_key_counts} PosEntries."
44+
4145
if duplicated_key_found
4246
log_issue(duplicated_key_set)
4347
return false

lib/fluent/tail_checker/pos.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ module TailChecker
2222
class PosFile
2323
attr_reader :entries
2424

25-
def initialize(path)
26-
@entries = PosFile.load_entries(path)
25+
def initialize(entries)
26+
@entries = entries
27+
end
28+
29+
def self.load(path)
30+
PosFile.new(PosFile.load_entries(path))
2731
end
2832

2933
def self.load_entries(path)

lib/fluent/tail_checker/tail_check.rb

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
require_relative "pos"
2020
require_relative "duplicated_pos_checker"
21+
require_relative "collection_ratio_checker"
2122

2223
module Fluent
2324
module TailChecker
@@ -75,7 +76,11 @@ def check
7576

7677
@pos_filepaths.each do |path|
7778
puts "\nCheck #{path}."
78-
succeeded = check_pos_file(path) && succeeded
79+
pos_file = try_to_open_pos_file(path)
80+
next if pos_file.nil?
81+
82+
succeeded = DuplicatedPosChecker.new(pos_file, @follow_inodes).check && succeeded
83+
succeeded = CollectionRatioChecker.new(pos_file, @follow_inodes).check && succeeded
7984
end
8085

8186
puts "\nAll check completed."
@@ -90,11 +95,8 @@ def check
9095
true
9196
end
9297

93-
def check_pos_file(path)
94-
succeeded = true
95-
posfile = PosFile.new(path)
96-
succeeded = DuplicatedPosChecker.new(posfile, @follow_inodes).check && succeeded
97-
succeeded
98+
def try_to_open_pos_file(path)
99+
PosFile.load(path)
98100
rescue => e
99101
$stderr.puts "Can not open the file. Skipped. Path: #{path}, Error: #{e}"
100102
end

test/data/log/bar.log

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
barbar
2+
barbarbarbar
3+
barbarbarbarbarbarbarbar

test/data/log/foo.log

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
foo
2+
foofoo
3+
foofoofoo
4+
foofoofoofoo

test/data/log/foo.log.1

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
foo
2+
foofoo
3+
foofoofoo
4+
foofoofoofoo
5+
foo
6+
foofoo
7+
foofoofoo
8+
foofoofoofoo
9+
foo
10+
foofoo
11+
foofoofoo
12+
foofoofoofoo
13+
foo
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
class Fluent::TailChecker::CollectionRatioCheckerTest < Test::Unit::TestCase
6+
data("Normal ratio", [["test/data/log/foo.log", "test/data/log/bar.log"], 0.9, false, true])
7+
data("Too low ratio", [["test/data/log/foo.log", "test/data/log/bar.log"], 0.7, false, false])
8+
data("Normal ratio with follow_inodes", [["test/data/log/foo.log", "test/data/log/bar.log"], 0.9, true, true])
9+
data("Too low ratio with follow_inodes", [["test/data/log/foo.log", "test/data/log/foo.log.1"], 0.7, true, false])
10+
test "Check should return false when too low collection ratio is detected" do |(paths, ratio, follow_inodes, expected)|
11+
pos_entries = paths.map do |path|
12+
stat = Fluent::FileWrapper.stat(path)
13+
Fluent::TailChecker::PosEntry.new(path, stat.size * ratio, stat.ino)
14+
end
15+
pos_file = Fluent::TailChecker::PosFile.new(pos_entries)
16+
checker = Fluent::TailChecker::CollectionRatioChecker.new(pos_file, follow_inodes)
17+
18+
result = checker.check
19+
20+
assert_equal(expected, result)
21+
end
22+
end

0 commit comments

Comments
 (0)