Skip to content

Commit 4eba0cc

Browse files
committed
Fold the HTTP status into the bulk export result on both paths
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>
1 parent ea25af6 commit 4eba0cc

3 files changed

Lines changed: 66 additions & 22 deletions

File tree

‎exporters/elasticsearch/include/opentelemetry/exporters/elasticsearch/detail/es_bulk_response.h‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,25 @@ namespace detail
2828
* stops the body being inspected counts as a failed export. With exceptions disabled there is
2929
* nothing to catch, since an allocation failure terminates instead.
3030
*
31+
* A non-2xx HTTP status is a failure regardless of the body: the top-level "errors" flag only
32+
* describes item-level outcomes and cannot override a transport or application error, so both
33+
* the synchronous and asynchronous paths must pass the status here rather than inspecting the
34+
* body alone.
35+
*
36+
* @param status_code the HTTP status code of the response
3137
* @param body the raw response body
3238
* @param failure_reason set to a short explanation when the function returns false
33-
* @return true when the response reports every item as written
39+
* @return true when the status is 2xx and the response reports every item as written
3440
*/
35-
inline bool IsBulkResponseSuccessful(const std::string &body, std::string &failure_reason) noexcept
41+
inline bool IsBulkResponseSuccessful(int status_code,
42+
const std::string &body,
43+
std::string &failure_reason) noexcept
3644
{
45+
if (status_code < 200 || status_code > 299)
46+
{
47+
failure_reason = "unexpected HTTP status " + std::to_string(status_code);
48+
return false;
49+
}
3750
#if OPENTELEMETRY_HAVE_EXCEPTIONS
3851
try
3952
{

‎exporters/elasticsearch/src/es_log_record_exporter.cc‎

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ class ResponseHandler : public http_client::EventHandler
102102
<< log_message);
103103
}
104104

105+
// Keep the status so Export() can fold it into the ExportResult; storing the body alone
106+
// let a non-2xx response be reported as a success.
107+
status_code_ = response.GetStatusCode();
108+
105109
// Set the response_received_ flag to true and notify any threads waiting on this result
106110
response_received_ = true;
107111
}
@@ -129,6 +133,15 @@ class ResponseHandler : public http_client::EventHandler
129133
return body_;
130134
}
131135

