Skip to content

Commit 6dd76cc

Browse files
committed
add feature to detect duplicated pos entries
Signed-off-by: Daijiro Fukuda <fukuda@clear-code.com>
1 parent a2a9c65 commit 6dd76cc

11 files changed

Lines changed: 311 additions & 11 deletions

File tree

.github/workflows/main.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,5 @@ jobs:
4242
bundler-cache: true
4343
- name: Install
4444
run: rake install
45-
- name: Run exe
46-
run: |
47-
tailcheck
45+
- name: Test command
46+
run: test/script/command-test.bash

exe/tailcheck

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
#!/usr/bin/env ruby
22
# frozen_string_literal: true
33

4-
require 'fluent/tail_checker'
5-
6-
puts Fluent::TailChecker::VERSION
4+
require "fluent/tail_checker"
75

86
command = Fluent::TailChecker::TailCheck.new
97
exit(command.run)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 "set"
18+
19+
module Fluent
20+
module TailChecker
21+
class DuplicatedPosChecker
22+
def initialize(posfile, follow_inodes)
23+
@posfile = posfile
24+
@follow_inodes = follow_inodes
25+
end
26+
27+
def check
28+
duplicated_key_found = false
29+
key_set = Set.new
30+
duplicated_key_set = Set.new
31+
32+
@posfile.entries.filter do |entry|
33+
not entry.unwatched?
34+
end.map do |entry|
35+
@follow_inodes ? entry.ino : entry.path
36+
end.each do |key|
37+
next if key_set.add?(key)
38+
39+
duplicated_key_found = true
40+
duplicated_key_set.add(key)
41+
end
42+
43+
if duplicated_key_found
44+
log_issue(duplicated_key_set)
45+
return false
46+
end
47+
48+
true
49+
end
50+
51+
def log_issue(duplicated_keys)
52+
if @follow_inodes
53+
puts "Duplicated PosEntries are found with follow_inodes. Unknown anomalies may be occurring."
54+
else
55+
puts "Duplicated PosEntries are found. This is a known log loss issue that was fixed in Fluentd v1.16.3."
56+
end
57+
end
58+
end
59+
end
60+
end

lib/fluent/tail_checker/pos.rb

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
module Fluent
18+
module TailChecker
19+
UNWATCHED_POSITION = 0xffffffffffffffff
20+
POSITION_FILE_ENTRY_REGEX = /^([^\t]+)\t([0-9a-fA-F]+)\t([0-9a-fA-F]+)/
21+
22+
class PosFile
23+
attr_reader :entries
24+
25+
def initialize(path)
26+
@entries = PosFile.load_entries(path)
27+
end
28+
29+
def self.load_entries(path)
30+
entries = []
31+
32+
File.open(path, File::RDONLY|File::BINARY) do |file|
33+
file.each_line do |line|
34+
m = POSITION_FILE_ENTRY_REGEX.match(line)
35+
next if m.nil?
36+
37+
entries << PosEntry.new(
38+
m[1],
39+
m[2].to_i(16),
40+
m[3].to_i(16),
41+
)
42+
end
43+
end
44+
45+
entries
46+
end
47+
end
48+
49+
class PosEntry
50+
attr_reader :path, :pos, :ino
51+
52+
def initialize(path, pos, ino)
53+
@path = path
54+
@pos = pos
55+
@ino = ino
56+
end
57+
58+
def unwatched?
59+
@pos == UNWATCHED_POSITION
60+
end
61+
end
62+
end
63+
end

