Skip to content

pack: format JSON floats using shortest round-trip representation#12129

Open
vitaly-sinev wants to merge 2 commits into
fluent:masterfrom
vitaly-sinev:float-json-6447-core
Open

pack: format JSON floats using shortest round-trip representation#12129
vitaly-sinev wants to merge 2 commits into
fluent:masterfrom
vitaly-sinev:float-json-6447-core

Conversation

@vitaly-sinev

@vitaly-sinev vitaly-sinev commented Jul 21, 2026

Copy link
Copy Markdown

Fixes #6447 (the issue was auto-closed by the stale bot, but the bug is still present on master).

Problem

When serializing floats to JSON (Format json / flb_msgpack_to_json), values are formatted with %.16g, so 0.072 is emitted as 0.07199999999999999. The value 0.072 is not exactly representable as an IEEE-754 double, and 16 significant digits expose the trailing noise. Beyond being ugly, %.16g does not guarantee round-trip: a large fraction of doubles reparse to a different value.

input:    {"test": 0.072}
before:   [{"test":0.07199999999999999}]
after:    [{"test":0.072}]

Fix

Add flb_pack_double_to_str() in flb_pack.c, which uses the already-vendored yyjson dtoa (yyjson_write_number) to produce the shortest decimal string that round-trips back to the same double. Non-finite values (nan/inf) fall back to the previous %.17g behavior. It is applied to the core flb_msgpack_to_json float path here; the same helper will be routed through the other double→string sites (flb_json, flb_typecast, OpenTelemetry metrics, out_loki, processor_content_modifier, router_config) in separate follow-up PRs to keep this one focused and easy to review/revert.

This is also faster than the old snprintf("%.16g") on the hot path and removes a latent (long long)double cast (UB for out-of-range doubles).

Behavior notes

  • Whole numbers (5.0), -0.0, nan/inf are unchanged (yyjson emits a trailing .0 for integer-valued doubles, matching the old %.1f special case).
  • Very large/small magnitudes change scientific-notation syntax (e.g. 1e-050.00001, 1e+20100000000000000000000.0); all outputs remain valid JSON numbers.

Testing

  • New internal tests in tests/internal/pack.c: json_pack_float (end-to-end through flb_msgpack_to_json) and pack_double_to_str (direct helper coverage — round-trip over normal/subnormal/exponent values, nan/inf fallback, -0.0, undersized-buffer boundary). ./bin/flb-it-pack passes.
  • Debug log output from testing the change: out_stdout with Format json on {"test":0.072} now prints [{"test":0.072}].
  • Integration scenario scenarios/out_stdout (14 tests) passes, both normally and under valgrind --leak-check=full (--valgrind --valgrind-strict) — no leaks or memory errors.
  • Attached Valgrind output that shows no leaks or memory corruption was found

Documentation

  • Documentation required for this feature — a note in fluent/fluent-bit-docs describing the JSON float format change (before/after) will follow.

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.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a public utility to convert double values into shortest round-tripping string form with safe buffer/error handling.
  • Bug Fixes
    • Improved JSON number formatting for packed floating-point values, including correct -0.0 and consistent shortest-string output; non-finite values use legacy formatting and existing NaN-to-null behavior is preserved.
  • Tests
    • Added new floating-point precision and edge-case coverage for float-to-JSON output and direct double-to-string conversion (finite/non-finite, truncation, and invalid arguments).

@coderabbitai

coderabbitai Bot commented Jul 21, 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

Run ID: 16475da4-dee4-421e-af38-708319472274

📥 Commits

Reviewing files that changed from the base of the PR and between 5a99f51 and 4f33276.

📒 Files selected for processing (3)
  • include/fluent-bit/flb_pack.h
  • src/flb_pack.c
  • tests/internal/pack.c
🚧 Files skipped from review as they are similar to previous changes (3)
  • include/fluent-bit/flb_pack.h
  • tests/internal/pack.c
  • src/flb_pack.c

