From 713810b92fdda2d4ff78ec32aa66197b5050b76e Mon Sep 17 00:00:00 2001 From: Petr Heinz Date: Mon, 19 Oct 2026 12:12:56 +0200 Subject: [PATCH 1/2] Add failing tests for T-2221: report a server 5xx after the retries run out Since 0.5.1 a real 5xx response after all retries drops the batch without any output, while a connection error prints why it gave up. These tests pin "Failed to send logs to Better Stack after 3 retries: HTTP " for 500 and 503, the exact network-error message, and silence when a retry succeeds. Co-Authored-By: Claude Opus 5.5 --- tests/test_flusher.py | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/tests/test_flusher.py b/tests/test_flusher.py index 065a870..b203372 100644 --- a/tests/test_flusher.py +++ b/tests/test_flusher.py @@ -143,9 +143,44 @@ def uploader(frame): for log in first_frame: pipe.put(log, block=False) - fw.step() # must not raise AttributeError + with patch('builtins.print') as mock_print: + fw.step() # must not raise AttributeError self.assertTrue(fw._clean) + # The batch is dropped here, so say why, as the network-error path does. + mock_print.assert_called_once_with('Failed to send logs to Better Stack after 3 retries: HTTP 500') + + @patch('logtail.flusher.time.sleep') + def test_step_logs_any_server_5xx_after_retries(self, _mock_sleep): + def uploader(frame): + resp = requests.models.Response() + resp.status_code = 503 + return resp + + pipe, _, fw = self._setup_worker(uploader) + pipe.put('log', block=False) + + with patch('builtins.print') as mock_print: + fw.step() + + mock_print.assert_called_once_with('Failed to send logs to Better Stack after 3 retries: HTTP 503') + + @patch('logtail.flusher.time.sleep') + def test_step_does_not_log_when_a_retry_succeeds(self, _mock_sleep): + status_codes = iter([500, 503, 202]) + + def uploader(frame): + resp = requests.models.Response() + resp.status_code = next(status_codes) + return resp + + pipe, _, fw = self._setup_worker(uploader) + pipe.put('log', block=False) + + with patch('builtins.print') as mock_print: + fw.step() + + mock_print.assert_not_called() @patch('logtail.flusher.time.sleep') def test_step_still_logs_network_error_fake500(self, _mock_sleep): @@ -164,7 +199,7 @@ def uploader(frame): with patch('builtins.print') as mock_print: fw.step() - mock_print.assert_called_once() + mock_print.assert_called_once_with('Failed to send logs to Better Stack after 3 retries: network is down') def test_shutdown_condition_empties_queue_and_shuts_down(self): self.buffer_capacity = 10 From 8d04803ad5269567d8d7c1dbef0d9a5031dc8795 Mon Sep 17 00:00:00 2001 From: Petr Heinz Date: Mon, 19 Oct 2026 12:13:35 +0200 Subject: [PATCH 2/2] Report a server 5xx after the retries run out, like a connection error When the retries run out on a 5xx answer, print "Failed to send logs to Better Stack after 3 retries: HTTP ", next to the existing "...: " for connection errors, instead of dropping the batch without any output. Co-Authored-By: Claude Opus 5.5 --- logtail/flusher.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/logtail/flusher.py b/logtail/flusher.py index 0ce1c5e..2c7d16d 100644 --- a/logtail/flusher.py +++ b/logtail/flusher.py @@ -89,13 +89,13 @@ def step(self) -> None: if delay is not None: time.sleep(delay) - # A network error surfaces as a Fake500 that carries an `exception` - # attribute; a genuine server HTTP 500 is a real `requests.Response` - # that has no such attribute. Use a default so the latter doesn't - # raise AttributeError here and kill the flush thread (dropping all - # subsequent logs for the life of the process). - if response.status_code == 500 and getattr(response, "exception", None) is not None: - print('Failed to send logs to Better Stack after {} retries: {}'.format(len(RETRY_SCHEDULE), response.exception)) + # The batch is dropped once the retries run out. A network error + # surfaces as a Fake500 that carries the `exception`; a 5xx answer + # from the server is a real `requests.Response` without one, so it + # is reported by its status code instead. + if _should_retry(response.status_code): + reason = getattr(response, 'exception', 'HTTP {}'.format(response.status_code)) + print('Failed to send logs to Better Stack after {} retries: {}'.format(len(RETRY_SCHEDULE), reason)) self._clean = True if shutdown and self.pipe.empty():