lib/fluent/tail_checker/tail_check.rb

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,90 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17+
require "optparse"
18+
19+
require_relative "pos"
20+
require_relative "duplicated_pos_checker"
21+
1722
module Fluent
1823
module TailChecker
1924
class TailCheck
20-
def run
21-
# TODO
22-
puts "TailCheck command run."
25+
def initialize
26+
@pos_filepaths = []
27+
@follow_inodes = false
28+
end
29+
30+
def run(argv=ARGV)
31+
parse_command_line(argv)
32+
@pos_filepaths = validate_paths(@pos_filepaths).to_a
33+
check
34+
end
35+
36+
def parse_command_line(argv)
37+
parser = OptionParser.new
38+
parser.version = VERSION
39+
parser.banner = <<~BANNER
40+
Usage: tailcheck [OPTIONS] POS_FILE...
41+
Example: tailcheck /path/to/pos1 /path/to/pos2
42+
Example: tailcheck /path/to/pos/*
43+
Example: tailcheck --follow_inodes /path/to/pos_with_follow_inodes
44+
Options:
45+
46+
BANNER
47+
48+
parser.on("--follow_inodes", "Check the specified pos files with the condition that the follow_inodes feature is enabled.", "Default: Disabled") do
49+
@follow_inodes = true
50+
end
51+
52+
@pos_filepaths = parser.parse(argv)
53+
end
54+
55+
def validate_paths(paths)
56+
Enumerator.new do |y|
57+
paths.each do |path|
58+
unless FileTest.exist?(path)
59+
$stderr.puts "File does not exist. Skipped. Path: #{path}"
60+
next
61+
end
62+
63+
y << path
64+
end
65+
end
66+
end
67+
68+
def check
69+
if @pos_filepaths.empty?
70+
$stderr.puts "No pos_file to be checked. Please specify valid pos_file paths."
71+
return false
72+
end
73+
74+
succeeded = true
75+
76+
@pos_filepaths.each do |path|
77+
puts "\nCheck #{path}."
78+
succeeded = check_pos_file(path) && succeeded
79+
end
80+
81+
puts "\nAll check completed."
82+
83+
unless succeeded
84+
puts "Some anomalies are found. Please check whether there is any log loss."
85+
# TODO add message about how to concact the community or us.
86+
return false
87+
end
88+
89+
puts "There is no anomalies."
2390
true
2491
end
92+
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+
rescue => e
99+
$stderr.puts "Can not open the file. Skipped. Path: #{path}, Error: #{e}"
100+
end
25101
end
26102
end
27103
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/test/foo.log ffffffffffffffff 00000000060e3795
2+
/test/foo.log.1 ffffffffffffffff 00000000060e3794
3+
/test/foo.log.2 000000000000000c 00000000060e3793
4+
/test/foo.log.3 ffffffffffffffff 00000000060e3796
5+
/test/foo.log 0000000000000004 00000000060e379a
6+
/test/foo.log.1 000000000000000c 00000000060e3795
7+
/test/foo.log 0000000000000004 00000000060e3796
8+
/test/foo.log 0000000000000004 00000000060e3793
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/test/foo.log ffffffffffffffff 00000000060e3789
2+
/test/bar.log 0000000000000008 00000000060e3794
3+
/test/foo.log 0000000000000008 00000000060e3789
4+
/test/foo.log ffffffffffffffff 0000000000000000
5+
/test/foo.log 000000000000000c 00000000060e378c

test/data/pos_follow_inodes_normal

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/test/foo.log ffffffffffffffff 00000000060e3795
2+
/test/foo.log.1 ffffffffffffffff 00000000060e3794
3+
/test/foo.log.2 ffffffffffffffff 00000000060e3793
4+
/test/foo.log.3 ffffffffffffffff 00000000060e3796
5+
/test/foo.log 0000000000000004 00000000060e379a
6+
/test/foo.log.1 000000000000000c 00000000060e3795
7+
/test/foo.log 0000000000000004 00000000060e3796
8+
/test/foo.log 0000000000000004 00000000060e3793

test/data/pos_normal

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/test/foo.log ffffffffffffffff 00000000060e3789
2+
/test/bar.log 0000000000000008 00000000060e3794
3+
/test/foo.log 0000000000000004 00000000060e378a

test/fluent/tail_checker_test.rb

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,78 @@ class Fluent::TailCheckerTest < Test::Unit::TestCase
99
end
1010
end
1111

12-
test "something useful" do
13-
assert_equal("actual", "actual")
12+
sub_test_case "Parse args" do
13+
data(
14+
"Minimum",
15+
[
16+
["/path/to/pos"],
17+
{ pos_filepaths: ["/path/to/pos"], follow_inodes: false },
18+
]
19+
)
20+
data(
21+
"Full",
22+
[
23+
["--follow_inodes", "/path/to/pos", "/path/to/pos2"],
24+
{ pos_filepaths: ["/path/to/pos", "/path/to/pos2"], follow_inodes: true },
25+
]
26+
)
27+
test "Correct args" do |(args, expected)|
28+
tail_check = Fluent::TailChecker::TailCheck.new
29+
30+
tail_check.parse_command_line(args)
31+
32+
result = {
33+
pos_filepaths: tail_check.instance_variable_get(:@pos_filepaths),
34+
follow_inodes: tail_check.instance_variable_get(:@follow_inodes),
35+
}
36+
assert_equal(expected, result)
37+
end
38+
39+
data("Invalid options", ["--foo", "/path/to/pos"])
40+
test "Raise error for invalid options" do |args|
41+
tail_check = Fluent::TailChecker::TailCheck.new
42+
43+
assert_raise(OptionParser::InvalidOption) do
44+
tail_check.parse_command_line(args)
45+
end
46+
end
47+
end
48+
49+
sub_test_case "Validate pos_file paths" do
50+
data(
51+
"Mixed existant and nonexistant paths",
52+
[
53+
["test/data/pos_normal", "foo", "test/data/pos_follow_inodes_normal", "bar"],
54+
["test/data/pos_normal", "test/data/pos_follow_inodes_normal"],
55+
]
56+
)
57+
test "Exclude nonexistant paths" do |(paths, expected)|
58+
tail_check = Fluent::TailChecker::TailCheck.new
59+
60+
validated_paths = tail_check.validate_paths(paths).to_a
61+
62+
assert_equal(expected, validated_paths)
63+
end
64+
end
65+
66+
sub_test_case "Check" do
67+
data("No pos_file", [[], false, false])
68+
data("No pos_file with follow_inodes", [[], true, false])
69+
data("Normal", [["test/data/pos_normal"], false, true])
70+
data("Duplicated unwatched paths", [["test/data/pos_duplicate_unwatched_path"], false, false])
71+
data("Normal with follow_inodes", [["test/data/pos_follow_inodes_normal"], true, true])
72+
data("Duplicated unwatched inodes with follow_inodes", [["test/data/pos_duplicate_unwatched_inode"], true, false])
73+
data("Duplicated unwatched paths with follow_inodes", [["test/data/pos_duplicate_unwatched_path"], true, true])
74+
data("Normal multiple", [["test/data/pos_follow_inodes_normal", "test/data/pos_duplicate_unwatched_path"], true, true])
75+
data("Anomaly multiple", [["test/data/pos_follow_inodes_normal", "test/data/pos_duplicate_unwatched_inode"], true, false])
76+
test "Return false when an anomaly is detected or there is no target" do |(paths, follow_inodes, expected)|
77+
tail_check = Fluent::TailChecker::TailCheck.new
78+
tail_check.instance_variable_set(:@pos_filepaths, paths)
79+
tail_check.instance_variable_set(:@follow_inodes, follow_inodes)
80+
81+
result = tail_check.check
82+
83+
assert_equal(expected, result)
84+
end
1485
end
1586
end

0 commit comments

Comments
 (0)