Skip to content

Commit 206c68f

Browse files
committed
Report a read or write error only when it is the result
Choosing between an error line that can describe an export the caller was told succeeded, and a debug line the default log level hides, was a false choice. recordCompletion() now reports whether it was the write that decided the outcome, and the log follows that. So an I/O error that ends the export says so at error level, and one that arrives after a response has already succeeded says nothing. Also: give the cases a CTest timeout, since a wait that stops returning would otherwise stall the job rather than fail it; and drop the claim that the request timeout arrives as a TimedOut event, which nothing dispatches.
1 parent a8bcd0b commit 206c68f

3 files changed

Lines changed: 47 additions & 16 deletions

File tree

‎exporters/elasticsearch/CMakeLists.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,8 @@ if(BUILD_TESTING)
5555
TARGET es_log_record_exporter_test
5656
TEST_PREFIX exporter.
5757
TEST_LIST es_log_record_exporter_test)
58+
59+
# These cases exist to catch a wait that never returns. Without a per test
60+
# bound a regression stalls the job instead of failing it.
61+
set_tests_properties(${es_log_record_exporter_test} PROPERTIES TIMEOUT 30)
5862
endif() # BUILD_TESTING

‎exporters/elasticsearch/src/es_log_record_exporter.cc‎

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ class ResponseHandler : public http_client::EventHandler
108108

109109
/**
110110
* A method the user calls to block their thread until the request has either produced a
111-
* response or failed. The longest duration is the timeout of the request, set by
112-
* SetTimeoutMs(), which arrives here as a TimedOut session event.
111+
* response or failed. It has no deadline of its own and relies on the HTTP client reporting
112+
* one of the terminal session states.
113113
*/
114114
bool waitForResponse()
115115
{
@@ -182,15 +182,19 @@ class ResponseHandler : public http_client::EventHandler
182182
recordCompletion(CompletionState::Failure);
183183
break;
184184
case http_client::SessionState::ReadError:
185-
OTEL_INTERNAL_LOG_DEBUG("[ES Log Exporter] Read error");
186-
// Kept at debug, like Destroyed above: recording is first writer wins, so this can
187-
// arrive after a response has already succeeded, and an error line there would not
188-
// describe the outcome Export() reports.
189-
recordCompletion(CompletionState::Failure);
185+
// Reported only when this is the outcome Export() returns. Recording is first writer
186+
// wins, so either of these can arrive after a response has already succeeded, and an
187+
// error line there would describe a failure the caller was never told about.
188+
if (recordCompletion(CompletionState::Failure))
189+
{
190+
OTEL_INTERNAL_LOG_ERROR("[ES Log Exporter] Read error");
191+
}
190192
break;
191193
case http_client::SessionState::WriteError:
192-
OTEL_INTERNAL_LOG_DEBUG("[ES Log Exporter] Write error");
193-
recordCompletion(CompletionState::Failure);
194+
if (recordCompletion(CompletionState::Failure))
195+
{
196+
OTEL_INTERNAL_LOG_ERROR("[ES Log Exporter] Write error");
197+
}
194198
break;
195199
case http_client::SessionState::Cancelled:
196200
OTEL_INTERNAL_LOG_ERROR("[ES Log Exporter] (manually) cancelled");
@@ -211,22 +215,27 @@ class ResponseHandler : public http_client::EventHandler
211215
* Record the outcome of the request, first writer wins, then release any waiter. Keeping the
212216
* first outcome means a session destroyed after a successful response does not overwrite it.
213217
*/
214-
void recordCompletion(CompletionState state)
218+
/// Returns whether this call is the one that decided the outcome.
219+
bool recordCompletion(CompletionState state)
215220
{
221+
bool recorded = false;
216222
{
217223
std::unique_lock<std::mutex> lk(mutex_);
218-
recordCompletionLocked(state);
224+
recorded = recordCompletionLocked(state);
219225
}
220226
cv_.notify_all();
227+
return recorded;
221228
}
222229

223230
/// As recordCompletion(), for callers that already hold mutex_ and notify themselves.
224-
void recordCompletionLocked(CompletionState state)
231+
bool recordCompletionLocked(CompletionState state)
225232
{
226-
if (completion_ == CompletionState::Pending)
233+
if (completion_ != CompletionState::Pending)
227234
{
228-
completion_ = state;
235+
return false;
229236
}
237+
completion_ = state;
238+
return true;
230239
}
231240

232241
// Define a condition variable and mutex

‎exporters/elasticsearch/test/es_log_record_exporter_test.cc‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,8 +353,8 @@ TEST(ElasticsearchLogsExporterSyncTests, SessionDestroyedAfterAResponseKeepsTheS
353353
EXPECT_EQ(result, opentelemetry::sdk::common::ExportResult::kSuccess);
354354
}
355355

356-
// Same rule for the two states this change makes terminal. It is why they log at debug rather
357-
// than error: reaching one of them says nothing about the result Export() goes on to report.
356+
// Same rule for the two states this change makes terminal, and the reason their error line is
357+
// conditional: reaching one of them says nothing on its own about the result Export() reports.
358358
TEST(ElasticsearchLogsExporterSyncTests, IoErrorAfterAResponseKeepsTheSuccess)
359359
{
360360
SKIP_WITHOUT_SYNC_EXPORT();
@@ -369,3 +369,21 @@ TEST(ElasticsearchLogsExporterSyncTests, IoErrorAfterAResponseKeepsTheSuccess)
369369
EXPECT_EQ(result, opentelemetry::sdk::common::ExportResult::kSuccess);
370370
}
371371
}
372+
373+
// The mirror image, and the case where the error line is the only diagnostic the caller gets:
374+
// the I/O error is recorded first, so a response arriving afterwards does not rescue the export.
375+
TEST(ElasticsearchLogsExporterSyncTests, IoErrorBeforeAResponseKeepsTheFailure)
376+
{
377+
SKIP_WITHOUT_SYNC_EXPORT();
378+
for (const auto state :
379+
{http_client::SessionState::ReadError, http_client::SessionState::WriteError})
380+
{
381+
SCOPED_TRACE(static_cast<int>(state));
382+
const auto result = ExportWith([state](http_client::EventHandler &handler) {
383+
handler.OnEvent(state, "");
384+
FakeResponse response(200, kAcceptedBody);
385+
handler.OnResponse(response);
386+
});
387+
EXPECT_EQ(result, opentelemetry::sdk::common::ExportResult::kFailure);
388+
}
389+
}

0 commit comments

Comments
 (0)