Skip to content

in_tail: drop multiline records whose reassembled content is empty (fixes #6703) - #12262

Open
allxm4 wants to merge 3 commits into
fluent:masterfrom
allxm4:fix/6703-drop-empty-multiline-content
Open

in_tail: drop multiline records whose reassembled content is empty (fixes #6703)#12262
allxm4 wants to merge 3 commits into
fluent:masterfrom
allxm4:fix/6703-drop-empty-multiline-content

Conversation

@allxm4

@allxm4 allxm4 commented Aug 11, 2026

Copy link
Copy Markdown

Fixes #6703.

Skip_Empty_Lines (tail_file.c) only inspects the raw line as read from
disk, before multiline reassembly. A CRI-framed empty-payload line
(<ts> stdout F with nothing after it) still has a non-zero raw length
(timestamp/stream/tag prefix), so it never trips that check, and the
reassembled record is emitted with an empty content field regardless.

This adds an opt-in drop_empty_content field to struct flb_ml_parser_ins,
wired from in_tail's existing Skip_Empty_Lines option (default off, no
behavior change unless already set), checked in flb_ml_flush_stream_group()
at the point that has visibility into the final reassembled content.

Verified with a real CRI-format log file on disk. Before: emits a record
with "log":""; after: dropped, surrounding valid lines untouched (no
truncation or merge). Full existing internal multiline test suite passes
(gcc and clang), plus 3 new tests covering the fix, backward compatibility
(option off = unchanged behavior), and an all-empty group. Valgrind clean.

Known limitation: only verified for the CRI parser (the one reported in
#6703). The built-in docker/JSON parser has its own, separate
skip_empty=TRUE behavior at the JSON-decode level that drops the key before
this code runs; not addressed here, and not the same failure mode.


Enter [N/A] in the box, if an item is not applicable to your change.

Testing
Before we can approve your change; please submit the following in a comment:

  • Example configuration file for the change
  • Debug log output from testing the change
  • Attached Valgrind output that shows no leaks or memory corruption was found

If this is a change to packaging of containers or native binaries then please confirm it works for all targets.

  • [N/A] Run local packaging test showing all targets (including any new ones) build.
  • [N/A] Set ok-package-test label to test for all targets (requires maintainer to do).

Documentation

  • Documentation required for this feature

Backporting

  • Backport to latest stable release.

Fluent Bit is licensed under Apache 2.0, by submitting this pull request I understand that this code will be released under the terms of that license.

Summary by CodeRabbit

  • New Features

    • Added support for filtering out multiline records with empty content.
    • Non-empty records continue to be processed normally.
    • Streams containing only empty payloads now produce no records when filtering is enabled.
  • Bug Fixes

    • Improved multiline flushing behavior for empty payloads.
    • Preserved existing behavior when empty-content filtering is disabled.

allxm4 added 2 commits August 11, 2026 20:56
Add a drop_empty_content flag to struct flb_ml_parser_ins. When set,
flb_ml_flush_stream_group() drops a flush whose key_content value is
an empty string instead of re-emitting the stored first-line map.

Defaults to FLB_FALSE (flb_calloc zero-inits the struct), so existing
behavior is unchanged unless a caller opts an instance in.

Signed-off-by: allxm4 <77125344+allxm4@users.noreply.github.com>
Skip_Empty_Lines only inspects the raw line as read from disk, before
multiline reassembly runs. A CRI-framed line with an empty payload
still has a non-zero raw length (timestamp/stream/tag prefix), so it
never trips that check, and the reassembled record is emitted with an
empty content field regardless.

Set drop_empty_content on each multiline parser instance from the
existing Skip_Empty_Lines option so the same user-facing intent also
covers post-reassembly content.

Signed-off-by: allxm4 <77125344+allxm4@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fbeaa20f-6893-48e7-a2c9-b1e8812b023b

📥 Commits

Reviewing files that changed from the base of the PR and between 6a82496 and daa157c.

📒 Files selected for processing (1)
  • tests/internal/multiline.c
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/internal/multiline.c

📝 Walkthrough

Walkthrough

The multiline parser now tracks the tail input’s skip_empty_lines setting. During flushes, it can omit records whose configured content field is an empty string. CRI regression tests cover enabled, disabled, and all-empty input behavior.

Changes

Empty multiline content handling

Layer / File(s) Summary
Configure and filter empty content
include/fluent-bit/multiline/flb_ml.h, plugins/in_tail/tail_config.c, src/multiline/flb_ml.c
The parser instance stores drop_empty_content. Tail configuration initializes it from skip_empty_lines. Multiline flushing skips original maps with empty configured content when enabled.
Validate CRI empty-content behavior
tests/internal/multiline.c
Fixtures and regression tests verify that empty payloads are dropped when enabled, retained when disabled, and produce no records when all payloads are empty.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: cosmo0920, edsiper

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: dropping empty multiline records in the tail input, and it references the fixed issue.
Linked Issues check ✅ Passed The changes satisfy issue #6703 by dropping empty reassembled CRI records when Skip_Empty_Lines is enabled while preserving default behavior.
Out of Scope Changes check ✅ Passed The implementation and regression tests are directly related to issue #6703 and the stated pull request objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@allxm4

allxm4 commented Aug 11, 2026

Copy link
Copy Markdown
Author

Example configuration (as used for all runs below; the fix activates via the existing Skip_Empty_Lines option, no new config surface):

[SERVICE]
    Flush 1
    Log_Level debug

[INPUT]
    Name              tail
    Path              /data/test.log
    Tag               test
    multiline.parser  docker, cri
    Skip_Empty_Lines  true
    Read_From_Head    true

[OUTPUT]
    Name    stdout
    Match   test
    Format  json_lines

Input file (/data/test.log, real CRI format, middle line has an empty payload):

2025-01-01T00:00:00.000000000Z stdout F hello world
2025-01-01T00:00:00.000000001Z stdout F 
2025-01-01T00:00:00.000000002Z stdout F goodbye world

Before/after (same patched binary; Skip_Empty_Lines false reproduces upstream behavior since the new field defaults off):

Skip_Empty_Lines false: the empty-payload line is emitted as a record with "log":"" (the bug):

{"date":1735689600.0,"time":"2025-01-01T00:00:00.000000000Z","stream":"stdout","_p":"F","log":"hello world"}
{"date":1735689600.0,"time":"2025-01-01T00:00:00.000000001Z","stream":"stdout","_p":"F","log":""}
{"date":1735689600.0,"time":"2025-01-01T00:00:00.000000002Z","stream":"stdout","_p":"F","log":"goodbye world"}

Skip_Empty_Lines true: empty record dropped, surrounding lines untouched:

{"date":1735689600.0,"time":"2025-01-01T00:00:00.000000000Z","stream":"stdout","_p":"F","log":"hello world"}
{"date":1735689600.0,"time":"2025-01-01T00:00:00.000000002Z","stream":"stdout","_p":"F","log":"goodbye world"}

Debug log output (Ubuntu 24.04, gcc, -DFLB_TESTS_INTERNAL=On -DFLB_DEV=On):

[2026/08/11 18:06:57.393] [ info] [fluent bit] version=5.1.0, commit=6a824964b2, pid=8
[2026/08/11 18:06:57.393] [ info] [input:tail:tail.0] multiline core started
[2026/08/11 18:06:57.393] [debug] [input:tail:tail.0] flb_tail_fs_inotify_init() initializing inotify tail input
[2026/08/11 18:06:57.393] [debug] [input:tail:tail.0] scanning path /data/test.log
[2026/08/11 18:06:57.393] [debug] [input:tail:tail.0] inode=22946704 with offset=0 appended as /data/test.log
[2026/08/11 18:06:57.393] [debug] [input:tail:tail.0] 1 new files found on path '/data/test.log'
[2026/08/11 18:06:57.395] [debug] [input:tail:tail.0] inode=22946704 file=/data/test.log promote to TAIL_EVENT
[2026/08/11 18:06:57.395] [ info] [input:tail:tail.0] inotify_fs_add(): inode=22946704 watch_fd=1 name=/data/test.log
{"date":1735689600.0,"time":"2025-01-01T00:00:00.000000000Z","stream":"stdout","_p":"F","log":"hello world"}
{"date":1735689600.0,"time":"2025-01-01T00:00:00.000000002Z","stream":"stdout","_p":"F","log":"goodbye world"}

Valgrind (valgrind --leak-check=full --show-leak-kinds=all against the real daemon processing the scenario above, clean SIGTERM shutdown):

==15== HEAP SUMMARY:
==15==     in use at exit: 0 bytes in 0 blocks
==15==   total heap usage: 5,232 allocs, 5,232 frees, 2,384,523 bytes allocated
==15==
==15== All heap blocks were freed -- no leaks are possible
==15==
==15== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Unit tests: full internal multiline suite (flb-it-multiline, 25 tests) passes on both gcc 13 and clang 18 (Ubuntu 24.04), including 3 new tests: the fix, backward compatibility (option off = unchanged behavior), and an all-empty group (drops everything, no crash).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/multiline/flb_ml.c`:
- Around line 1687-1689: Apply the project’s C brace style by moving each
control-block opening brace onto its own following line: the if block in
src/multiline/flb_ml.c (1687-1689), and the for/if blocks in
tests/internal/multiline.c (650-655, 659-661, 697-702, 706-708, 744-749, and
753-755). Preserve all existing control-flow behavior.

In `@tests/internal/multiline.c`:
- Around line 85-114: Update the initializer entries in the new issue_6703
fixtures to use four-space indentation instead of two spaces, including
issue_6703_input, issue_6703_output, issue_6703_disabled_output,
issue_6703_all_empty_input, and issue_6703_all_empty_output.
- Around line 649-655: In tests/internal/multiline.c at lines 649-655, 696-702,
and 743-749, update the append loops in test_issue_6703 and
test_issue_6703_all_empty to assign each flb_ml_append_text() result to ret and
assert ret >= 0, covering both success and failure paths.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 788433fc-f547-4a84-9f9a-cbb2673c7acd

📥 Commits

Reviewing files that changed from the base of the PR and between ae51533 and 6a82496.

📒 Files selected for processing (4)
  • include/fluent-bit/multiline/flb_ml.h
  • plugins/in_tail/tail_config.c
  • src/multiline/flb_ml.c
  • tests/internal/multiline.c

Comment thread src/multiline/flb_ml.c
Comment on lines +1687 to +1689
if (parser_i->drop_empty_content) {
key_id = get_key_id(&map, parser_i->key_content);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the required control-block brace style.

Move each control-block opening brace to the next line.

  • src/multiline/flb_ml.c#L1687-L1689: move the if opening brace to the next line.
  • tests/internal/multiline.c#L650-L655: move the for opening brace to the next line.
  • tests/internal/multiline.c#L659-L661: move the if opening brace to the next line.
  • tests/internal/multiline.c#L697-L702: move the for opening brace to the next line.
  • tests/internal/multiline.c#L706-L708: move the if opening brace to the next line.
  • tests/internal/multiline.c#L744-L749: move the for opening brace to the next line.
  • tests/internal/multiline.c#L753-L755: move the if opening brace to the next line.

As per coding guidelines, C control blocks must place the opening brace on the next line.

📍 Affects 2 files
  • src/multiline/flb_ml.c#L1687-L1689 (this comment)
  • tests/internal/multiline.c#L650-L655
  • tests/internal/multiline.c#L659-L661
  • tests/internal/multiline.c#L697-L702
  • tests/internal/multiline.c#L706-L708
  • tests/internal/multiline.c#L744-L749
  • tests/internal/multiline.c#L753-L755
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/multiline/flb_ml.c` around lines 1687 - 1689, Apply the project’s C brace
style by moving each control-block opening brace onto its own following line:
the if block in src/multiline/flb_ml.c (1687-1689), and the for/if blocks in
tests/internal/multiline.c (650-655, 659-661, 697-702, 706-708, 744-749, and
753-755). Preserve all existing control-flow behavior.

Source: Coding guidelines

Comment on lines +85 to +114
/* CRI, empty payload dropped via drop_empty_content */
struct record_check issue_6703_input[] = {
{"2025-01-01T00:00:00.000000000Z stdout F hello world"},
{"2025-01-01T00:00:00.000000001Z stdout F "},
{"2025-01-01T00:00:00.000000002Z stdout F goodbye world"}
};

struct record_check issue_6703_output[] = {
{"hello world"},
{"goodbye world"}
};

/* Same input, drop_empty_content left off (default): behavior is unchanged */
struct record_check issue_6703_disabled_output[] = {
{"hello world"},
{""},
{"goodbye world"}
};

/* CRI, every line in isolation is an empty payload */
struct record_check issue_6703_all_empty_input[] = {
{"2025-01-01T00:00:00.000000000Z stdout F "},
{"2025-01-01T00:00:00.000000001Z stdout F "},
{"2025-01-01T00:00:00.000000002Z stdout F "}
};

/* Sink for issue_6703_all_empty: only read if the fix regresses */
struct record_check issue_6703_all_empty_output[] = {
{""}, {""}, {""}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use four-space indentation in the new fixtures.

Lines 87-113 use two-space indentation for initializer entries. Use four spaces.

As per coding guidelines, **/*.{c,h,cc,cpp,cxx} requires four-space indentation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/internal/multiline.c` around lines 85 - 114, Update the initializer
entries in the new issue_6703 fixtures to use four-space indentation instead of
two spaces, including issue_6703_input, issue_6703_output,
issue_6703_disabled_output, issue_6703_all_empty_input, and
issue_6703_all_empty_output.

Source: Coding guidelines

Comment on lines +649 to +655
entries = sizeof(issue_6703_input) / sizeof(struct record_check);
for (i = 0; i < entries; i++) {
r = &issue_6703_input[i];
len = strlen(r->buf);
flb_time_get(&tm);
flb_ml_append_text(ml, stream_id, &tm, r->buf, len);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Check every flb_ml_append_text() result.

The tests discard the append result. In test_issue_6703 and test_issue_6703_all_empty, failure to process empty input can still satisfy the expected flush count. Assign the result to ret and check ret >= 0 in each loop.

  • tests/internal/multiline.c#L649-L655: check each append result in the enabled test.
  • tests/internal/multiline.c#L696-L702: check each append result in the disabled test.
  • tests/internal/multiline.c#L743-L749: check each append result in the all-empty test.

As per coding guidelines, validate both success and failure paths.

📍 Affects 1 file
  • tests/internal/multiline.c#L649-L655 (this comment)
  • tests/internal/multiline.c#L696-L702
  • tests/internal/multiline.c#L743-L749
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/internal/multiline.c` around lines 649 - 655, In
tests/internal/multiline.c at lines 649-655, 696-702, and 743-749, update the
append loops in test_issue_6703 and test_issue_6703_all_empty to assign each
flb_ml_append_text() result to ret and assert ret >= 0, covering both success
and failure paths.

Source: Coding guidelines

Add three cases: a standalone CRI line with an empty payload is
dropped when drop_empty_content is set, is still emitted when the
option is not set (default, backward-compat), and a group where
every line is empty does not crash or flush anything.

Signed-off-by: allxm4 <77125344+allxm4@users.noreply.github.com>
@allxm4
allxm4 force-pushed the fix/6703-drop-empty-multiline-content branch from 6a82496 to daa157c Compare August 11, 2026 18:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CRI parser outputs empty log lines

1 participant