📝 Walkthrough

Walkthrough

Adds a public helper for shortest round-tripping double formatting, applies it to float32 and float64 JSON serialization, preserves NaN-to-null conversion, and adds tests for formatting, fallback, truncation, invalid arguments, and JSON output.

Changes

Floating-point serialization

Layer / File(s) Summary
Double formatting contract and helper
include/fluent-bit/flb_pack.h, src/flb_pack.c
Adds flb_pack_double_to_str() with buffer validation, yyjson shortest formatting for finite values, and legacy fallback formatting.
JSON float serialization
src/fluent-bit/flb_pack.c
Uses the helper for Msgpack float32 and float64 conversion while retaining NaN-to-null handling.
Floating-point serialization tests
tests/internal/pack.c
Tests shortest representations, round-tripping, non-finite values, truncation, invalid arguments, and JSON output registration.

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

Suggested reviewers: cosmo0920

🚥 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 matches the main change: formatting JSON floats with the shortest round-trip representation.
Linked Issues check ✅ Passed The PR addresses #6447 by fixing JSON float serialization to preserve values like 0.072 in stdout and HTTP output.
Out of Scope Changes check ✅ Passed The added helper and tests are directly tied to the float-formatting fix and do not introduce unrelated changes.
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.

@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: 1

🤖 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 `@tests/internal/pack.c`:
- Around line 989-1035: Add failure-path assertions to test_pack_double_to_str
for both a NULL output buffer and a zero buf_size, verifying each call returns
-1 without dereferencing or writing the buffer. Keep the existing valid-buffer
and undersized nonzero-buffer coverage unchanged.
🪄 Autofix (Beta)

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

Run ID: e5cfd107-cc5e-4358-8554-c1aa61ce49d9

📥 Commits

Reviewing files that changed from the base of the PR and between 8a4c159 and 0d49ce3.

📒 Files selected for processing (3)
  • include/fluent-bit/flb_pack.h
  • src/flb_pack.c
  • tests/internal/pack.c

Comment thread tests/internal/pack.c

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0d49ce3ea7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/flb_pack.c Outdated