136+
/**
137+
* Returns the HTTP status code of the response
138+
*/
139+
int GetStatusCode()
140+
{
141+
std::unique_lock<std::mutex> lk(mutex_);
142+
return status_code_;
143+
}
144+
132145
// Callback method when an http event occurs
133146
void OnEvent(http_client::SessionState state, nostd::string_view /* reason */) noexcept override
134147
{
@@ -201,6 +214,9 @@ class ResponseHandler : public http_client::EventHandler
201214
// A string to store the response body
202215
std::string body_ = "";
203216

217+
// The HTTP status code of the response
218+
int status_code_ = 0;
219+
204220
// Whether to print the results from the callback
205221
bool console_debug_ = false;
206222
};
@@ -247,17 +263,8 @@ class AsyncResponseHandler : public http_client::EventHandler
247263
OTEL_INTERNAL_LOG_DEBUG(
248264
"[ES Log Exporter] Got response from Elasticsearch, response body: " << body_);
249265
}
250-
const auto status_code = response.GetStatusCode();
251-
if (!(status_code >= 200 && status_code <= 299))
252-
{
253-
OTEL_INTERNAL_LOG_ERROR("[ES Log Exporter] Export failed with HTTP status "
254-
<< status_code << ", response body: " << body_);
255-
result_callback_(sdk::common::ExportResult::kFailure);
256-
return;
257-
}
258-
259266
std::string failure_reason;
260-
if (!detail::IsBulkResponseSuccessful(body_, failure_reason))
267+
if (!detail::IsBulkResponseSuccessful(response.GetStatusCode(), body_, failure_reason))
261268
{
262269
OTEL_INTERNAL_LOG_ERROR("[ES Log Exporter] Logs were not written to Elasticsearch correctly, "
263270
<< failure_reason << ", response body: " << body_);
@@ -460,7 +467,7 @@ sdk::common::ExportResult ElasticsearchLogRecordExporter::Export(
460467
// Parse the response output to determine if Elasticsearch consumed it correctly
461468
std::string responseBody = handler->GetResponseBody();
462469
std::string failure_reason;
463-
if (!detail::IsBulkResponseSuccessful(responseBody, failure_reason))
470+
if (!detail::IsBulkResponseSuccessful(handler->GetStatusCode(), responseBody, failure_reason))
464471
{
465472
OTEL_INTERNAL_LOG_ERROR("[ES Log Exporter] Logs were not written to Elasticsearch correctly, "
466473
<< failure_reason << ", response body: " << responseBody);

‎exporters/elasticsearch/test/es_log_record_exporter_test.cc‎

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -164,36 +164,60 @@ constexpr const char *kOneItemRejected =
164164
TEST(ElasticsearchBulkResponseTests, ReportsSuccessWhenErrorsIsFalse)
165165
{
166166
std::string reason;
167-
EXPECT_TRUE(logs_exporter::detail::IsBulkResponseSuccessful(kPrettySuccess, reason));
168-
EXPECT_TRUE(logs_exporter::detail::IsBulkResponseSuccessful(kCompactSuccess, reason));
167+
EXPECT_TRUE(logs_exporter::detail::IsBulkResponseSuccessful(200, kPrettySuccess, reason));
168+
EXPECT_TRUE(logs_exporter::detail::IsBulkResponseSuccessful(200, kCompactSuccess, reason));
169169
}
170170

171171
TEST(ElasticsearchBulkResponseTests, ReportsFailureWhenAnyItemWasRejected)
172172
{
173173
std::string reason;
174-
EXPECT_FALSE(logs_exporter::detail::IsBulkResponseSuccessful(kOneItemRejected, reason));
174+
EXPECT_FALSE(logs_exporter::detail::IsBulkResponseSuccessful(200, kOneItemRejected, reason));
175175
EXPECT_NE(reason.find("mapper_parsing_exception"), std::string::npos);
176176
}
177177

178178
TEST(ElasticsearchBulkResponseTests, ReportsFailureOnUnusableBody)
179179
{
180180
std::string reason;
181-
EXPECT_FALSE(logs_exporter::detail::IsBulkResponseSuccessful("not json at all", reason));
181+
EXPECT_FALSE(logs_exporter::detail::IsBulkResponseSuccessful(200, "not json at all", reason));
182182
EXPECT_FALSE(reason.empty());
183183

184-
EXPECT_FALSE(logs_exporter::detail::IsBulkResponseSuccessful("", reason));
184+
EXPECT_FALSE(logs_exporter::detail::IsBulkResponseSuccessful(200, "", reason));
185185
EXPECT_FALSE(reason.empty());
186186

187187
// An array rather than the expected object.
188-
EXPECT_FALSE(logs_exporter::detail::IsBulkResponseSuccessful("[1,2,3]", reason));
188+
EXPECT_FALSE(logs_exporter::detail::IsBulkResponseSuccessful(200, "[1,2,3]", reason));
189189
EXPECT_FALSE(reason.empty());
190190
}
191191

192192
TEST(ElasticsearchBulkResponseTests, ReportsFailureWhenErrorsFieldIsMissingOrNotBoolean)
193193
{
194194
std::string reason;
195195
EXPECT_FALSE(
196-
logs_exporter::detail::IsBulkResponseSuccessful(R"({"took":30,"items":[]})", reason));
197-
EXPECT_FALSE(
198-
logs_exporter::detail::IsBulkResponseSuccessful(R"({"errors":"false","items":[]})", reason));
196+
logs_exporter::detail::IsBulkResponseSuccessful(200, R"({"took":30,"items":[]})", reason));
197+
EXPECT_FALSE(logs_exporter::detail::IsBulkResponseSuccessful(
198+
200, R"({"errors":"false","items":[]})", reason));
199+
}
200+
201+
// A non-2xx status is a failure even when the body reports errors:false. This is the invariant
202+
// the substring check and the body-only check both missed, on both the sync and async paths.
203+
TEST(ElasticsearchBulkResponseTests, ReportsFailureOnNon2xxStatus)
204+
{
205+
std::string reason;
206+
const char *ok_body = R"({"errors":false,"items":[]})";
207+
EXPECT_FALSE(logs_exporter::detail::IsBulkResponseSuccessful(500, ok_body, reason));
208+
EXPECT_FALSE(logs_exporter::detail::IsBulkResponseSuccessful(429, ok_body, reason));
209+
EXPECT_FALSE(logs_exporter::detail::IsBulkResponseSuccessful(400, ok_body, reason));
210+
EXPECT_TRUE(logs_exporter::detail::IsBulkResponseSuccessful(200, ok_body, reason));
211+
}
212+
213+
// errors:true with no extractable item error still fails, with the generic reason.
214+
TEST(ElasticsearchBulkResponseTests, ReportsFailureWhenErrorsTrueWithoutItemError)
215+
{
216+
std::string reason;
217+
EXPECT_FALSE(logs_exporter::detail::IsBulkResponseSuccessful(200, R"({"errors":true,"items":[]})",
218+
reason));
219+
EXPECT_FALSE(logs_exporter::detail::IsBulkResponseSuccessful(
220+
200, R"({"errors":true,"items":[null]})", reason));
221+
EXPECT_FALSE(logs_exporter::detail::IsBulkResponseSuccessful(
222+
200, R"({"errors":true,"items":[{"index":42}]})", reason));
199223
}

0 commit comments

Comments
 (0)