Skip to content

Commit 044dd81

Browse files
AkashKumar7902Watson1978claude
authored
parser_syslog: accept a space after RFC3164 priority (#5449)
<!-- Thank you for contributing to Fluentd! Your commits need to follow DCO: https://probot.github.io/apps/dco/ And please provide the following information to help us make the most of your pull request: --> **Which issue(s) this PR fixes**: Fixes #4158 **What this PR does / why we need it**: Some syslog senders emit RFC3164 records with an ASCII space between the priority and header. Depending on whether `in_syslog` or the parser owns priority extraction, the RFC3164 parser sees that space either immediately after `>` or as the first character of its input, and currently attempts to parse it as the start of the timestamp. This change accepts the RFC3164 boundary space in `rfc3164` and the RFC3164 path of `auto`, across both parser engines and both priority-ownership models. Canonical records without the space continue to parse unchanged, malformed priorities remain invalid, and RFC5424 parsing is unchanged. For the default RFC3164 time format, regression coverage verifies that two boundary spaces and tabs remain invalid. An explicit RFC3164 `time_format` beginning with a space treats that leading space as timestamp syntax, so this change does not impose or claim a one-space maximum for that configuration. This also corrects the string parser's dynamic timestamp slice cursor and the field-count accounting needed by RFC3164 time formats that begin with a space. Truncated subsecond records with the boundary space are rejected through the normal `[nil, nil]` path instead of raising while looking for the missing field delimiter. **Docs Changes**: None. This broadens input compatibility without adding or changing configuration. **Release Note**: parser_syslog: accept one optional ASCII space between RFC3164 priority and header. **Testing**: - `bundle exec ruby -Itest test/plugin/test_parser_syslog.rb --name test_truncated_subsecond_rfc3164_is_rejected_without_raising` — 4 tests, 12 assertions, 0 failures, 0 errors across `rfc3164`/`auto` and parser-owned/input-owned priority - `bundle exec ruby -Itest test/plugin/test_parser_syslog.rb` — 132 tests, 521 assertions, 0 failures, 0 errors - `bundle exec ruby -Itest test/plugin/test_in_syslog.rb` — 41 tests, 280 assertions, 0 failures, 0 errors - `bundle exec rake test` — 4,401 tests, 16,154 assertions, 0 failures, 0 errors, 3 pendings, 36 omissions - `gem exec rubocop` — 460 files inspected, 0 offenses - Balanced microbenchmark with 8 samples per revision and case — all 8 prespecified fixed regression gates passed. The narrowest throughput ratio was 0.953; this was used only as a regression gate, and no performance improvement is claimed. - Installed-gem UDP validation — 20/20 scenarios passed. Both baseline and candidate gems were built and installed into isolated environments, real `fluentd --no-supervisor` processes were started, and packets were sent by external UDP clients. The original 16-scenario matrix covered both parser engines, both priority-ownership models, `rfc3164`/`auto`, canonical and spaced packets, whitespace rejection, RFC5424 compatibility, and explicit leading-space time formats. Four additional string-parser scenarios verified that the truncated subsecond packet takes the normal unmatched path without an exception and that a valid `.5` sibling is parsed by the same live process across `rfc3164`/`auto` and both priority-ownership modes. --------- Signed-off-by: Akash Kumar <meakash7902@gmail.com> Signed-off-by: Shizuo Fujita <fujita@clear-code.com> Co-authored-by: Shizuo Fujita <fujita@clear-code.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 2e91eb6 commit 044dd81

3 files changed

Lines changed: 436 additions & 6 deletions

File tree

‎lib/fluent/plugin/parser_syslog.rb‎

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def initialize
7171
@time_parser_rfc5424 = nil
7272
@space_count_rfc3164 = nil
7373
@space_count_rfc5424 = nil
74+
@time_format_starts_with_space_rfc3164 = false
7475
@skip_space_count_rfc3164 = false
7576
@skip_space_count_rfc5424 = false
7677
@time_parser_rfc5424_without_subseconds = nil
@@ -123,10 +124,12 @@ class << self
123124

124125
def setup_time_parser_3164(time_fmt)
125126
@time_parser_rfc3164 = time_parser_create(format: time_fmt)
127+
@time_format_starts_with_space_rfc3164 = time_fmt.start_with?(SPLIT_CHAR)
126128
if ['%b %d %H:%M:%S', '%b %d %H:%M:%S.%N'].include?(time_fmt)
127129
@skip_space_count_rfc3164 = true
128130
end
129131
@space_count_rfc3164 = time_fmt.squeeze(' ').count(' ') + 1
132+
@space_count_rfc3164 -= 1 if @time_format_starts_with_space_rfc3164
130133
end
131134

132135
def setup_time_parser_5424(time_fmt)
@@ -162,6 +165,8 @@ def parse_auto(text, &block)
162165
end
163166

164167
SPLIT_CHAR = ' '.freeze
168+
DOT_CHAR = '.'.freeze
169+
RFC3164_PRI_DIGITS_REGEXP = /\A[0-9]{1,3}\z/
165170

166171
def parse_rfc3164_regex(text, &block)
167172
idx = 0
@@ -180,13 +185,26 @@ def parse_rfc3164_regex(text, &block)
180185

181186
i = idx - 1
182187
sq = false
188+
first_time_field = true
183189
@space_count_rfc3164.times do
184190
while text[i + 1] == SPLIT_CHAR
191+
if first_time_field
192+
unless @time_format_starts_with_space_rfc3164
193+
idx += 1
194+
i += 1
195+
break
196+
end
197+
end
185198
sq = true
186199
i += 1
187200
end
188201

202+
first_time_field = false
189203
i = text.index(SPLIT_CHAR, i + 1)
204+
unless i
205+
yield nil, nil
206+
return
207+
end
190208
end
191209

192210
time_str = sq ? text.slice(idx, i - idx).squeeze(SPLIT_CHAR) : text.slice(idx, i - idx)
@@ -287,16 +305,30 @@ def parse_rfc3164(text, &block)
287305
end
288306
end
289307

308+
if text[cursor] == SPLIT_CHAR
309+
cursor = rfc3164_space_cursor(text, cursor)
310+
unless cursor
311+
yield nil, nil
312+
return
313+
end
314+
end
315+
290316
if @skip_space_count_rfc3164
291317
# header part
292318
time_size = 15 # skip Mmm dd hh:mm:ss
293-
time_end = text[cursor + time_size]
319+
time_end_index = cursor + time_size
320+
time_end = text[time_end_index]
321+
294322
if time_end == SPLIT_CHAR
295323
time_str = text.slice(cursor, time_size)
296324
cursor += 16 # time + ' '
297-
elsif time_end == '.'.freeze
325+
elsif time_end == DOT_CHAR
298326
# support subsecond time
299-
i = text.index(SPLIT_CHAR, time_size)
327+
i = text.index(SPLIT_CHAR, time_end_index)
328+
unless i
329+
yield nil, nil
330+
return
331+
end
300332
time_str = text.slice(cursor, i - cursor)
301333
cursor = i + 1
302334
else
@@ -312,14 +344,18 @@ def parse_rfc3164(text, &block)
312344
i += 1
313345
end
314346
i = text.index(SPLIT_CHAR, i + 1)
347+
unless i
348+
yield nil, nil
349+
return
350+
end
315351
end
316352

317-
time_str = sq ? text.slice(idx, i - cursor).squeeze(SPLIT_CHAR) : text.slice(cursor, i - cursor)
353+
time_str = sq ? text.slice(cursor, i - cursor).squeeze(SPLIT_CHAR) : text.slice(cursor, i - cursor)
318354
cursor = i + 1
319355
end
320356

321357
i = text.index(SPLIT_CHAR, cursor)
322-
if i.nil?
358+
unless i
323359
yield nil, nil
324360
return
325361
end
@@ -363,12 +399,23 @@ def parse_rfc3164(text, &block)
363399
msg.chomp!
364400
record['message'] = msg
365401

366-
time = @time_parser_rfc3164.parse(time_str)
402+
begin
403+
time = @time_parser_rfc3164.parse(time_str)
404+
rescue Fluent::TimeParser::TimeParseError
405+
yield nil, nil
406+
return
407+
end
367408
record['time'] = time_str if @keep_time_key
368409

369410
yield time, record
370411
end
371412

413+
def rfc3164_space_cursor(text, cursor)
414+
return if @with_priority && !RFC3164_PRI_DIGITS_REGEXP.match?(text.slice(1, cursor - 2))
415+
416+
@time_format_starts_with_space_rfc3164 ? cursor : cursor + 1
417+
end
418+
372419
NILVALUE = '-'.freeze
373420

374421
def parse_rfc5424(text, &block)

‎test/plugin/test_in_syslog.rb‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,79 @@ def test_time_format(data)
124124
}
125125
end
126126

