From cc3f2f7ad41161d1caaff0bc84cdd4594a930c2e Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Fri, 1 Aug 2025 15:22:15 +0200 Subject: [PATCH 01/13] feat(http): add error handling for exporting --- .../exporter/otlp/proto/http/_log_exporter/__init__.py | 6 +++++- .../exporter/otlp/proto/http/metric_exporter/__init__.py | 8 +++++++- .../exporter/otlp/proto/http/trace_exporter/__init__.py | 6 +++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index b120a2cca4..6e90c63dd0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -186,7 +186,11 @@ def export( serialized_data = encode_logs(batch).SerializeToString() deadline_sec = time() + self._timeout for retry_num in range(_MAX_RETRYS): - resp = self._export(serialized_data, deadline_sec - time()) + try: + resp = self._export(serialized_data, deadline_sec - time()) + except Exception as error: + _logger.error("Failed to export logs batch reason: %s", error) + return LogExportResult.FAILURE if resp.ok: return LogRecordExportResult.SUCCESS # multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff. diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index c6d657e7ae..81801d1a51 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -231,7 +231,13 @@ def export( serialized_data = encode_metrics(metrics_data).SerializeToString() deadline_sec = time() + self._timeout for retry_num in range(_MAX_RETRYS): - resp = self._export(serialized_data, deadline_sec - time()) + try: + resp = self._export(serialized_data, deadline_sec - time()) + except Exception as error: + _logger.error( + "Failed to export metrics batch reason: %s", error + ) + return MetricExportResult.FAILURE if resp.ok: return MetricExportResult.SUCCESS # multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff. diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 055e829dab..f47eccaa8e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -179,7 +179,11 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: serialized_data = encode_spans(spans).SerializePartialToString() deadline_sec = time() + self._timeout for retry_num in range(_MAX_RETRYS): - resp = self._export(serialized_data, deadline_sec - time()) + try: + resp = self._export(serialized_data, deadline_sec - time()) + except Exception as error: + _logger.error("Failed to export span batch reason: %s", error) + return SpanExportResult.FAILURE if resp.ok: return SpanExportResult.SUCCESS # multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff. From bcc20fd0cc4459d753fb623a273955fda726b668 Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Mon, 27 Oct 2025 13:39:31 +0100 Subject: [PATCH 02/13] feat(http_exporter): allow to run retry loop on connection errors --- .../otlp/proto/http/_log_exporter/__init__.py | 28 +++++++++++-------- .../proto/http/metric_exporter/__init__.py | 28 +++++++++++-------- .../proto/http/trace_exporter/__init__.py | 28 +++++++++++-------- 3 files changed, 48 insertions(+), 36 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index 6e90c63dd0..270284237e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -186,30 +186,34 @@ def export( serialized_data = encode_logs(batch).SerializeToString() deadline_sec = time() + self._timeout for retry_num in range(_MAX_RETRYS): + # multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff. + backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2) try: resp = self._export(serialized_data, deadline_sec - time()) + if resp.ok: + return LogExportResult.SUCCESS + if not _is_retryable(resp): + _logger.error( + "Failed to export logs batch code: %s, reason: %s", + resp.status_code, + resp.text, + ) + return LogExportResult.FAILURE except Exception as error: _logger.error("Failed to export logs batch reason: %s", error) - return LogExportResult.FAILURE - if resp.ok: - return LogRecordExportResult.SUCCESS - # multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff. - backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2) + if ( - not _is_retryable(resp) - or retry_num + 1 == _MAX_RETRYS + retry_num + 1 == _MAX_RETRYS or backoff_seconds > (deadline_sec - time()) or self._shutdown ): _logger.error( - "Failed to export logs batch code: %s, reason: %s", - resp.status_code, - resp.text, + "Failed to export logs batch due to timeout," + "max retries or shutdown." ) return LogRecordExportResult.FAILURE _logger.warning( - "Transient error %s encountered while exporting logs batch, retrying in %.2fs.", - resp.reason, + "Transient error encountered while exporting logs batch, retrying in %.2fs.", backoff_seconds, ) shutdown = self._shutdown_is_occuring.wait(backoff_seconds) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index 81801d1a51..0d71a6ed16 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -231,32 +231,36 @@ def export( serialized_data = encode_metrics(metrics_data).SerializeToString() deadline_sec = time() + self._timeout for retry_num in range(_MAX_RETRYS): + # multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff. + backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2) try: resp = self._export(serialized_data, deadline_sec - time()) + if resp.ok: + return MetricExportResult.SUCCESS + if not _is_retryable(resp): + _logger.error( + "Failed to export metrics batch code: %s, reason: %s", + resp.status_code, + resp.text, + ) + return MetricExportResult.FAILURE except Exception as error: _logger.error( "Failed to export metrics batch reason: %s", error ) - return MetricExportResult.FAILURE - if resp.ok: - return MetricExportResult.SUCCESS - # multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff. - backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2) + if ( - not _is_retryable(resp) - or retry_num + 1 == _MAX_RETRYS + retry_num + 1 == _MAX_RETRYS or backoff_seconds > (deadline_sec - time()) or self._shutdown ): _logger.error( - "Failed to export metrics batch code: %s, reason: %s", - resp.status_code, - resp.text, + "Failed to export metrics batch due to timeout," + "max retries or shutdown." ) return MetricExportResult.FAILURE _logger.warning( - "Transient error %s encountered while exporting metrics batch, retrying in %.2fs.", - resp.reason, + "Transient error encountered while exporting metrics batch, retrying in %.2fs.", backoff_seconds, ) shutdown = self._shutdown_in_progress.wait(backoff_seconds) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index f47eccaa8e..91583da5a6 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -179,30 +179,34 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: serialized_data = encode_spans(spans).SerializePartialToString() deadline_sec = time() + self._timeout for retry_num in range(_MAX_RETRYS): + # multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff. + backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2) try: resp = self._export(serialized_data, deadline_sec - time()) + if resp.ok: + return SpanExportResult.SUCCESS + if not _is_retryable(resp): + _logger.error( + "Failed to export span batch code: %s, reason: %s", + resp.status_code, + resp.text, + ) + return SpanExportResult.FAILURE except Exception as error: _logger.error("Failed to export span batch reason: %s", error) - return SpanExportResult.FAILURE - if resp.ok: - return SpanExportResult.SUCCESS - # multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff. - backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2) + if ( - not _is_retryable(resp) - or retry_num + 1 == _MAX_RETRYS + retry_num + 1 == _MAX_RETRYS or backoff_seconds > (deadline_sec - time()) or self._shutdown ): _logger.error( - "Failed to export span batch code: %s, reason: %s", - resp.status_code, - resp.text, + "Failed to export span batch due to timeout," + "max retries or shutdown." ) return SpanExportResult.FAILURE _logger.warning( - "Transient error %s encountered while exporting span batch, retrying in %.2fs.", - resp.reason, + "Transient error encountered while exporting span batch, retrying in %.2fs.", backoff_seconds, ) shutdown = self._shutdown_in_progress.wait(backoff_seconds) From b8cea5cbf6bf3313dde96e65442f143ceb289f22 Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Thu, 6 Nov 2025 11:19:22 +0100 Subject: [PATCH 03/13] feat(http): change error types that are caught --- .../exporter/otlp/proto/http/_log_exporter/__init__.py | 2 +- .../exporter/otlp/proto/http/metric_exporter/__init__.py | 2 +- .../exporter/otlp/proto/http/trace_exporter/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index 270284237e..6575b836e3 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -199,7 +199,7 @@ def export( resp.text, ) return LogExportResult.FAILURE - except Exception as error: + except requests.exceptions.RequestException as error: _logger.error("Failed to export logs batch reason: %s", error) if ( diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index 0d71a6ed16..8b869b2e5a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -244,7 +244,7 @@ def export( resp.text, ) return MetricExportResult.FAILURE - except Exception as error: + except requests.exceptions.RequestException as error: _logger.error( "Failed to export metrics batch reason: %s", error ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 91583da5a6..a02b945cdc 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -192,7 +192,7 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: resp.text, ) return SpanExportResult.FAILURE - except Exception as error: + except requests.exceptions.RequestException as error: _logger.error("Failed to export span batch reason: %s", error) if ( From fb0684e37bb617abbeef1921457db132f3ddfdde Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Thu, 6 Nov 2025 11:33:16 +0100 Subject: [PATCH 04/13] refactor(http): introduce variables to unify logging --- .../otlp/proto/http/_log_exporter/__init__.py | 26 ++++++++++++------- .../proto/http/metric_exporter/__init__.py | 25 +++++++++++------- .../proto/http/trace_exporter/__init__.py | 26 ++++++++++++------- 3 files changed, 49 insertions(+), 28 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index 6575b836e3..4cda8a38d9 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -192,15 +192,22 @@ def export( resp = self._export(serialized_data, deadline_sec - time()) if resp.ok: return LogExportResult.SUCCESS - if not _is_retryable(resp): - _logger.error( - "Failed to export logs batch code: %s, reason: %s", - resp.status_code, - resp.text, - ) - return LogExportResult.FAILURE except requests.exceptions.RequestException as error: - _logger.error("Failed to export logs batch reason: %s", error) + reason = str(error) + retryable = True + status_code = None + else: + reason = resp.reason + retryable = _is_retryable(resp) + status_code = resp.status_code + + if not retryable: + _logger.error( + "Failed to export logs batch code: %s, reason: %s", + status_code, + reason, + ) + return LogExportResult.FAILURE if ( retry_num + 1 == _MAX_RETRYS @@ -213,7 +220,8 @@ def export( ) return LogRecordExportResult.FAILURE _logger.warning( - "Transient error encountered while exporting logs batch, retrying in %.2fs.", + "Transient error %s encountered while exporting logs batch, retrying in %.2fs.", + reason, backoff_seconds, ) shutdown = self._shutdown_is_occuring.wait(backoff_seconds) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index 8b869b2e5a..1618ad7c68 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -237,18 +237,22 @@ def export( resp = self._export(serialized_data, deadline_sec - time()) if resp.ok: return MetricExportResult.SUCCESS - if not _is_retryable(resp): - _logger.error( - "Failed to export metrics batch code: %s, reason: %s", - resp.status_code, - resp.text, - ) - return MetricExportResult.FAILURE except requests.exceptions.RequestException as error: + reason = str(error) + retryable = True + status_code = None + else: + reason = resp.reason + retryable = _is_retryable(resp) + status_code = resp.status_code + + if not retryable: _logger.error( - "Failed to export metrics batch reason: %s", error + "Failed to export metrics batch code: %s, reason: %s", + status_code, + reason, ) - + return MetricExportResult.FAILURE if ( retry_num + 1 == _MAX_RETRYS or backoff_seconds > (deadline_sec - time()) @@ -260,7 +264,8 @@ def export( ) return MetricExportResult.FAILURE _logger.warning( - "Transient error encountered while exporting metrics batch, retrying in %.2fs.", + "Transient error %s encountered while exporting metrics batch, retrying in %.2fs.", + reason, backoff_seconds, ) shutdown = self._shutdown_in_progress.wait(backoff_seconds) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index a02b945cdc..8a974c8462 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -185,15 +185,22 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: resp = self._export(serialized_data, deadline_sec - time()) if resp.ok: return SpanExportResult.SUCCESS - if not _is_retryable(resp): - _logger.error( - "Failed to export span batch code: %s, reason: %s", - resp.status_code, - resp.text, - ) - return SpanExportResult.FAILURE except requests.exceptions.RequestException as error: - _logger.error("Failed to export span batch reason: %s", error) + reason = str(error) + retryable = True + status_code = None + else: + reason = resp.reason + retryable = _is_retryable(resp) + status_code = resp.status_code + + if not retryable: + _logger.error( + "Failed to export span batch code: %s, reason: %s", + status_code, + reason, + ) + return SpanExportResult.FAILURE if ( retry_num + 1 == _MAX_RETRYS @@ -206,7 +213,8 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: ) return SpanExportResult.FAILURE _logger.warning( - "Transient error encountered while exporting span batch, retrying in %.2fs.", + "Transient error %s encountered while exporting span batch, retrying in %.2fs.", + reason, backoff_seconds, ) shutdown = self._shutdown_in_progress.wait(backoff_seconds) From 80a5f8757701802c7ec75d9a318f082de00de88f Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Mon, 17 Nov 2025 14:27:25 +0100 Subject: [PATCH 05/13] feat(http_exporter): only retry on connection error --- .../exporter/otlp/proto/http/_log_exporter/__init__.py | 5 ++++- .../exporter/otlp/proto/http/metric_exporter/__init__.py | 5 ++++- .../exporter/otlp/proto/http/trace_exporter/__init__.py | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index 4cda8a38d9..4a7f7f1cb0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -194,7 +194,10 @@ def export( return LogExportResult.SUCCESS except requests.exceptions.RequestException as error: reason = str(error) - retryable = True + if isinstance(error, ConnectionError): + retryable = True + else: + retryable = False status_code = None else: reason = resp.reason diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index 1618ad7c68..d3a943bef5 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -239,7 +239,10 @@ def export( return MetricExportResult.SUCCESS except requests.exceptions.RequestException as error: reason = str(error) - retryable = True + if isinstance(error, ConnectionError): + retryable = True + else: + retryable = False status_code = None else: reason = resp.reason diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 8a974c8462..31a9ff978e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -187,7 +187,10 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: return SpanExportResult.SUCCESS except requests.exceptions.RequestException as error: reason = str(error) - retryable = True + if isinstance(error, ConnectionError): + retryable = True + else: + retryable = False status_code = None else: reason = resp.reason From 943634fe4d3c87a0ef064e9978bb389f65343db1 Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Mon, 17 Nov 2025 14:27:54 +0100 Subject: [PATCH 06/13] test(http_exporter): add test case for connection errors while exporting --- .../metrics/test_otlp_metrics_exporter.py | 44 +++++++++++++++++++ .../tests/test_proto_log_exporter.py | 43 ++++++++++++++++++ .../tests/test_proto_span_exporter.py | 43 ++++++++++++++++++ 3 files changed, 130 insertions(+) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py index eca1aed5d9..8c2026f682 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py @@ -19,7 +19,9 @@ from unittest import TestCase from unittest.mock import ANY, MagicMock, Mock, patch +import requests from requests import Session +from requests.exceptions import ConnectionError from requests.models import Response from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( @@ -555,6 +557,48 @@ def test_retry_timeout(self, mock_post): warning.records[0].message, ) + @patch.object(Session, "post") + def test_export_no_collector_available_retryable(self, mock_post): + exporter = OTLPMetricExporter(timeout=1.5) + msg = "Server not available." + mock_post.side_effect = ConnectionError(msg) + with self.assertLogs(level=WARNING) as warning: + before = time.time() + # Set timeout to 1.5 seconds + self.assertEqual( + exporter.export(self.metrics["sum_int"]), + MetricExportResult.FAILURE, + ) + after = time.time() + # First call at time 0, second at time 1, then an early return before the second backoff sleep b/c it would exceed timeout. + # Additionally every retry results in two calls, therefore 4. + self.assertEqual(mock_post.call_count, 4) + # There's a +/-20% jitter on each backoff. + self.assertTrue(0.75 < after - before < 1.25) + self.assertIn( + f"Transient error {msg} encountered while exporting metrics batch, retrying in", + warning.records[0].message, + ) + + @patch.object(Session, "post") + def test_export_no_collector_available(self, mock_post): + exporter = OTLPMetricExporter(timeout=1.5) + + mock_post.side_effect = requests.exceptions.RequestException() + with self.assertLogs(level=WARNING) as warning: + # Set timeout to 1.5 seconds + self.assertEqual( + exporter.export(self.metrics["sum_int"]), + MetricExportResult.FAILURE, + ) + # First call at time 0, second at time 1, then an early return before the second backoff sleep b/c it would exceed timeout. + self.assertEqual(mock_post.call_count, 1) + # There's a +/-20% jitter on each backoff. + self.assertIn( + "Failed to export metrics batch code", + warning.records[0].message, + ) + @patch.object(Session, "post") def test_timeout_set_correctly(self, mock_post): resp = Response() diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py index 31e824a980..ba50665243 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -24,6 +24,7 @@ import requests from google.protobuf.json_format import MessageToDict from requests import Session +from requests.exceptions import ConnectionError from requests.models import Response from opentelemetry._logs import LogRecord, SeverityNumber @@ -483,6 +484,48 @@ def test_retry_timeout(self, mock_post): warning.records[0].message, ) + @patch.object(Session, "post") + def test_export_no_collector_available_retryable(self, mock_post): + exporter = OTLPLogExporter(timeout=1.5) + msg = "Server not available." + mock_post.side_effect = ConnectionError(msg) + with self.assertLogs(level=WARNING) as warning: + before = time.time() + # Set timeout to 1.5 seconds + self.assertEqual( + exporter.export(self._get_sdk_log_data()), + LogExportResult.FAILURE, + ) + after = time.time() + # First call at time 0, second at time 1, then an early return before the second backoff sleep b/c it would exceed timeout. + # Additionally every retry results in two calls, therefore 4. + self.assertEqual(mock_post.call_count, 4) + # There's a +/-20% jitter on each backoff. + self.assertTrue(0.75 < after - before < 1.25) + self.assertIn( + f"Transient error {msg} encountered while exporting logs batch, retrying in", + warning.records[0].message, + ) + + @patch.object(Session, "post") + def test_export_no_collector_available(self, mock_post): + exporter = OTLPLogExporter(timeout=1.5) + + mock_post.side_effect = requests.exceptions.RequestException() + with self.assertLogs(level=WARNING) as warning: + # Set timeout to 1.5 seconds + self.assertEqual( + exporter.export(self._get_sdk_log_data()), + LogExportResult.FAILURE, + ) + # First call at time 0, second at time 1, then an early return before the second backoff sleep b/c it would exceed timeout. + self.assertEqual(mock_post.call_count, 1) + # There's a +/-20% jitter on each backoff. + self.assertIn( + "Failed to export logs batch code", + warning.records[0].message, + ) + @patch.object(Session, "post") def test_timeout_set_correctly(self, mock_post): resp = Response() diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py index 10dcb1a9e0..bf2302a3cf 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py @@ -20,6 +20,7 @@ import requests from requests import Session +from requests.exceptions import ConnectionError from requests.models import Response from opentelemetry.exporter.otlp.proto.http import Compression @@ -303,6 +304,48 @@ def test_retry_timeout(self, mock_post): warning.records[0].message, ) + @patch.object(Session, "post") + def test_export_no_collector_available_retryable(self, mock_post): + exporter = OTLPSpanExporter(timeout=1.5) + msg = "Server not available." + mock_post.side_effect = ConnectionError(msg) + with self.assertLogs(level=WARNING) as warning: + before = time.time() + # Set timeout to 1.5 seconds + self.assertEqual( + exporter.export([BASIC_SPAN]), + SpanExportResult.FAILURE, + ) + after = time.time() + # First call at time 0, second at time 1, then an early return before the second backoff sleep b/c it would exceed timeout. + # Additionally every retry results in two calls, therefore 4. + self.assertEqual(mock_post.call_count, 4) + # There's a +/-20% jitter on each backoff. + self.assertTrue(0.75 < after - before < 1.25) + self.assertIn( + f"Transient error {msg} encountered while exporting span batch, retrying in", + warning.records[0].message, + ) + + @patch.object(Session, "post") + def test_export_no_collector_available(self, mock_post): + exporter = OTLPSpanExporter(timeout=1.5) + + mock_post.side_effect = requests.exceptions.RequestException() + with self.assertLogs(level=WARNING) as warning: + # Set timeout to 1.5 seconds + self.assertEqual( + exporter.export([BASIC_SPAN]), + SpanExportResult.FAILURE, + ) + # First call at time 0, second at time 1, then an early return before the second backoff sleep b/c it would exceed timeout. + self.assertEqual(mock_post.call_count, 1) + # There's a +/-20% jitter on each backoff. + self.assertIn( + "Failed to export span batch code", + warning.records[0].message, + ) + @patch.object(Session, "post") def test_timeout_set_correctly(self, mock_post): resp = Response() From 397b0409e0990404fefdc6e34d97f7a4f2dc42c4 Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Mon, 24 Nov 2025 14:16:43 +0100 Subject: [PATCH 07/13] refactor(http): simplify if statements --- .../exporter/otlp/proto/http/_log_exporter/__init__.py | 5 +---- .../exporter/otlp/proto/http/metric_exporter/__init__.py | 5 +---- .../exporter/otlp/proto/http/trace_exporter/__init__.py | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index 4a7f7f1cb0..bb46e8bef1 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -194,10 +194,7 @@ def export( return LogExportResult.SUCCESS except requests.exceptions.RequestException as error: reason = str(error) - if isinstance(error, ConnectionError): - retryable = True - else: - retryable = False + retryable = isinstance(error, ConnectionError) status_code = None else: reason = resp.reason diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index d3a943bef5..d9e3fc8970 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -239,10 +239,7 @@ def export( return MetricExportResult.SUCCESS except requests.exceptions.RequestException as error: reason = str(error) - if isinstance(error, ConnectionError): - retryable = True - else: - retryable = False + retryable = isinstance(error, ConnectionError) status_code = None else: reason = resp.reason diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 31a9ff978e..7835ebd819 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -187,10 +187,7 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: return SpanExportResult.SUCCESS except requests.exceptions.RequestException as error: reason = str(error) - if isinstance(error, ConnectionError): - retryable = True - else: - retryable = False + retryable = isinstance(error, ConnectionError) status_code = None else: reason = resp.reason From afc67a39c980b5cca7675716793a85ce475975d8 Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Mon, 24 Nov 2025 14:18:11 +0100 Subject: [PATCH 08/13] docs(changelog): add changes --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fa6d145d6..2534f8df2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,7 +76,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4654](https://github.com/open-telemetry/opentelemetry-python/pull/4654)). - Fix type checking for built-in metric exporters ([#4820](https://github.com/open-telemetry/opentelemetry-python/pull/4820)) - +- fix: handle connection error + ([#4712](https://github.com/open-telemetry/opentelemetry-python/pull/4709)) + ## Version 1.38.0/0.59b0 (2025-10-16) - Add `rstcheck` to pre-commit to stop introducing invalid RST From 8d9e42e424bca2f43762f3522938445656bd8d5e Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Thu, 27 Nov 2025 10:16:18 +0100 Subject: [PATCH 09/13] fix(http_exporter): use correct class after rebase --- .../exporter/otlp/proto/http/_log_exporter/__init__.py | 4 ++-- .../tests/test_proto_log_exporter.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index bb46e8bef1..cd152d1384 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -191,7 +191,7 @@ def export( try: resp = self._export(serialized_data, deadline_sec - time()) if resp.ok: - return LogExportResult.SUCCESS + return LogRecordExportResult.SUCCESS except requests.exceptions.RequestException as error: reason = str(error) retryable = isinstance(error, ConnectionError) @@ -207,7 +207,7 @@ def export( status_code, reason, ) - return LogExportResult.FAILURE + return LogRecordExportResult.FAILURE if ( retry_num + 1 == _MAX_RETRYS diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py index ba50665243..78a51bfb0c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -494,7 +494,7 @@ def test_export_no_collector_available_retryable(self, mock_post): # Set timeout to 1.5 seconds self.assertEqual( exporter.export(self._get_sdk_log_data()), - LogExportResult.FAILURE, + LogRecordExportResult.FAILURE, ) after = time.time() # First call at time 0, second at time 1, then an early return before the second backoff sleep b/c it would exceed timeout. @@ -516,7 +516,7 @@ def test_export_no_collector_available(self, mock_post): # Set timeout to 1.5 seconds self.assertEqual( exporter.export(self._get_sdk_log_data()), - LogExportResult.FAILURE, + LogRecordExportResult.FAILURE, ) # First call at time 0, second at time 1, then an early return before the second backoff sleep b/c it would exceed timeout. self.assertEqual(mock_post.call_count, 1) From b36fb7907a102e0e1b2ec235dcfcf8870955d4cb Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Mon, 1 Dec 2025 10:45:23 +0100 Subject: [PATCH 10/13] docs(changelog): update changelog --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2534f8df2c..d259014cb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,8 +76,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4654](https://github.com/open-telemetry/opentelemetry-python/pull/4654)). - Fix type checking for built-in metric exporters ([#4820](https://github.com/open-telemetry/opentelemetry-python/pull/4820)) -- fix: handle connection error - ([#4712](https://github.com/open-telemetry/opentelemetry-python/pull/4709)) +- `opentelemetry-exporter-otlp-proto-http`: fix retry logic and error handling for connection failures in trace, metric, and log exporters + ([#4709](https://github.com/open-telemetry/opentelemetry-python/pull/4709)) + ([#4712](https://github.com/open-telemetry/opentelemetry-python/issues/4712)) ## Version 1.38.0/0.59b0 (2025-10-16) From f63e8100b0eb2aa3a659002f0c83f98fefb820ed Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Mon, 1 Dec 2025 10:45:47 +0100 Subject: [PATCH 11/13] refactor(http_exporter): add empty space in logs --- .../exporter/otlp/proto/http/_log_exporter/__init__.py | 2 +- .../exporter/otlp/proto/http/metric_exporter/__init__.py | 2 +- .../exporter/otlp/proto/http/trace_exporter/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index cd152d1384..de5747f408 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -215,7 +215,7 @@ def export( or self._shutdown ): _logger.error( - "Failed to export logs batch due to timeout," + "Failed to export logs batch due to timeout, " "max retries or shutdown." ) return LogRecordExportResult.FAILURE diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index d9e3fc8970..12d36b7827 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -259,7 +259,7 @@ def export( or self._shutdown ): _logger.error( - "Failed to export metrics batch due to timeout," + "Failed to export metrics batch due to timeout, " "max retries or shutdown." ) return MetricExportResult.FAILURE diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 7835ebd819..b247b44660 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -208,7 +208,7 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: or self._shutdown ): _logger.error( - "Failed to export span batch due to timeout," + "Failed to export span batch due to timeout, " "max retries or shutdown." ) return SpanExportResult.FAILURE From f501b946a843dc45211305e56b8389e0ec6dc5a9 Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Thu, 4 Dec 2025 18:02:12 +0100 Subject: [PATCH 12/13] docs(tests): remove comments --- .../tests/metrics/test_otlp_metrics_exporter.py | 7 ------- .../tests/test_proto_log_exporter.py | 7 ------- .../tests/test_proto_span_exporter.py | 7 ------- 3 files changed, 21 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py index 8c2026f682..15e54feed7 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py @@ -564,16 +564,12 @@ def test_export_no_collector_available_retryable(self, mock_post): mock_post.side_effect = ConnectionError(msg) with self.assertLogs(level=WARNING) as warning: before = time.time() - # Set timeout to 1.5 seconds self.assertEqual( exporter.export(self.metrics["sum_int"]), MetricExportResult.FAILURE, ) after = time.time() - # First call at time 0, second at time 1, then an early return before the second backoff sleep b/c it would exceed timeout. - # Additionally every retry results in two calls, therefore 4. self.assertEqual(mock_post.call_count, 4) - # There's a +/-20% jitter on each backoff. self.assertTrue(0.75 < after - before < 1.25) self.assertIn( f"Transient error {msg} encountered while exporting metrics batch, retrying in", @@ -586,14 +582,11 @@ def test_export_no_collector_available(self, mock_post): mock_post.side_effect = requests.exceptions.RequestException() with self.assertLogs(level=WARNING) as warning: - # Set timeout to 1.5 seconds self.assertEqual( exporter.export(self.metrics["sum_int"]), MetricExportResult.FAILURE, ) - # First call at time 0, second at time 1, then an early return before the second backoff sleep b/c it would exceed timeout. self.assertEqual(mock_post.call_count, 1) - # There's a +/-20% jitter on each backoff. self.assertIn( "Failed to export metrics batch code", warning.records[0].message, diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py index 78a51bfb0c..8494c62cbb 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -491,16 +491,12 @@ def test_export_no_collector_available_retryable(self, mock_post): mock_post.side_effect = ConnectionError(msg) with self.assertLogs(level=WARNING) as warning: before = time.time() - # Set timeout to 1.5 seconds self.assertEqual( exporter.export(self._get_sdk_log_data()), LogRecordExportResult.FAILURE, ) after = time.time() - # First call at time 0, second at time 1, then an early return before the second backoff sleep b/c it would exceed timeout. - # Additionally every retry results in two calls, therefore 4. self.assertEqual(mock_post.call_count, 4) - # There's a +/-20% jitter on each backoff. self.assertTrue(0.75 < after - before < 1.25) self.assertIn( f"Transient error {msg} encountered while exporting logs batch, retrying in", @@ -513,14 +509,11 @@ def test_export_no_collector_available(self, mock_post): mock_post.side_effect = requests.exceptions.RequestException() with self.assertLogs(level=WARNING) as warning: - # Set timeout to 1.5 seconds self.assertEqual( exporter.export(self._get_sdk_log_data()), LogRecordExportResult.FAILURE, ) - # First call at time 0, second at time 1, then an early return before the second backoff sleep b/c it would exceed timeout. self.assertEqual(mock_post.call_count, 1) - # There's a +/-20% jitter on each backoff. self.assertIn( "Failed to export logs batch code", warning.records[0].message, diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py index bf2302a3cf..a19e62d29e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py @@ -311,16 +311,12 @@ def test_export_no_collector_available_retryable(self, mock_post): mock_post.side_effect = ConnectionError(msg) with self.assertLogs(level=WARNING) as warning: before = time.time() - # Set timeout to 1.5 seconds self.assertEqual( exporter.export([BASIC_SPAN]), SpanExportResult.FAILURE, ) after = time.time() - # First call at time 0, second at time 1, then an early return before the second backoff sleep b/c it would exceed timeout. - # Additionally every retry results in two calls, therefore 4. self.assertEqual(mock_post.call_count, 4) - # There's a +/-20% jitter on each backoff. self.assertTrue(0.75 < after - before < 1.25) self.assertIn( f"Transient error {msg} encountered while exporting span batch, retrying in", @@ -333,14 +329,11 @@ def test_export_no_collector_available(self, mock_post): mock_post.side_effect = requests.exceptions.RequestException() with self.assertLogs(level=WARNING) as warning: - # Set timeout to 1.5 seconds self.assertEqual( exporter.export([BASIC_SPAN]), SpanExportResult.FAILURE, ) - # First call at time 0, second at time 1, then an early return before the second backoff sleep b/c it would exceed timeout. self.assertEqual(mock_post.call_count, 1) - # There's a +/-20% jitter on each backoff. self.assertIn( "Failed to export span batch code", warning.records[0].message, From ef2ff34b0dba8583e5c78c86b1535ef422af879a Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Tue, 9 Dec 2025 10:59:05 +0100 Subject: [PATCH 13/13] refactor(tests): simplify tests --- .../tests/metrics/test_otlp_metrics_exporter.py | 7 +++---- .../tests/test_proto_log_exporter.py | 7 +++---- .../tests/test_proto_span_exporter.py | 7 +++---- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py index 15e54feed7..2dbbadccb9 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py @@ -563,14 +563,13 @@ def test_export_no_collector_available_retryable(self, mock_post): msg = "Server not available." mock_post.side_effect = ConnectionError(msg) with self.assertLogs(level=WARNING) as warning: - before = time.time() self.assertEqual( exporter.export(self.metrics["sum_int"]), MetricExportResult.FAILURE, ) - after = time.time() - self.assertEqual(mock_post.call_count, 4) - self.assertTrue(0.75 < after - before < 1.25) + # Check for greater 2 because the request is on each retry + # done twice at the moment. + self.assertGreater(mock_post.call_count, 2) self.assertIn( f"Transient error {msg} encountered while exporting metrics batch, retrying in", warning.records[0].message, diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py index 8494c62cbb..c86ac1f6ba 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -490,14 +490,13 @@ def test_export_no_collector_available_retryable(self, mock_post): msg = "Server not available." mock_post.side_effect = ConnectionError(msg) with self.assertLogs(level=WARNING) as warning: - before = time.time() self.assertEqual( exporter.export(self._get_sdk_log_data()), LogRecordExportResult.FAILURE, ) - after = time.time() - self.assertEqual(mock_post.call_count, 4) - self.assertTrue(0.75 < after - before < 1.25) + # Check for greater 2 because the request is on each retry + # done twice at the moment. + self.assertGreater(mock_post.call_count, 2) self.assertIn( f"Transient error {msg} encountered while exporting logs batch, retrying in", warning.records[0].message, diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py index a19e62d29e..5f61344bbf 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py @@ -310,14 +310,13 @@ def test_export_no_collector_available_retryable(self, mock_post): msg = "Server not available." mock_post.side_effect = ConnectionError(msg) with self.assertLogs(level=WARNING) as warning: - before = time.time() self.assertEqual( exporter.export([BASIC_SPAN]), SpanExportResult.FAILURE, ) - after = time.time() - self.assertEqual(mock_post.call_count, 4) - self.assertTrue(0.75 < after - before < 1.25) + # Check for greater 2 because the request is on each retry + # done twice at the moment. + self.assertGreater(mock_post.call_count, 2) self.assertIn( f"Transient error {msg} encountered while exporting span batch, retrying in", warning.records[0].message,