[EXPORTER] Fix Elasticsearch log exporter aborting on invalid UTF-8 - #4501
Merged
marcalff merged 3 commits intoSep 1, 2026
Merged
Conversation
A log record's body or attributes may carry bytes that are not valid UTF-8 (e.g. a truncated multibyte sequence, a payload read in another encoding, or a string built from a file path). ElasticSearchRecordable::WriteValue stores the value as given, without validating or transcoding it. Export() then called nlohmann::json::dump() with its default strict error handler, which throws on invalid UTF-8; since Export() is noexcept, that throw became std::terminate() and aborted the whole process, not just the one export. Switch to error_handler_t::replace, which substitutes U+FFFD for the invalid bytes instead of throwing, so the record (and the rest of the batch) is still exported. Fixes open-telemetry#4439
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4501 +/- ##
==========================================
+ Coverage 83.25% 83.47% +0.23%
==========================================
Files 521 521
Lines 20379 20380 +1
==========================================
+ Hits 16964 17011 +47
+ Misses 3415 3369 -46
🚀 New features to boost your workflow:
|
…ctor> The fake Response's ForEachHeader overrides use nostd::function_ref directly; it was only reaching the test via a transitive include. <memory> and <vector> are only used transitively too and iwyu wants them dropped.
marcalff
reviewed
Sep 1, 2026
Comment on lines
+130
to
+134
| // Regression test: a log record whose body carries bytes that are not valid UTF-8 used to | ||
| // abort the process. ElasticSearchRecordable::WriteValue stores the value as given, and | ||
| // Export() previously called nlohmann::json::dump() with its default strict error handler, | ||
| // which throws on invalid UTF-8; since Export() is noexcept, that throw became | ||
| // std::terminate(). The exporter now tolerates it instead of crashing. |
Member
There was a problem hiding this comment.
No changes needed, but a nit comment for future PRs:
Comments in the CODE should be about what the code does.
Comments about what the code did before a change belongs to the PR or issue description, not in the code itself.
That said, thanks for the tests.
marcalff
approved these changes
Sep 1, 2026
marcalff
left a comment
Member
There was a problem hiding this comment.
LGTM, thanks for the fix and tests.
thc1006
added a commit
to thc1006/opentelemetry-cpp
that referenced
this pull request
Sep 8, 2026
…ccepts Main grew its own FakeResponse, FakeRequest, FakeSession and FakeHttpClient in open-telemetry#4071 and open-telemetry#4501, in an unnamed namespace, and this branch already had four of the same names in another unnamed namespace in the same file. Reopening an unnamed namespace names the same namespace, so the rebase merged both without a conflict into four redefinitions that do not compile. Merged rather than renamed. Main's set is used once and answers with one fixed response; this branch's runs a script the case supplies, which is the general case. So the session and the client take a script and default to answering the way main's did, and the existing call site is untouched. The default body had to change too, which is not cosmetic. It read `{"errors": false, "failed" : 0}`, and this pull request stops reading `failed` and starts requiring one `items` result per submitted record, so that body is now rejected: with it, ExportingARecordWithInvalidUtf8DoesNotAbort returns kFailure and fails. The default is now a body a server could send for a one record batch, and the comment that described the old substring search is rewritten. Verified in both configurations, since the guards cut different code: 26 tests, 22 passed and 4 skipped without async export, 20 passed and 6 skipped with it, no failures in either. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
thc1006
added a commit
to thc1006/opentelemetry-cpp
that referenced
this pull request
Sep 8, 2026
Main's open-telemetry#4071 and open-telemetry#4501 put FakeResponse, FakeRequest, FakeSession and FakeHttpClient in an unnamed namespace at the top of this file, and this branch already had four of those names in a second unnamed namespace lower down. Reopening an unnamed namespace names the same namespace, so the rebase merged both with no conflict at all and left four redefinitions. One set now. The session and the client take a script and default to answering the way main's did, so main's own call site needs no edit, and DeferredSession and DeferredHttpClient are untouched. The default body stays as main wrote it. This branch does not change how the exporter decides success, so what main's cases send still passes here. Verified in both configurations with maintainer mode on: 19 cases, all passing without async export, 3 passing and 16 skipping with it. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
thc1006
added a commit
to thc1006/opentelemetry-cpp
that referenced
this pull request
Sep 8, 2026
Main's open-telemetry#4071 and open-telemetry#4501 put FakeResponse, FakeRequest, FakeSession and FakeHttpClient in an unnamed namespace at the top of this file, and this branch already had four of those names in a second unnamed namespace lower down. Reopening an unnamed namespace names the same namespace, so the rebase merged both with no conflict at all and left four redefinitions. One set now. The session and the client take a script and default to answering the way main's did, so main's own call site needs no edit. The script carries the handler as a shared_ptr rather than a reference, because the cases here have to keep it and send a second event to it, and the client keeps its on_create_session and on_cancel_all hooks, both empty by default. The default body stays as main wrote it. This branch does not change how the exporter decides success, so what main's cases send still passes here. Verified in both configurations with maintainer mode on: 12 cases, 12 passing with async export and 3 passing with 9 skipping without it. Removing CompleteOnce's compare and exchange turns six of them red, so the rewritten fixtures still discriminate. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4439
Summary
A log record's body or attributes may carry bytes that are not valid UTF-8 (a truncated multibyte sequence, a payload read in another encoding, a message built from a file path, etc).
ElasticSearchRecordable::WriteValuestores the value as given, without validating or transcoding it.Export()then callednlohmann::json::dump()with its default strict error handler, which throwstype_error.316on invalid UTF-8. SinceExport()isnoexcept, that throw becomesstd::terminate(), aborting the whole process rather than just failing the one export.Fix
Switch to
error_handler_t::replace, which substitutes U+FFFD for the invalid bytes instead of throwing. The record (and the rest of the bulk batch) is still exported, with just the invalid bytes replaced.Test plan
ElasticsearchLogsExporterTests.ExportingARecordWithInvalidUtf8DoesNotAbort, using a fake injectableHttpClient/Session/Request/Response(the exporter already supports client injection) so the test doesn't need a real Elasticsearch instance.std::terminate/abort from the issue.es_log_record_exporter_testsuite passes (3 tests, 3 pre-existing disabled tests unaffected).