127+
data(
128+
'regexp/rfc3164/parser priority' => ['regexp', 'rfc3164', true],
129+
'string/rfc3164/parser priority' => ['string', 'rfc3164', true],
130+
'regexp/auto/parser priority' => ['regexp', 'auto', true],
131+
'string/auto/parser priority' => ['string', 'auto', true],
132+
'regexp/rfc3164/input priority' => ['regexp', 'rfc3164', false],
133+
'string/rfc3164/input priority' => ['string', 'rfc3164', false],
134+
'regexp/auto/input priority' => ['regexp', 'auto', false],
135+
'string/auto/input priority' => ['string', 'auto', false],
136+
)
137+
def test_space_between_rfc3164_priority_and_header(data)
138+
parser_engine, message_format, with_priority = data
139+
d = create_driver([
140+
ipv4_config,
141+
'severity_key severity',
142+
'facility_key facility',
143+
'<parse>',
144+
" parser_engine #{parser_engine}",
145+
" message_format #{message_format}",
146+
" with_priority #{with_priority}",
147+
'</parse>',
148+
].join("\n"))
149+
150+
message = 'Apr 25 16:43:29 PAA-SW1-1 General[procLOG]: main.c(257) 272264 %% Stopping System API application'
151+
d.run(expect_emits: 2) do
152+
u = UDPSocket.new
153+
u.connect('127.0.0.1', @port)
154+
u.send("<14>#{message}", 0)
155+
u.send("<14> #{message}", 0)
156+
end
157+
158+
assert_equal(2, d.events.size)
159+
assert_equal(d.events[0][1], d.events[1][1])
160+
assert_equal(d.events[0][2], d.events[1][2])
161+
d.events.each do |tag, time, record|
162+
assert_equal('syslog.user.info', tag)
163+
assert_equal(event_time('Apr 25 16:43:29', format: '%b %d %H:%M:%S'), time)
164+
assert_equal('user', record['facility'])
165+
assert_equal('info', record['severity'])
166+
assert_equal('PAA-SW1-1', record['host'])
167+
assert_equal('General', record['ident'])
168+
assert_equal('main.c(257) 272264 %% Stopping System API application', record['message'])
169+
end
170+
end
171+
172+
data('regexp' => 'regexp', 'string' => 'string')
173+
def test_space_after_input_owned_priority_preserves_input_priority_syntax(parser_engine)
174+
d = create_driver([
175+
ipv4_config,
176+
'emit_unmatched_lines true',
177+
'<parse>',
178+
" parser_engine #{parser_engine}",
179+
' with_priority false',
180+
'</parse>',
181+
].join("\n"))
182+
183+
messages = [
184+
'<1234>Apr 25 16:43:29 host app: message',
185+
'<1234> Apr 25 16:43:29 host app: message',
186+
'<ab> Apr 25 16:43:29 host app: message',
187+
]
188+
d.run(expect_emits: 3) do
189+
u = UDPSocket.new
190+
u.connect('127.0.0.1', @port)
191+
messages.each { |message| u.send(message, 0) }
192+
end
193+
194+
assert_equal(3, d.events.size)
195+
assert_equal(d.events[0], d.events[1])
196+
assert_equal('syslog.unmatched', d.events[2][0])
197+
assert_equal(messages[2], d.events[2][2]['unmatched_line'])
198+
end
199+
127200
def test_msg_size
128201
d = create_driver
129202
tests = create_test_case

0 commit comments

Comments
 (0)