Skip to content

Commit 8360a28

Browse files
iberianpigclaude
andcommitted
fix: improve error handling and empty config behavior in TailContextInput
- Add warning log when tail_context command fails with exit status - Change empty config behavior from break to sleep+continue for hot-reload support - Update RBS type definitions for detector and tail_context_input - Add tests for error logging and empty config loop behavior Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 804cfa5 commit 8360a28

4 files changed

Lines changed: 58 additions & 10 deletions

File tree

lib/fusuma/plugin/inputs/tail_context_input.rb

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require "open3"
34
require_relative "input"
45

56
module Fusuma
@@ -30,15 +31,19 @@ def start(reader, writer)
3031
# Returns nil on failure
3132
#: (String) -> String?
3233
def execute_command(command)
33-
result = `#{command}`
34-
return nil unless $?.success?
34+
stdout, stderr, status = Open3.capture3(command)
35+
unless status.success?
36+
MultiLogger.warn "tail_context command failed: #{command} (exit status: #{status.exitstatus})"
37+
MultiLogger.warn " stderr: #{stderr}" unless stderr.empty?
38+
return nil
39+
end
3540

36-
result.strip
41+
stdout.strip
3742
end
3843

3944
# Reads the tail_context section from the config file
4045
# Returns an empty Hash if no config is found
41-
#: () -> Hash[untyped, untyped]
46+
#: () -> (String | Hash[untyped, untyped] | Integer | Float)
4247
def tail_contexts
4348
Config.search(Config::Index.new(:tail_context)) || {}
4449
end
@@ -70,10 +75,14 @@ def create_io
7075
IO.pipe
7176
end
7277

78+
#: (StringIO) -> void
7379
def watch_loop(writer)
7480
loop do
7581
contexts = tail_contexts
76-
break if contexts.empty?
82+
if contexts.empty?
83+
sleep DEFAULT_INTERVAL
84+
next
85+
end
7786

7887
contexts.each do |name, config|
7988
command = config[:command]

sig/generated/lib/fusuma/plugin/detectors/detector.rbs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ module Fusuma
2929

3030
# @param record [Events::Records::Record]
3131
# @return [Events::Event]
32-
# : (record: Fusuma::Plugin::Events::Records::IndexRecord) -> Fusuma::Plugin::Events::Event
33-
def create_event: (record: Fusuma::Plugin::Events::Records::IndexRecord) -> Fusuma::Plugin::Events::Event
32+
# : (record: Fusuma::Plugin::Events::Records::Record) -> Fusuma::Plugin::Events::Event
33+
def create_event: (record: Fusuma::Plugin::Events::Records::Record) -> Fusuma::Plugin::Events::Event
3434

3535
# : () -> Time
3636
def last_time: () -> Time

sig/generated/lib/fusuma/plugin/inputs/tail_context_input.rbs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ module Fusuma
1919

2020
# Reads the tail_context section from the config file
2121
# Returns an empty Hash if no config is found
22-
# : () -> Hash[untyped, untyped]
23-
def tail_contexts: () -> Hash[untyped, untyped]
22+
# : () -> (String | Hash[untyped, untyped] | Integer | Float)
23+
def tail_contexts: () -> (String | Hash[untyped, untyped] | Integer | Float)
2424

2525
# Executes the specified command periodically and
2626
# writes to writer in "name:value" format only when the value changes
@@ -36,7 +36,8 @@ module Fusuma
3636

3737
def create_io: () -> untyped
3838

39-
def watch_loop: (untyped writer) -> untyped
39+
# : (StringIO) -> void
40+
def watch_loop: (StringIO) -> void
4041

4142
def find_min_interval: (untyped contexts) -> untyped
4243
end

spec/fusuma/plugin/inputs/tail_context_input_spec.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ module Inputs
4444
result = @input.execute_command("exit 1")
4545
expect(result).to be_nil
4646
end
47+
48+
it "logs warning with MultiLogger.warn when command fails" do
49+
expect(MultiLogger).to receive(:warn).with(/tail_context command failed:.*exit 1.*exit status: 1/)
50+
@input.execute_command("exit 1")
51+
end
52+
53+
it "logs stderr when command fails with error output" do
54+
expect(MultiLogger).to receive(:warn).with(/tail_context command failed:/)
55+
expect(MultiLogger).to receive(:warn).with(/stderr:.*error message/)
56+
@input.execute_command("echo 'error message' >&2 && exit 1")
57+
end
4758
end
4859

4960
describe "#tail_contexts" do
@@ -130,6 +141,33 @@ module Inputs
130141
expect(@writer.read).to eq ""
131142
end
132143
end
144+
145+
describe "#watch_loop (private)" do
146+
before do
147+
@writer = StringIO.new
148+
@input.reset_last_values
149+
end
150+
151+
it "sleeps DEFAULT_INTERVAL and continues loop when config is empty" do
152+
call_count = 0
153+
allow(@input).to receive(:tail_contexts) do
154+
call_count += 1
155+
raise StopIteration if call_count > 2
156+
{}
157+
end
158+
allow(@input).to receive(:sleep).with(1.0)
159+
160+
expect(@input).to receive(:sleep).with(1.0).at_least(:once)
161+
162+
begin
163+
@input.send(:watch_loop, @writer)
164+
rescue StopIteration
165+
# expected
166+
end
167+
168+
expect(call_count).to be > 1
169+
end
170+
end
133171
end
134172
end
135173
end

0 commit comments

Comments
 (0)