int flb_pack_double_to_str(double val, char *out_buf, size_t buf_size)
{
yyjson_val num;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep FLB_YYJSON=Off builds compiling

When Fluent Bit is configured with -DFLB_YYJSON=Off, CMake does not add the yyjson include directory or define FLB_HAVE_YYJSON, but this helper still declares yyjson_val and calls yyjson APIs unconditionally. In that supported build configuration, src/flb_pack.c now fails to compile with unknown type name 'yyjson_val'; please guard this path or provide the legacy snprintf fallback when yyjson is disabled.

Useful? React with 👍 / 👎.

Comment thread src/flb_pack.c
@vitaly-sinev

Copy link
Copy Markdown
Author

Thanks for the automated reviews — addressed all three findings and force-pushed:

  • Uninitialized yyjson_val (Codex P1): yyjson_set_real() inspects the tag via unsafe_yyjson_is_ctn() before overwriting, so the transient value is now zero-initialized (yyjson_val num = { 0 };).
  • -DFLB_YYJSON=Off build (Codex P1): the yyjson path is now guarded by #ifdef FLB_HAVE_YYJSON; when the yyjson backend is disabled the helper falls back to %.17g (still round-trips). Verified compiling and passing flb-it-pack with both FLB_YYJSON=On and FLB_YYJSON=Off.
  • Missing failure-path coverage (CodeRabbit): test_pack_double_to_str now also asserts the NULL output buffer and buf_size == 0 contract returns -1. The shortest-form assertions are guarded with FLB_HAVE_YYJSON; round-trip / non-finite / truncation checks run in both build modes.

@vitaly-sinev

Copy link
Copy Markdown
Author

@coderabbitai review

@vitaly-sinev

Copy link
Copy Markdown
Author

@codex review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d04dd0a404

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/flb_pack.c Outdated
* Non-finite (nan/inf), unexpected yyjson failure, or a build without the
* yyjson backend: fall back to legacy formatting.
*/
len = snprintf(out_buf, buf_size, "%.17g", val);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve float formatting when yyjson is disabled

In builds configured with -DFLB_YYJSON=Off, all finite doubles now reach this fallback. That is not the legacy path removed above: integer-valued floats like 5.0 and -0.0 serialize as 5 and -0, and the #6447 case still serializes with 17-digit noise instead of the advertised shortest token. Please preserve the old integer/float formatting or provide a real dtoa fallback when yyjson is unavailable.

Useful? React with 👍 / 👎.

@vitaly-sinev
vitaly-sinev force-pushed the float-json-6447-core branch from d04dd0a to 5a99f51 Compare July 21, 2026 22:08
@vitaly-sinev

Copy link
Copy Markdown
Author

Addressed the P2 (yyjson-disabled formatting) and force-pushed.

The fallback now preserves the trailing .0 for integer-valued doubles instead of dropping to %.17g for everything, so with -DFLB_YYJSON=Off integer-valued floats (5.0, -0.0) keep their float marker as before. floor()/fabs() are used for the integer-valued test to avoid the undefined behavior of casting an out-of-range double to long long. Non-integer fractions still take %.17g in that build (a true shortest dtoa isn't available without yyjson, and that matches the prior %.16g-era behavior of that build).

Added a #ifndef FLB_HAVE_YYJSON test asserting 5.0/-0.0 keep the .0. Verified flb-it-pack builds and passes with both FLB_YYJSON=On and FLB_YYJSON=Off.

@vitaly-sinev

Copy link
Copy Markdown
Author

@codex review

@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: 1

🧹 Nitpick comments (1)
src/flb_pack.c (1)

988-1030: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Apply the required C layout to the changed helper.

The changed if/else blocks use same-line opening braces, and tmp, end, and num are declared inside a conditional block rather than at function start.

As per coding guidelines, “Put function opening braces on the next line” and “Declare variables at the start of functions rather than mid-block.”

🤖 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/flb_pack.c` around lines 988 - 1030, Update the changed helper’s
formatting to follow the required C layout: place the function’s opening brace
on the next line and move the tmp, end, and num declarations to the function
start, preserving their existing types and initialization. Reformat the affected
if/else blocks so opening braces are on the next line without changing behavior.

Source: Coding guidelines

🤖 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/flb_pack.c`:
- Around line 1084-1087: Update the double-formatting failure branch in the
map-value serialization path around flb_pack_double_to_str and try_to_write:
when the helper returns a negative result, propagate failure through
msg2json_end instead of resetting i to zero. Preserve normal writing for
successful conversions and ensure no empty JSON value is emitted.

---

Nitpick comments:
In `@src/flb_pack.c`:
- Around line 988-1030: Update the changed helper’s formatting to follow the
required C layout: place the function’s opening brace on the next line and move
the tmp, end, and num declarations to the function start, preserving their
existing types and initialization. Reformat the affected if/else blocks so
opening braces are on the next line without changing behavior.
🪄 Autofix (Beta)

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

Run ID: 2907bdca-c479-4b9b-859d-8d768ba77e5b

📥 Commits

Reviewing files that changed from the base of the PR and between d04dd0a and 5a99f51.

📒 Files selected for processing (3)
  • include/fluent-bit/flb_pack.h
  • src/flb_pack.c
  • tests/internal/pack.c
🚧 Files skipped from review as they are similar to previous changes (2)
  • include/fluent-bit/flb_pack.h
  • tests/internal/pack.c

Comment thread src/flb_pack.c

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a99f511d2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/flb_pack.c
}
else {
i = snprintf(temp, sizeof(temp)-1, "%.16g", o->via.f64);
i = flb_pack_double_to_str(o->via.f64, temp, sizeof(temp));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve bounded JSON sizes for large floats

When flb_msgpack_to_json() is used by callers with fixed buffers, this new formatter can expand each 9-byte MessagePack double such as 1e20 into 100000000000000000000.0 (23 bytes) instead of the old compact exponent form. I checked existing direct callers: the S3 and Chronicle log_key extraction paths allocate only about the input MessagePack size (bytes + bytes / 4) and only treat ret < 0 as failure, so an array/object log key containing several such floats can make flb_msgpack_to_json() return 0 for insufficient space and then be emitted as an empty/truncated value. Please either keep exponent notation for these magnitudes or update the fixed-buffer callers to grow/retry and handle ret == 0 before switching this core path.

Useful? React with 👍 / 👎.

vitaly-sinev and others added 2 commits July 22, 2026 01:17
Serialize double values with the shortest decimal string that parses
back to the same value using yyjson's dtoa, so e.g. 0.072 is emitted as
"0.072" instead of "0.07199999999999999" while preserving round-trip
precision.

Add flb_pack_double_to_str() and use it in flb_msgpack_to_json(). The
integer-valued special case is dropped because the helper already
renders whole numbers with a trailing ".0". Non-finite values (nan,
inf) keep the previous formatting.

Fixes fluent#6447

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: v.sinev <vit.phantom@gmail.com>
Verify that doubles are serialized to JSON using their shortest
representation, e.g. 0.072 is emitted as "0.072" and not
"0.07199999999999999", covering the regression from fluent#6447.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: v.sinev <vit.phantom@gmail.com>
@vitaly-sinev
vitaly-sinev force-pushed the float-json-6447-core branch from 5a99f51 to 4f33276 Compare July 21, 2026 22:27
@vitaly-sinev

Copy link
Copy Markdown
Author

Good catch — on a formatting failure the FLOAT case now goto msg2json_end (returns failure) instead of writing an empty token, so a malformed {"key":} can no longer be emitted while reporting success. Pushed.

@vitaly-sinev

Copy link
Copy Markdown
Author

Thanks — I looked into this. A few points:

  • The ret == 0-on-truncation handling is pre-existing and orthogonal to this change. I audited every fixed-buffer caller of flb_msgpack_to_json(): the internal _str/_sds wrappers and the AWS aggregation / CloudWatch / Kinesis paths all test ret <= 0 and grow-or-flush, so they already handle truncation correctly. Only out_s3 and out_chronicle use ret < 0 and would miss the 0 case — and that predates this PR.
  • The worst-case token width here (~24 bytes for a double) is comparable to the previous %.16g (~24) and %.1f (up to ~21) paths, so this doesn't materially change the overflow surface for those callers; it only widens output in the ~1e16–1e21 range.

The clean fix is at the API boundary rather than in this float PR: make flb_msgpack_to_json() return -1 (not 0) on truncation so the existing ret < 0 checks in out_s3/out_chronicle detect it, fixing all fixed-buffer callers at once. I'll send that as a separate focused pack: PR to keep this one scoped to the float formatting. Keeping this PR as-is.

@vitaly-sinev

Copy link
Copy Markdown
Author

Opened #12133 for the truncation-signalling fix discussed above (flb_msgpack_to_json() returning -1 instead of 0 on buffer overflow, so out_s3/out_chronicle detect it). Keeping this PR scoped to the float formatting.

@vitaly-sinev

Copy link
Copy Markdown
Author

Gentle ping 🙂 — this is the primary fix for #6447 (JSON floats like 0.072 currently serialize as 0.07199999999999999 via Format json). It's self-contained (flb_pack.c + a regression test), DCO and CodeRabbit are green, and it's verified locally with both FLB_YYJSON=On and Off plus the out_stdout integration scenario under valgrind.

Would a maintainer be able to take a look / set ok-to-test when you get a chance? Happy to adjust anything. Thanks!

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.

Float numbers in JSON stdout and http output plugin misformatted

1 participant