[BUG] Decide Elasticsearch bulk export success from HTTP status and the errors flag - #4297
Open
thc1006 wants to merge 31 commits into
Open
[BUG] Decide Elasticsearch bulk export success from HTTP status and the errors flag#4297thc1006 wants to merge 31 commits into
thc1006 wants to merge 31 commits into
Conversation
thc1006
added a commit
to thc1006/opentelemetry-cpp
that referenced
this pull request
Jul 24, 2026
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4297 +/- ##
==========================================
- Coverage 81.29% 81.17% -0.12%
==========================================
Files 447 451 +4
Lines 19028 19250 +222
==========================================
+ Hits 15467 15624 +157
- Misses 3561 3626 +65
🚀 New features to boost your workflow:
|
thc1006
added a commit
to thc1006/opentelemetry-cpp
that referenced
this pull request
Jul 25, 2026
Review of open-telemetry#4297 found the synchronous path reported a non-2xx response as a success: ResponseHandler::OnResponse only logged the status and unconditionally set response_received_, waitForResponse returned true, and Export then checked the body alone, so HTTP 500 with a body of {"errors":false} became kSuccess. My description claiming the sync path already checked the status was wrong: it observed the status but never let it affect the ExportResult. IsBulkResponseSuccessful now takes the status code and treats a non-2xx as a failure before looking at the body. The top-level "errors" flag is item-level and cannot override a transport or application error. Both paths call it: the async handler drops its duplicated status check, and the sync handler stores the status so Export can pass it in. Tests assert the invariant directly, including HTTP 500 with {"errors":false} on the sync-shaped path, plus the errors:true generic reason and the missing item-error branches. A full handler-level mock across HttpClient/Session is out of scope: the exporter has no such mock today (its network tests are DISABLED_), and the status invariant is what the bug was, so it is covered at the validator instead. Fixes open-telemetry#4295 Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
…string
Both export paths decided whether a bulk write succeeded with
body.find("\"failed\" : 0") == std::string::npos
which is wrong in both directions. "failed" is per-shard information for
a single item, so a batch where one item was rejected and another
reported "failed": 0 was recorded as a success and the rejected records
were silently lost. The literal also contains pretty-printing spaces, so
a compact response never matched and a completely successful export was
reported as a failure.
Both paths now parse the body and check the top level "errors" field,
which is what the bulk API defines as the outcome for the batch, and
report the first item error rather than only that something went wrong.
A malformed or unparseable body counts as a failure. The asynchronous
path also checks the HTTP status, which it previously ignored while the
synchronous path did not.
nlohmann/json.hpp was already included by this file and already linked
in both the CMake and Bazel targets, so this adds no dependency.
Fixes open-telemetry#4295
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
AsyncResponseHandler::OnResponse is noexcept, and the helper it now calls can throw: dump() and the string concatenation that builds the failure reason both allocate. clang-tidy reported bugprone-exception-escape on OnResponse, which was a warning this branch added rather than one that was already there. The helper is now noexcept and absorbs anything thrown while inspecting the body, which is the right answer anyway: a response that cannot be inspected is not a successful export. The default reason is set inside the try so the handler itself does no allocation. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
The Bazel noexcept configuration compiles with -fno-exceptions, where try is a hard error rather than a warning, so the previous commit broke that job. The repository already has an idiom for this, OPENTELEMETRY_HAVE_EXCEPTIONS, used the same way in attribute_utils.h. Nothing is lost when it is off: without exceptions an allocation failure terminates rather than unwinding, so there is nothing for the handler to catch in the first place. The macro arrives through opentelemetry/version.h, which includes macros.h with an IWYU pragma export, so no new include is needed. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Codecov reported 0% patch coverage: none of the 34 changed lines were exercised. The reason was structural rather than an oversight about which tests to write. The helper sat in an anonymous namespace inside the .cc, so no test translation unit could reach it, and the existing Elasticsearch tests are almost all DISABLED_ because they need a live server. It is a pure function from a response body to a verdict, so it belongs somewhere a test can call it. It moves to a detail header, following the detail/ pattern already used by ext/http/client/detail/default_factory.h, and is added to the Bazel hdrs so the test target picks it up. Four tests cover it: pretty-printed and compact successes, a batch with one rejected item where the reason must name the underlying error, an unusable body, and a missing or non-boolean errors field. The first two are the cases the old substring check got wrong in each direction. nlohmann/json.hpp is no longer used by the .cc and is removed from it. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
IWYU flagged es_log_record_exporter.cc for a missing nlohmann/json.hpp: it calls GetJSON().dump() at the point it builds the request body, which is a direct use of the type. When the bulk response helper moved to its own header I removed the include with it, but this use remained, and IWYU treats reaching it through es_log_recordable.h as an indirect dependency. The include goes back. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Review of open-telemetry#4297 found the synchronous path reported a non-2xx response as a success: ResponseHandler::OnResponse only logged the status and unconditionally set response_received_, waitForResponse returned true, and Export then checked the body alone, so HTTP 500 with a body of {"errors":false} became kSuccess. My description claiming the sync path already checked the status was wrong: it observed the status but never let it affect the ExportResult. IsBulkResponseSuccessful now takes the status code and treats a non-2xx as a failure before looking at the body. The top-level "errors" flag is item-level and cannot override a transport or application error. Both paths call it: the async handler drops its duplicated status check, and the sync handler stores the status so Export can pass it in. Tests assert the invariant directly, including HTTP 500 with {"errors":false} on the sync-shaped path, plus the errors:true generic reason and the missing item-error branches. A full handler-level mock across HttpClient/Session is out of scope: the exporter has no such mock today (its network tests are DISABLED_), and the status invariant is what the bug was, so it is covered at the validator instead. Fixes open-telemetry#4295 Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
An errors:false body was trusted even when it carried no item results, so
a truncated or unrelated response such as {"errors":false} or an empty
"items" array reported a dropped batch as a success. The check now
requires "items" to be an array whose size matches the number of records
exported: both the sync and async paths pass that count, and the async
handler stores it alongside the response.
Also move the non-2xx status check inside the exception guard so building
its failure reason cannot terminate a noexcept caller on allocation
failure, drop the ?pretty query now that the parser no longer depends on
whitespace, and log a synchronous non-2xx response once from Export()
rather than a second time with the full body from the response handler.
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Review found the added items.size() == records check to be half-baked schema validation beyond the scope of open-telemetry#4295: it checks array length but not that each entry is a real operation result, and it would reject a legitimate filter_path response. A 2xx response whose top-level "errors" is false is Elasticsearch's authoritative batch-success signal, so the condition returns to 2xx + JSON object + boolean errors. Also clear failure_reason on entry so a success cannot leave a stale reason behind, and document the exception path's reason as best-effort. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
thc1006
force-pushed
the
bugfix/elasticsearch-response-handling
branch
from
July 25, 2026 09:28
d751066 to
229ee19
Compare
thc1006
marked this pull request as ready for review
July 25, 2026 19:12
…response-handling # Conflicts: # CHANGELOG.md
6 tasks
Add two response-parser tests and soften one doc comment; no change to the validator logic. - TreatsThe2xxRangeAsTheSuccessBand: 199/200/299/300 pin the status boundary. - DoesNotRequireItemsForSuccess: a body with errors:false and no items (e.g. filter_path-filtered) is a success, locking out re-adding an items-count check. - Reword the header comment so it no longer claims every 2xx means every operation was applied; a successful 2xx with errors:false is treated as batch success.
…response-handling # Conflicts: # CHANGELOG.md
Resolves the overlap with open-telemetry#4298, which landed after this branch last synced. Both changes rewrite the same region of the synchronous ResponseHandler. open-telemetry#4298 owns transport completion: OnResponse records a completion state under the handler mutex and Export() waits on a predicate, first writer wins. This branch owns the application outcome: it keeps the HTTP status alongside the body so Export() can hand both to IsBulkResponseSuccessful(). The two layer rather than compete. recordCompletionLocked(Success) now means the transport delivered a response, not that the export succeeded. A failure event that fired first still keeps Failure, so Export() returns kFailure without consulting the body. response_received_ is gone, replaced by the completion state.
es_bulk_response.h includes nlohmann/json.hpp. The component's *.h glob would install it, which would make nlohmann a public dependency of the installed Elasticsearch headers for the first time. es_log_recordable.h is the existing precedent: it is the only other Elasticsearch header that includes nlohmann, and it is already excluded for the same reason.
thc1006
force-pushed
the
bugfix/elasticsearch-response-handling
branch
from
August 2, 2026 19:04
5298a83 to
189419b
Compare
The helper tests call IsBulkResponseSuccessful() directly, so they cannot show that the status and the body reach it. A handler that stored a fixed status, or an Export() that never asked for one, passes all of them. Three cases through the exporter with a fake HTTP client: an accepted bulk response, the rejected item from open-telemetry#4295 whose shard counter still reads "failed" : 0, and a 500 carrying a body the parser would otherwise accept. The fake client is the same one open-telemetry#4331 adds to this file. Whichever lands first, the other drops the duplicate when it rebases.
thc1006
force-pushed
the
bugfix/elasticsearch-response-handling
branch
from
August 2, 2026 19:13
189419b to
0283411
Compare
The file name pattern stops the header shipping but the directory is still traversed and created, so the package carried an empty include/opentelemetry/exporters/elasticsearch/detail.
The exporter posts an unfiltered /_bulk with one index operation per
record, and Elasticsearch answers those with one entry in items per
operation. The check accepted any 2xx body carrying errors:false, so
{"errors":false\} alone was reported as a successful write of every
record in the batch.
An exporter that reports success without a write acknowledgement gives
the caller no reason to retry, which is the more dangerous direction of
the two. A misconfigured proxy, a non-Elasticsearch endpoint answering
with coincidentally shaped JSON, or a server returning an incomplete
envelope all reached the same result.
An earlier revision of this branch dropped a count check as half baked
schema validation, on the grounds that a filter_path response need not
carry items. That reasoning does not apply to this exporter: it never
sends filter_path. Confirming the response answers as many operations as
were submitted is not the same as validating each item's contents.
thc1006
force-pushed
the
bugfix/elasticsearch-response-handling
branch
from
August 2, 2026 21:43
49cf38c to
223618d
Compare
RejectedItemIsAFailedExport used a body with two items while exporting one record, so after the acknowledgement count went in it was failing on the count rather than on the rejected item. It still passed, which is the worst way for a test to be wrong.
The helper takes std::size_t but reached the definition only through <nlohmann/json.hpp> or <string>. The CHANGELOG line named the substring match alone, which stopped being the whole story once the HTTP status and the acknowledgement count joined the decision.
GTEST_SKIP returns, so a skip at the top of each body left the rest unreachable, which MSVC reports as C4702 and the maintainer mode jobs turn into an error. A fixture that skips in SetUp keeps every case in the binary, which gtest_add_tests still needs, and the anonymous namespace keeps clang-tidy's misc-use-internal-linkage quiet.
The signature gained expected_items and the return contract gained the exact-count requirement, but the comment still described the two argument form.
… to run The coverage job configures all-options-abiv2-preview, which turns on ENABLE_ASYNC_EXPORT, so the wiring cases skip there and nothing calls Export() at all. That left six lines of this change unexecuted in the one configuration Codecov measures: the handler's submitted_operations_ member, the bulk URI, the handler construction, and the parse with its failure log. Export() returns before a response arrives on that path, so the parsed result decides only which internal log line is written. These two cases read it through a captured log handler, the way the batch span processor test does.
hdrs is a target's public interface, so listing the detail header there let a Bazel consumer include a header the CMake package deliberately does not install. It moves to its own target that only this package can depend on, reached through implementation_deps so the exporter does not re-export it, and the test depends on it directly. Checked with a consumer that depends only on the exporter target: the include now fails to resolve, and adding the internal target to that consumer makes it build again.
Its only assertion sat behind OTEL_INTERNAL_LOG_LEVEL, so a consumer that lowers the level would have compiled a case with nothing to check. Asserting the Export() result as well matches the accepted case and states the contract this path has: success is reported whatever the response said, which is why the outcome is read from the log.
The acknowledgement count made the array length evidence that the batch was
written, but nothing looked at what the entries were. A body such as
{"errors":false,"items":[null,null]} for a two record batch matched the
count and was reported as a successful write of both records.
The entries were already required to be objects, but only on the errors:true
path, where the walk that extracts the first item error skips anything else.
The same body with errors:true was a failure while errors:false was a
success, so a responder that sends neither shape correctly picked the verdict
with a flag it also controls. The requirement moves above the errors branch
so that it holds on both paths, and the skip inside the walk goes with it.
Elasticsearch answers each operation with an object keyed by the action name,
so no conforming response changes verdict. The check stops at the entry being
an object: an item status that disagrees with the top level errors flag is
the server contradicting its own summary, and re-deriving that flag from item
statuses would only duplicate it.
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
An "items" entry only had to be an object, which is where the walk that
extracts the first item error happened to guard rather than a line with a
reason behind it. {"errors":false,"items":[{},{}]} for a two record batch
matched the count, was an object per entry, and was reported as a successful
write of both records.
Elasticsearch's bulk schema answers each operation with an object keyed by
the action whose value is that operation's result, and makes "status" a
required member of the result. An entry with no action key, or one whose
action holds no status, acknowledges nothing. Only the presence of a status
is read: its value is the operation's own HTTP status, and whether any
operation failed is what the top-level "errors" flag already reports, so
re-deriving it here would duplicate that flag and put status policy in the
exporter.
The shared response fixtures gain the "status" and "_index" that a real bulk
response carries. They were abridged to the fields the old substring check
looked at.
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
The check trusted "errors" once the entries looked like operation results, so
{"errors":false,"items":[{"index":{"status":400}}]} was reported as a
successful write of a record Elasticsearch had rejected.
A false "errors" asserts that every operation was applied, and each
acknowledgement carries the operation's own status, so the response can be
checked against itself. A conforming server never sends that combination,
which is why this rejects nothing the flag alone would have accepted: it
catches only a response that contradicts itself.
2xx is the whole success band here. The exporter writes every record with an
index operation, which Elasticsearch answers with 200 or 201, so there is no
create returning 409 or delete returning 404 to weigh, and the band is the
one the response's own HTTP status already uses.
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
An entry only had to carry some action whose result held a status, so
{"errors":false,"items":[{"unknown":{"status":201}}]} and an entry answering
two operations at once both passed. Neither is evidence that the index
operations this exporter submitted were processed, and the count check they
satisfy is what makes them dangerous: a proxy or an Elastic compatible
endpoint answering a hundred records with a hundred filler entries reported a
successful write, the batch was dropped, and the records were gone.
Elasticsearch keys each entry by the action it answers, and the exporter
writes every record with an index action, so a conforming answer to this
request has exactly one member per entry and it is named "index". Requiring
that, rather than any action at all, is what ties the acknowledgement to the
operation that was sent.
The case is mutation checked: removing the "index" lookup while leaving the
member count and the status requirement in place turns it red, so it pins the
operation identity rather than the JSON shape around it.
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
The three ways an entry can fail to be an index acknowledgement reported one reason between them, so the log said an entry was wrong without saying which kind of wrong. A padded array, an answer to an action that was never submitted, and a result with no status now say so separately. With every entry checked to be one index result, the error walk no longer needs to look for the operation or guard against it not being an object, and the comments that restated all of this in prose are gone with it. The bulk action in Export() points at the parser, which requires the acknowledgement to name that same action. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Two comments described a check this branch removes, which is the kind of rationale @dbarker asked to keep in the pull request and the git history rather than in the code, on open-telemetry#4327. They now describe the bodies themselves: two successes that differ only in whitespace, and a rejected item whose shard counter still reads as a success to anything reading the body as text. 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 #4295
The bug
Both export paths decided whether a bulk write succeeded with
body.find("\"failed\" : 0").failedis per-shard information for one item, not the batch outcome (which is the top-levelerrorsflag), and the literal even baked in pretty-printing whitespace, so it was wrong in both directions: a batch with a rejected item read as success, and a compact successful response read as failure.The fix
A single
IsBulkResponseSuccessful(status_code, body, expected_items, reason)that both paths call. A non-2xx status is a failure first. Then the body has to be a JSON object with a booleanerrorsflag and anitemsarray holding one operation result per operation the request submitted, anderrorshas to be false. A malformed body counts as a failure, and the first item error is reported so the log says why.The count is there because the exporter posts an unfiltered
/_bulkwith exactly one index operation per record, and Elasticsearch answers those with one entry initemsper operation. Without it,{"errors":false}on its own was reported as a successful write of the whole batch. An exporter that claims success without a write acknowledgement gives the caller no reason to retry, which is the worse of the two directions to be wrong in, and a misconfigured proxy or a non-Elasticsearch endpoint answering with coincidentally shaped JSON both landed there.Matching the count is not enough on its own. Elasticsearch answers each operation with an object keyed by the action name, so
{"errors":false,"items":[null,null]}for a two record batch had the right length and was reported as a successful write of both records. That requirement already existed, but only on theerrors:truepath, where the walk that extracts the first item error skips anything that is not an object. The same body was therefore a failure witherrors:trueand a success witherrors:false, so a responder that sends neither shape correctly picked the verdict with a flag it also controls. The requirement now runs beforeerrorsis read, and the skip inside the walk goes with it.An acknowledgement is an entry with exactly one member, named
index, holding a result object with astatus. Elasticsearch keys each entry by the action it answers and the exporter writes every record with an index action, so that is what an answer to this request looks like, and the bulk schema makesstatusa required member of the result alongside_index.Nothing weaker ties the entry to the operation that was sent. Being an object was not the line it looked like:
{"errors":false,"items":[{},{}]}matched the count and was an object per entry. Neither is carrying some action or other:{"unknown":{"status":201}}answers something this exporter never submitted, and{"index":{"status":201},"delete":{}}answers one operation with two. The count check is what makes those dangerous rather than untidy. A hundred records answered with a hundred filler entries reads as a successful write, the processor drops the batch, and the records are gone.The status is then read against the flag rather than instead of it. A false
errorsasserts that every operation was applied, so{"errors":false,"items":[{"index":{"status":400}}]}is the response contradicting itself, and a response that contradicts itself is not evidence that anything was written. A conforming server never sends that combination, which is why this rejects nothing the flag alone would have accepted.2xx is the whole success band here, and that is a property of this exporter rather than a policy choice: every record is written with an
indexoperation, which Elasticsearch answers with 200 or 201. There is nocreatereturning 409 for a duplicate ordeletereturning 404 to weigh, so the band is the same one the response's own HTTP status uses, pinned at 200/299/300 by a case.The check stops there. It never asks what an operation's status means beyond the band, and it does not read
errorbodies undererrors:false, since the status already covers them.An earlier revision of this branch had the count check and dropped it, on the grounds that a
filter_pathresponse need not carryitems. That reasoning does not apply here: this exporter never sendsfilter_path. If it ever does, the request side should say so rather than the parser accepting two incompatible response shapes.The HTTP status matters and was the second-round finding: the synchronous path previously only logged a non-2xx status and still returned success, so
HTTP 500with{"errors":false}was reported askSuccess. The status is now part of the result on both paths; the async handler drops its duplicated check and the sync handler stores the status forExportto pass in.Structure
The helper lives in
include/opentelemetry/exporters/elasticsearch/detail/es_bulk_response.hso tests can reach it, following thedetail/pattern already used byext/http/client/detail/default_factory.h. It is adetailheader rather than an anonymous-namespace function in the.cc(an earlier revision put it there, which is why it could not be tested).It is excluded from the installed package, both the header and the
detaildirectory, since the file name pattern alone still leaves an empty directory behind in the package. The component's*.hglob would otherwise ship it, and it includes<nlohmann/json.hpp>, which would make nlohmann a public dependency of the installed Elasticsearch headers for the first time.es_log_recordable.his the precedent one line above in the same call: it is the only other header here that includes nlohmann, and it is already excluded for the same reason. Verified by installing into a clean prefix and listing what lands underinclude/opentelemetry/exporters/elasticsearch, which is now justes_log_record_exporter.h.Worth your call rather than mine: the only other
detail/directory in the tree isext/http/client/detail, and it is installed today, sodetail/has meant "public but unstable" here rather than "private". #4327 stops installing it, which would make this exclusion the convention rather than the exception, but that PR is not merged and I would rather not lean on it. By the reading that holds today the file belongs insrc/instead, which would need no exclusion at all. I kept it where the tests can reach it and excluded it, and I am happy to move it if you prefer the convention held.On the Bazel side the header is not in the exporter target's
hdrs.hdrsis a target's public interface, so listing it there would let a Bazel consumer include a header the CMake package deliberately does not install. It has its own target, visible only to this package, and the exporter reaches it throughimplementation_depsso it is not re-exported; the test depends on it directly.Checked rather than assumed. A consumer that depends only on
:es_log_record_exporter:and the same consumer with
:es_bulk_responseadded builds. This is the repository's firstimplementation_depsand its first target levelvisibility, so say if you would rather the header simply stayed inhdrsand the two package surfaces differed, the way//ext:headerscurrently does.One other thing in this diff that is not strictly part of the fix: the bulk URI drops
?pretty. The old check depended on pretty printed whitespace, so asking the server for it no longer serves any purpose, but it does change the request and I would rather name it than have it found.Guarded with
OPENTELEMETRY_HAVE_EXCEPTIONSso thetry/catchcompiles under the-fno-exceptionsBazel config.<nlohmann/json.hpp>stays included by the.ccbecause it still callsGetJSON().dump()directly.Tests
The helper's cases are covered directly: pretty and compact success, a rejected item (reason names the underlying error), a non-2xx status with
{"errors":false}(the sync false-success invariant),errors:truewith no extractable item error, malformed and empty bodies, and a missing or non-booleanerrorsfield. The 2xx range is pinned at its 199/200/299/300 boundaries. The acknowledgement count is pinned in both directions: noitems,items:null, too few, too many, and the exact count. Entries that do not acknowledge an index operation are pinned separately:nullentries, scalars, a nested array,{}, an action the exporter never submitted,indexholding a scalar, two members in one entry, two members where one of them is valid,indexwith no status, and the samenullshape witherrors:true, which has to fail whichever way the flag reads. That case is mutation checked: removing theindexlookup from the helper while leaving the member count and the status requirement turns it red, so it pins the operation identity rather than the JSON shape around it. A separate case covers the response contradicting itself: a 400 undererrors:false, one rejection among several acknowledgements, and the 200/299/300 band boundary. The shared success and rejection fixtures now carry thestatusand_indexa real bulk response has; they were abridged to what the old substring check looked at.Now that #4298 has landed, a fake session that responds from inside
SendRequest()no longer deadlocks the synchronous path, so three cases run through the exporter itself rather than through the helper:"failed" : 0, is a failed export,The third is the one the helper tests cannot give you. A handler that stored a fixed status, or an
Export()that never asked for one, passes every helper case.Those three drive the synchronous path, and they skip in the configuration the coverage job builds.
code.coverageconfiguresall-options-abiv2-preview, which turns onENABLE_ASYNC_EXPORT, so nothing there calledExport()at all and six lines of this change went unexecuted: the handler'ssubmitted_operations_member, the bulk URI, the handler construction, and the parse with its failure log.Two more cases cover that path.
Export()returns before a response arrives there, so the parsed result decides only which internal log line is written, and they read it through a captured log handler the waybatch_span_processor_testdoes: an accepted response logs no export failure, a rejected one does. Measured with lcov on the coverage preset, the six lines go from zero hits to covered. Of the two, only the rejected one discriminates: putting the substring check back on the asynchronous path makes it fail, because"failed" : 0appears in a body that has a rejected item.The accepted one passes either way and is there as its control.
Both sets are fixtures that skip in
SetUprather than cases that compile out.gtest_add_testsregisters from the source, so a case missing from the binary is still handed to CTest, and a gtest filter that matches nothing exits zero, which reports a pass without running. Putting the skip inSetUprather than at the top of each body also keepsGTEST_SKIP, which returns, from leaving the rest of a body unreachable, which MSVC reports as C4702 and the maintainer mode jobs turn into an error.The
catch (...)guard in the helper stays uncovered. It is unreachable by input, since the parser rejects malformed bodies rather than throwing, and reaching it needs allocation fault injection, which has no precedent here.The fake client is the same one #4331 adds to this file. Whichever lands first, the other drops the duplicate when it rebases.
Verification
[ PASSED ] 16 tests.withWITH_ELASTICSEARCH=ON: eleven helper cases, the recordable and client construction cases, and the three above. The two asynchronous cases skip in that configuration;WITH_ASYNC_EXPORT_PREVIEW=ONruns them and skips the three synchronous ones,[ PASSED ] 15 tests.Both configurations build with no warnings underOTELCPP_MAINTAINER_MODE=ON.Export()to the old substring check and rebuilding fails exactly the two cases that should discriminate:AcceptedBulkResponseIsASuccessfulExportstill passes there, which is correct: the happy path works under either check, so it is not a discriminator.Success is decided from the compact
/_bulkresponse; parsing no longer depends on pretty-printing../ci/do_ci.sh formatexits 0 with no diff (clang-format 18, cmake-format 0.6.13, buildifier 3.5.0).The lines the coverage report still marks in
es_bulk_response.hare the defensivecatch (...)that keeps anything from escaping thenoexceptresponse handlers. No responsebody reaches it, since the parser rejects malformed and invalid-UTF-8 input before any
throwing call, so exercising it would mean injecting an allocation failure through a global
operator newoverride. That is program-wide machinery this repository does not useelsewhere, and it behaves differently in the shared-library configurations, so I left the
guard uncovered rather than add it. I can add it if you would rather have the coverage.
clang-tidy was measured against
mainrather than in isolation, and over the test target so that the test file is compiled as well as the exporter. On theall-options-abiv2-previewpreset both trees report the same three checks and the same twenty two warning lines, and nothing ines_bulk_response.h, so the branch adds nothing towarning_limit. The include-what-you-use jobs are green on all three presets.