From 7dec4f57555648fa3cd39bae80064f45dade16fa Mon Sep 17 00:00:00 2001 From: Lorenzo Ronzani Date: Fri, 24 Apr 2026 09:50:03 +0000 Subject: [PATCH 1/8] Extracted common constants --- .../exporter/otlp/proto/http/_common/__init__.py | 7 +++++++ .../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 ++--- .../tests/metrics/test_otlp_metrics_exporter.py | 6 ++++-- .../tests/test_proto_log_exporter.py | 6 ++++-- .../tests/test_proto_span_exporter.py | 4 +++- 7 files changed, 24 insertions(+), 14 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py index 1bdb7d228c..ab9e9e4ee1 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py @@ -20,8 +20,15 @@ from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER, ) +from opentelemetry.exporter.otlp.proto.http import ( + Compression, +) from opentelemetry.util._importlib_metadata import entry_points +DEFAULT_COMPRESSION = Compression.NoCompression +DEFAULT_ENDPOINT = "http://localhost:4318/" +DEFAULT_TIMEOUT = 10 # in seconds + def _is_retryable(resp: requests.Response) -> bool: if resp.status_code == 408: 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 6032433dd1..c6fc9d64f8 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 @@ -37,6 +37,8 @@ from opentelemetry.exporter.otlp.proto.http._common import ( _is_retryable, _load_session_from_envvar, + DEFAULT_ENDPOINT, + DEFAULT_TIMEOUT, ) from opentelemetry.metrics import MeterProvider from opentelemetry.sdk._logs import ReadableLogRecord @@ -75,10 +77,7 @@ _logger.addFilter(DuplicateFilter()) -DEFAULT_COMPRESSION = Compression.NoCompression -DEFAULT_ENDPOINT = "http://localhost:4318/" DEFAULT_LOGS_EXPORT_PATH = "v1/logs" -DEFAULT_TIMEOUT = 10 # in seconds _MAX_RETRYS = 6 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 efd63b4543..d43d7fa264 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 @@ -53,6 +53,8 @@ from opentelemetry.exporter.otlp.proto.http._common import ( _is_retryable, _load_session_from_envvar, + DEFAULT_ENDPOINT, + DEFAULT_TIMEOUT, ) from opentelemetry.metrics import MeterProvider from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( # noqa: F401 @@ -111,10 +113,7 @@ _logger = logging.getLogger(__name__) -DEFAULT_COMPRESSION = Compression.NoCompression -DEFAULT_ENDPOINT = "http://localhost:4318/" DEFAULT_METRICS_EXPORT_PATH = "v1/metrics" -DEFAULT_TIMEOUT = 10 # in seconds _MAX_RETRYS = 6 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 018d89df1e..fc8baf4603 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 @@ -39,6 +39,8 @@ from opentelemetry.exporter.otlp.proto.http._common import ( _is_retryable, _load_session_from_envvar, + DEFAULT_ENDPOINT, + DEFAULT_TIMEOUT, ) from opentelemetry.metrics import MeterProvider from opentelemetry.sdk.environment_variables import ( @@ -71,10 +73,7 @@ _logger = logging.getLogger(__name__) -DEFAULT_COMPRESSION = Compression.NoCompression -DEFAULT_ENDPOINT = "http://localhost:4318/" DEFAULT_TRACES_EXPORT_PATH = "v1/traces" -DEFAULT_TIMEOUT = 10 # in seconds _MAX_RETRYS = 6 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 5f7ae2afa9..6fcbe75efc 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 @@ -30,11 +30,13 @@ encode_metrics, ) from opentelemetry.exporter.otlp.proto.http import Compression -from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( +from opentelemetry.exporter.otlp.proto.http._common import ( DEFAULT_COMPRESSION, DEFAULT_ENDPOINT, - DEFAULT_METRICS_EXPORT_PATH, DEFAULT_TIMEOUT, +) +from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( + DEFAULT_METRICS_EXPORT_PATH, OTLPMetricExporter, _get_split_resource_metrics_pb2, _split_metrics_data, 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 7981b0bc82..1f81daedee 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 @@ -29,11 +29,13 @@ from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.exporter.otlp.proto.http import Compression -from opentelemetry.exporter.otlp.proto.http._log_exporter import ( +from opentelemetry.exporter.otlp.proto.http._common import ( DEFAULT_COMPRESSION, DEFAULT_ENDPOINT, - DEFAULT_LOGS_EXPORT_PATH, DEFAULT_TIMEOUT, +) +from opentelemetry.exporter.otlp.proto.http._log_exporter import ( + DEFAULT_LOGS_EXPORT_PATH, OTLPLogExporter, ) from opentelemetry.exporter.otlp.proto.http.version import __version__ 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 0df471aa69..f3c9fdb023 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 @@ -24,10 +24,12 @@ from requests.models import Response from opentelemetry.exporter.otlp.proto.http import Compression -from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( +from opentelemetry.exporter.otlp.proto.http._common import ( DEFAULT_COMPRESSION, DEFAULT_ENDPOINT, DEFAULT_TIMEOUT, +) +from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( DEFAULT_TRACES_EXPORT_PATH, OTLPSpanExporter, ) From 0ae61f2a84f2c331251b523d7432afec96e1ee53 Mon Sep 17 00:00:00 2001 From: Lorenzo Ronzani Date: Fri, 24 Apr 2026 10:07:23 +0000 Subject: [PATCH 2/8] Extracted setup session --- .../otlp/proto/http/_common/__init__.py | 29 ++++++++++++++++++- .../otlp/proto/http/_log_exporter/__init__.py | 22 ++++---------- .../proto/http/metric_exporter/__init__.py | 22 ++++---------- .../proto/http/trace_exporter/__init__.py | 22 ++++---------- 4 files changed, 46 insertions(+), 49 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py index ab9e9e4ee1..4a4b2aea92 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py @@ -13,7 +13,7 @@ # limitations under the License. from os import environ -from typing import Literal, Optional +from typing import Dict, Literal, Optional import requests @@ -21,6 +21,7 @@ _OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER, ) from opentelemetry.exporter.otlp.proto.http import ( + _OTLP_HTTP_HEADERS, Compression, ) from opentelemetry.util._importlib_metadata import entry_points @@ -71,3 +72,29 @@ def _load_session_from_envvar( f" must be of type `requests.Session`." ) return None + + +def setup_session( + session: Optional[requests.Session], + cred_envvar: Literal[ + "OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER", + "OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER", + "OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER", + ], + headers: Dict[str, str], + compression: Compression, +) -> requests.Session: + configured_session = ( + session + or _load_session_from_envvar(cred_envvar) + or requests.Session() + ) + configured_session.headers.update(headers) + configured_session.headers.update(_OTLP_HTTP_HEADERS) + # let users override our defaults + configured_session.headers.update(headers) + if compression is not Compression.NoCompression: + configured_session.headers.update( + {"Content-Encoding": compression.value} + ) + return configured_session 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 c6fc9d64f8..1481d51e86 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 @@ -31,12 +31,11 @@ ) from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs from opentelemetry.exporter.otlp.proto.http import ( - _OTLP_HTTP_HEADERS, Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( _is_retryable, - _load_session_from_envvar, + setup_session, DEFAULT_ENDPOINT, DEFAULT_TIMEOUT, ) @@ -134,21 +133,12 @@ def __init__( ) ) self._compression = compression or _compression_from_env() - self._session = ( - session - or _load_session_from_envvar( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER - ) - or requests.Session() + self._session = setup_session( + session, + _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER, + self._headers, + self._compression, ) - self._session.headers.update(self._headers) - self._session.headers.update(_OTLP_HTTP_HEADERS) - # let users override our defaults - self._session.headers.update(self._headers) - if self._compression is not Compression.NoCompression: - self._session.headers.update( - {"Content-Encoding": self._compression.value} - ) self._shutdown = False self._metrics = ExporterMetrics( 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 d43d7fa264..81a1ff1044 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 @@ -47,12 +47,11 @@ encode_metrics, ) from opentelemetry.exporter.otlp.proto.http import ( - _OTLP_HTTP_HEADERS, Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( _is_retryable, - _load_session_from_envvar, + setup_session, DEFAULT_ENDPOINT, DEFAULT_TIMEOUT, ) @@ -194,21 +193,12 @@ def __init__( ) ) self._compression = compression or _compression_from_env() - self._session = ( - session - or _load_session_from_envvar( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER - ) - or requests.Session() + self._session = setup_session( + session, + _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER, + self._headers, + self._compression, ) - self._session.headers.update(self._headers) - self._session.headers.update(_OTLP_HTTP_HEADERS) - # let users override our defaults - self._session.headers.update(self._headers) - if self._compression is not Compression.NoCompression: - self._session.headers.update( - {"Content-Encoding": self._compression.value} - ) self._common_configuration( preferred_temporality, preferred_aggregation 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 fc8baf4603..6f5af675a1 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 @@ -33,12 +33,11 @@ encode_spans, ) from opentelemetry.exporter.otlp.proto.http import ( - _OTLP_HTTP_HEADERS, Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( _is_retryable, - _load_session_from_envvar, + setup_session, DEFAULT_ENDPOINT, DEFAULT_TIMEOUT, ) @@ -129,21 +128,12 @@ def __init__( ) ) self._compression = compression or _compression_from_env() - self._session = ( - session - or _load_session_from_envvar( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER - ) - or requests.Session() + self._session = setup_session( + session, + _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER, + self._headers, + self._compression, ) - self._session.headers.update(self._headers) - self._session.headers.update(_OTLP_HTTP_HEADERS) - # let users override our defaults - self._session.headers.update(self._headers) - if self._compression is not Compression.NoCompression: - self._session.headers.update( - {"Content-Encoding": self._compression.value} - ) self._shutdown = False self._metrics = ExporterMetrics( From e21ee19627f9ed6e2d1514e86f0ded784785e77d Mon Sep 17 00:00:00 2001 From: Lorenzo Ronzani Date: Fri, 24 Apr 2026 11:44:15 +0000 Subject: [PATCH 3/8] Extracted _export function --- .../otlp/proto/http/_common/__init__.py | 47 ++++++++++++++++- .../otlp/proto/http/_log_exporter/__init__.py | 51 ++++--------------- .../proto/http/metric_exporter/__init__.py | 51 ++++--------------- .../proto/http/trace_exporter/__init__.py | 51 ++++--------------- .../metrics/test_otlp_metrics_exporter.py | 2 +- .../tests/test_proto_log_exporter.py | 2 +- .../tests/test_proto_span_exporter.py | 2 +- 7 files changed, 79 insertions(+), 127 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py index 4a4b2aea92..04dc962d85 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py @@ -12,10 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +import gzip +import zlib +from io import BytesIO from os import environ -from typing import Dict, Literal, Optional +from typing import Dict, Literal, Optional, Tuple, Union import requests +from requests.exceptions import ConnectionError from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER, @@ -98,3 +102,44 @@ def setup_session( {"Content-Encoding": compression.value} ) return configured_session + + +def _export( + session: requests.Session, + endpoint: str, + serialized_data: bytes, + compression: Compression, + certificate_file: Union[str, bool], + client_cert: Optional[Union[str, Tuple[str, str]]], + timeout_sec: float, +) -> requests.Response: + data = serialized_data + if compression == Compression.Gzip: + gzip_data = BytesIO() + with gzip.GzipFile(fileobj=gzip_data, mode="w") as gzip_stream: + gzip_stream.write(serialized_data) + data = gzip_data.getvalue() + elif compression == Compression.Deflate: + data = zlib.compress(serialized_data) + + # By default, keep-alive is enabled in Session's request + # headers. Backends may choose to close the connection + # while a post happens which causes an unhandled + # exception. This try/except will retry the post on such exceptions + try: + resp = session.post( + url=endpoint, + data=data, + verify=certificate_file, + timeout=timeout_sec, + cert=client_cert, + ) + except ConnectionError: + resp = session.post( + url=endpoint, + data=data, + verify=certificate_file, + timeout=timeout_sec, + cert=client_cert, + ) + return resp 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 1481d51e86..d30b07f42c 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 @@ -12,12 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import gzip import logging import random import threading -import zlib -from io import BytesIO from os import environ from time import time from typing import Dict, Optional, Sequence @@ -34,6 +31,7 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( + _export, _is_retryable, setup_session, DEFAULT_ENDPOINT, @@ -148,43 +146,6 @@ def __init__( meter_provider, ) - def _export( - self, serialized_data: bytes, timeout_sec: Optional[float] = None - ): - data = serialized_data - if self._compression == Compression.Gzip: - gzip_data = BytesIO() - with gzip.GzipFile(fileobj=gzip_data, mode="w") as gzip_stream: - gzip_stream.write(serialized_data) - data = gzip_data.getvalue() - elif self._compression == Compression.Deflate: - data = zlib.compress(serialized_data) - - if timeout_sec is None: - timeout_sec = self._timeout - - # By default, keep-alive is enabled in Session's request - # headers. Backends may choose to close the connection - # while a post happens which causes an unhandled - # exception. This try/except will retry the post on such exceptions - try: - resp = self._session.post( - url=self._endpoint, - data=data, - verify=self._certificate_file, - timeout=timeout_sec, - cert=self._client_cert, - ) - except ConnectionError: - resp = self._session.post( - url=self._endpoint, - data=data, - verify=self._certificate_file, - timeout=timeout_sec, - cert=self._client_cert, - ) - return resp - def export( self, batch: Sequence[ReadableLogRecord] ) -> LogRecordExportResult: @@ -200,7 +161,15 @@ def export( backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2) export_error: Optional[Exception] = None try: - resp = self._export(serialized_data, deadline_sec - time()) + resp = _export( + self._session, + self._endpoint, + serialized_data, + self._compression, + self._certificate_file, + self._client_cert, + deadline_sec - time() if deadline_sec - time() != None else self._timeout, + ) if resp.ok: return LogRecordExportResult.SUCCESS except requests.exceptions.RequestException as error: 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 81a1ff1044..406865ece8 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 @@ -12,12 +12,9 @@ # limitations under the License. from __future__ import annotations -import gzip import logging import random import threading -import zlib -from io import BytesIO from os import environ from time import time from typing import ( # noqa: F401 @@ -50,6 +47,7 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( + _export, _is_retryable, setup_session, DEFAULT_ENDPOINT, @@ -213,43 +211,6 @@ def __init__( meter_provider, ) - def _export( - self, serialized_data: bytes, timeout_sec: Optional[float] = None - ): - data = serialized_data - if self._compression == Compression.Gzip: - gzip_data = BytesIO() - with gzip.GzipFile(fileobj=gzip_data, mode="w") as gzip_stream: - gzip_stream.write(serialized_data) - data = gzip_data.getvalue() - elif self._compression == Compression.Deflate: - data = zlib.compress(serialized_data) - - if timeout_sec is None: - timeout_sec = self._timeout - - # By default, keep-alive is enabled in Session's request - # headers. Backends may choose to close the connection - # while a post happens which causes an unhandled - # exception. This try/except will retry the post on such exceptions - try: - resp = self._session.post( - url=self._endpoint, - data=data, - verify=self._certificate_file, - timeout=timeout_sec, - cert=self._client_cert, - ) - except ConnectionError: - resp = self._session.post( - url=self._endpoint, - data=data, - verify=self._certificate_file, - timeout=timeout_sec, - cert=self._client_cert, - ) - return resp - def _export_with_retries( self, export_request: ExportMetricsServiceRequest, @@ -273,7 +234,15 @@ def _export_with_retries( backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2) export_error: Optional[Exception] = None try: - resp = self._export(serialized_data, deadline_sec - time()) + resp = _export( + self._session, + self._endpoint, + serialized_data, + self._compression, + self._certificate_file, + self._client_cert, + deadline_sec - time() if deadline_sec - time() != None else self._timeout, + ) if resp.ok: return MetricExportResult.SUCCESS except requests.exceptions.RequestException as 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 6f5af675a1..e68d9daca2 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 @@ -12,12 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import gzip import logging import random import threading -import zlib -from io import BytesIO from os import environ from time import time from typing import Dict, Optional, Sequence @@ -36,6 +33,7 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( + _export, _is_retryable, setup_session, DEFAULT_ENDPOINT, @@ -143,43 +141,6 @@ def __init__( meter_provider, ) - def _export( - self, serialized_data: bytes, timeout_sec: Optional[float] = None - ): - data = serialized_data - if self._compression == Compression.Gzip: - gzip_data = BytesIO() - with gzip.GzipFile(fileobj=gzip_data, mode="w") as gzip_stream: - gzip_stream.write(serialized_data) - data = gzip_data.getvalue() - elif self._compression == Compression.Deflate: - data = zlib.compress(serialized_data) - - if timeout_sec is None: - timeout_sec = self._timeout - - # By default, keep-alive is enabled in Session's request - # headers. Backends may choose to close the connection - # while a post happens which causes an unhandled - # exception. This try/except will retry the post on such exceptions - try: - resp = self._session.post( - url=self._endpoint, - data=data, - verify=self._certificate_file, - timeout=timeout_sec, - cert=self._client_cert, - ) - except ConnectionError: - resp = self._session.post( - url=self._endpoint, - data=data, - verify=self._certificate_file, - timeout=timeout_sec, - cert=self._client_cert, - ) - return resp - def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: if self._shutdown: _logger.warning("Exporter already shutdown, ignoring batch") @@ -193,7 +154,15 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2) export_error: Optional[Exception] = None try: - resp = self._export(serialized_data, deadline_sec - time()) + resp = _export( + self._session, + self._endpoint, + serialized_data, + self._compression, + self._certificate_file, + self._client_cert, + deadline_sec - time() if deadline_sec - time() != None else self._timeout + ) if resp.ok: return SpanExportResult.SUCCESS except requests.exceptions.RequestException as error: 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 6fcbe75efc..ca5bdbce38 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 @@ -1267,7 +1267,7 @@ def test_exponential_explicit_bucket_histogram(self): ExplicitBucketHistogramAggregation, ) - @patch.object(OTLPMetricExporter, "_export", return_value=Mock(ok=True)) + @patch("opentelemetry.exporter.otlp.proto.http.metric_exporter._export", return_value=Mock(ok=True)) def test_2xx_status_code(self, mock_otlp_metric_exporter): """ Test that any HTTP 2XX code returns a successful result 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 1f81daedee..2dcbe27c05 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 @@ -458,7 +458,7 @@ def _get_sdk_log_data() -> List[ReadWriteLogRecord]: return [log1, log2, log3, log4] - @patch.object(OTLPLogExporter, "_export", return_value=Mock(ok=True)) + @patch("opentelemetry.exporter.otlp.proto.http._log_exporter._export", return_value=Mock(ok=True)) def test_2xx_status_code(self, mock_otlp_metric_exporter): """ Test that any HTTP 2XX code returns a successful result 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 f3c9fdb023..3c786fcc20 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 @@ -279,7 +279,7 @@ def test_headers_parse_from_env(self): ), ) - @patch.object(OTLPSpanExporter, "_export", return_value=Mock(ok=True)) + @patch("opentelemetry.exporter.otlp.proto.http.trace_exporter._export", return_value=Mock(ok=True)) def test_2xx_status_code(self, mock_otlp_metric_exporter): """ Test that any HTTP 2XX code returns a successful result From c80b870ab8d2a06e6da972c3ac0247d6e614d0e2 Mon Sep 17 00:00:00 2001 From: Lorenzo Ronzani Date: Wed, 29 Apr 2026 06:59:42 +0000 Subject: [PATCH 4/8] Extracted export with retries fnctionality --- .../otlp/proto/http/_common/__init__.py | 94 ++++++++++++- .../otlp/proto/http/_log_exporter/__init__.py | 94 ++----------- .../proto/http/metric_exporter/__init__.py | 131 +++--------------- .../proto/http/trace_exporter/__init__.py | 94 ++----------- .../metrics/test_otlp_metrics_exporter.py | 2 +- .../tests/test_proto_log_exporter.py | 2 +- .../tests/test_proto_span_exporter.py | 2 +- 7 files changed, 146 insertions(+), 273 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py index 04dc962d85..0e26b3de40 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py @@ -13,10 +13,14 @@ # limitations under the License. import gzip +import logging +import random +import threading import zlib from io import BytesIO from os import environ -from typing import Dict, Literal, Optional, Tuple, Union +from time import time +from typing import Any, Dict, Literal, Optional, Tuple, Union import requests from requests.exceptions import ConnectionError @@ -28,11 +32,17 @@ _OTLP_HTTP_HEADERS, Compression, ) +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_RESPONSE_STATUS_CODE, +) from opentelemetry.util._importlib_metadata import entry_points +_logger = logging.getLogger(__name__) + DEFAULT_COMPRESSION = Compression.NoCompression DEFAULT_ENDPOINT = "http://localhost:4318/" DEFAULT_TIMEOUT = 10 # in seconds +_MAX_RETRYS = 6 def _is_retryable(resp: requests.Response) -> bool: @@ -143,3 +153,85 @@ def _export( cert=client_cert, ) return resp + + +def _export_with_retries( + session: requests.Session, + endpoint: str, + serialized_data: bytes, + compression: Compression, + certificate_file: Union[str, bool], + client_cert: Optional[Union[str, Tuple[str, str]]], + timeout: float, + shutdown_event: threading.Event, + result: Any, + batch_name: str, +) -> bool: + deadline_sec = time() + 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) + export_error: Optional[Exception] = None + try: + resp = _export( + session, + endpoint, + serialized_data, + compression, + certificate_file, + client_cert, + deadline_sec - time(), + ) + if resp.ok: + return True + except requests.exceptions.RequestException as error: + reason = error + export_error = error + retryable = isinstance(error, ConnectionError) + status_code = None + else: + reason = resp.reason + retryable = _is_retryable(resp) + status_code = resp.status_code + + error_attrs = ( + {HTTP_RESPONSE_STATUS_CODE: status_code} + if status_code is not None + else None + ) + + if not retryable: + _logger.error( + "Failed to export %s batch code: %s, reason: %s", + batch_name, + status_code, + reason, + ) + result.error = export_error + result.error_attrs = error_attrs + return False + + if ( + retry_num + 1 == _MAX_RETRYS + or backoff_seconds > (deadline_sec - time()) + or shutdown_event.is_set() + ): + _logger.error( + "Failed to export %s batch due to timeout, " + "max retries or shutdown.", + batch_name, + ) + result.error = export_error + result.error_attrs = error_attrs + return False + + _logger.warning( + "Transient error %s encountered while exporting %s batch, retrying in %.2fs.", + reason, + batch_name, + backoff_seconds, + ) + if shutdown_event.wait(backoff_seconds): + _logger.warning("Shutdown in progress, aborting retry.") + break + return False 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 d30b07f42c..cd404187c9 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 @@ -13,15 +13,12 @@ # limitations under the License. import logging -import random import threading from os import environ -from time import time from typing import Dict, Optional, Sequence from urllib.parse import urlparse import requests -from requests.exceptions import ConnectionError from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( ExporterMetrics, @@ -31,8 +28,7 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( - _export, - _is_retryable, + _export_with_retries, setup_session, DEFAULT_ENDPOINT, DEFAULT_TIMEOUT, @@ -64,9 +60,6 @@ from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) -from opentelemetry.semconv.attributes.http_attributes import ( - HTTP_RESPONSE_STATUS_CODE, -) from opentelemetry.util.re import parse_env_headers _logger = logging.getLogger(__name__) @@ -75,7 +68,6 @@ DEFAULT_LOGS_EXPORT_PATH = "v1/logs" -_MAX_RETRYS = 6 class OTLPLogExporter(LogRecordExporter): @@ -153,77 +145,21 @@ def export( _logger.warning("Exporter already shutdown, ignoring batch") return LogRecordExportResult.FAILURE + serialized_data = encode_logs(batch).SerializeToString() with self._metrics.export_operation(len(batch)) as result: - 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) - export_error: Optional[Exception] = None - try: - resp = _export( - self._session, - self._endpoint, - serialized_data, - self._compression, - self._certificate_file, - self._client_cert, - deadline_sec - time() if deadline_sec - time() != None else self._timeout, - ) - if resp.ok: - return LogRecordExportResult.SUCCESS - except requests.exceptions.RequestException as error: - reason = error - export_error = error - retryable = isinstance(error, ConnectionError) - 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, - ) - error_attrs = ( - {HTTP_RESPONSE_STATUS_CODE: status_code} - if status_code is not None - else None - ) - result.error = export_error - result.error_attrs = error_attrs - return LogRecordExportResult.FAILURE - - if ( - retry_num + 1 == _MAX_RETRYS - or backoff_seconds > (deadline_sec - time()) - or self._shutdown - ): - _logger.error( - "Failed to export logs batch due to timeout, " - "max retries or shutdown." - ) - error_attrs = ( - {HTTP_RESPONSE_STATUS_CODE: status_code} - if status_code is not None - else None - ) - result.error = export_error - result.error_attrs = error_attrs - return LogRecordExportResult.FAILURE - _logger.warning( - "Transient error %s encountered while exporting logs batch, retrying in %.2fs.", - reason, - backoff_seconds, - ) - shutdown = self._shutdown_is_occuring.wait(backoff_seconds) - if shutdown: - _logger.warning("Shutdown in progress, aborting retry.") - break - return LogRecordExportResult.FAILURE + success = _export_with_retries( + self._session, + self._endpoint, + serialized_data, + self._compression, + self._certificate_file, + self._client_cert, + self._timeout, + self._shutdown_is_occuring, + result, + "logs", + ) + return LogRecordExportResult.SUCCESS if success else LogRecordExportResult.FAILURE def force_flush(self, timeout_millis: float = 10_000) -> bool: """Nothing is buffered in this exporter, so this method does nothing.""" 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 406865ece8..128990f7dc 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 @@ -13,10 +13,8 @@ from __future__ import annotations import logging -import random import threading from os import environ -from time import time from typing import ( # noqa: F401 Any, Callable, @@ -28,7 +26,6 @@ from urllib.parse import urlparse import requests -from requests.exceptions import ConnectionError from typing_extensions import deprecated from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( @@ -47,8 +44,7 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( - _export, - _is_retryable, + _export_with_retries, setup_session, DEFAULT_ENDPOINT, DEFAULT_TIMEOUT, @@ -102,16 +98,12 @@ from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) -from opentelemetry.semconv.attributes.http_attributes import ( - HTTP_RESPONSE_STATUS_CODE, -) from opentelemetry.util.re import parse_env_headers _logger = logging.getLogger(__name__) DEFAULT_METRICS_EXPORT_PATH = "v1/metrics" -_MAX_RETRYS = 6 class OTLPMetricExporter(MetricExporter, OTLPMetricExporterMixin): @@ -211,93 +203,6 @@ def __init__( meter_provider, ) - def _export_with_retries( - self, - export_request: ExportMetricsServiceRequest, - deadline_sec: float, - num_items: int, - ) -> MetricExportResult: - """Export serialized data with retry logic until success, non-transient error, or exponential backoff maxed out. - - Args: - export_request: ExportMetricsServiceRequest object containing metrics data to export - deadline_sec: timestamp deadline for the export - - Returns: - MetricExportResult: SUCCESS if export succeeded, FAILURE otherwise - """ - with self._metrics.export_operation(num_items) as result: - serialized_data = export_request.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) - export_error: Optional[Exception] = None - try: - resp = _export( - self._session, - self._endpoint, - serialized_data, - self._compression, - self._certificate_file, - self._client_cert, - deadline_sec - time() if deadline_sec - time() != None else self._timeout, - ) - if resp.ok: - return MetricExportResult.SUCCESS - except requests.exceptions.RequestException as error: - reason = error - export_error = error - retryable = isinstance(error, ConnectionError) - 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 code: %s, reason: %s", - status_code, - reason, - ) - error_attrs = ( - {HTTP_RESPONSE_STATUS_CODE: status_code} - if status_code is not None - else None - ) - result.error = export_error - result.error_attrs = error_attrs - return MetricExportResult.FAILURE - if ( - retry_num + 1 == _MAX_RETRYS - or backoff_seconds > (deadline_sec - time()) - or self._shutdown - ): - _logger.error( - "Failed to export metrics batch due to timeout, " - "max retries or shutdown." - ) - error_attrs = ( - {HTTP_RESPONSE_STATUS_CODE: status_code} - if status_code is not None - else None - ) - result.error = export_error - result.error_attrs = error_attrs - return MetricExportResult.FAILURE - - _logger.warning( - "Transient error %s encountered while exporting metrics batch, retrying in %.2fs.", - reason, - backoff_seconds, - ) - shutdown = self._shutdown_in_progress.wait(backoff_seconds) - if shutdown: - _logger.warning("Shutdown in progress, aborting retry.") - break - return MetricExportResult.FAILURE - def export( self, metrics_data: MetricsData, @@ -315,26 +220,30 @@ def export( num_items += len(metric.data.data_points) export_request = encode_metrics(metrics_data) - deadline_sec = time() + self._timeout + + def _do_export(request: ExportMetricsServiceRequest) -> bool: + serialized_data = request.SerializeToString() + with self._metrics.export_operation(num_items) as result: + return _export_with_retries( + self._session, + self._endpoint, + serialized_data, + self._compression, + self._certificate_file, + self._client_cert, + self._timeout, + self._shutdown_in_progress, + result, + "metrics", + ) # If no batch size configured, export as single batch with retries as configured if self._max_export_batch_size is None: - return self._export_with_retries( - export_request, deadline_sec, num_items - ) + return MetricExportResult.SUCCESS if _do_export(export_request) else MetricExportResult.FAILURE # Else, export in batches of configured size - batched_export_requests = _split_metrics_data( - export_request, self._max_export_batch_size - ) - - for split_metrics_data in batched_export_requests: - export_result = self._export_with_retries( - split_metrics_data, - deadline_sec, - num_items, - ) - if export_result != MetricExportResult.SUCCESS: + for split_request in _split_metrics_data(export_request, self._max_export_batch_size): + if not _do_export(split_request): return MetricExportResult.FAILURE # Only returns SUCCESS if all batches succeeded 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 e68d9daca2..385cbf9937 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 @@ -13,15 +13,12 @@ # limitations under the License. import logging -import random import threading from os import environ -from time import time from typing import Dict, Optional, Sequence from urllib.parse import urlparse import requests -from requests.exceptions import ConnectionError from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( ExporterMetrics, @@ -33,8 +30,7 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( - _export, - _is_retryable, + _export_with_retries, setup_session, DEFAULT_ENDPOINT, DEFAULT_TIMEOUT, @@ -62,16 +58,12 @@ from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) -from opentelemetry.semconv.attributes.http_attributes import ( - HTTP_RESPONSE_STATUS_CODE, -) from opentelemetry.util.re import parse_env_headers _logger = logging.getLogger(__name__) DEFAULT_TRACES_EXPORT_PATH = "v1/traces" -_MAX_RETRYS = 6 class OTLPSpanExporter(SpanExporter): @@ -146,77 +138,21 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: _logger.warning("Exporter already shutdown, ignoring batch") return SpanExportResult.FAILURE + serialized_data = encode_spans(spans).SerializePartialToString() with self._metrics.export_operation(len(spans)) as result: - 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) - export_error: Optional[Exception] = None - try: - resp = _export( - self._session, - self._endpoint, - serialized_data, - self._compression, - self._certificate_file, - self._client_cert, - deadline_sec - time() if deadline_sec - time() != None else self._timeout - ) - if resp.ok: - return SpanExportResult.SUCCESS - except requests.exceptions.RequestException as error: - reason = error - export_error = error - retryable = isinstance(error, ConnectionError) - 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, - ) - error_attrs = ( - {HTTP_RESPONSE_STATUS_CODE: status_code} - if status_code is not None - else None - ) - result.error = export_error - result.error_attrs = error_attrs - return SpanExportResult.FAILURE - - if ( - retry_num + 1 == _MAX_RETRYS - or backoff_seconds > (deadline_sec - time()) - or self._shutdown - ): - _logger.error( - "Failed to export span batch due to timeout, " - "max retries or shutdown." - ) - error_attrs = ( - {HTTP_RESPONSE_STATUS_CODE: status_code} - if status_code is not None - else None - ) - result.error = export_error - result.error_attrs = error_attrs - return SpanExportResult.FAILURE - _logger.warning( - "Transient error %s encountered while exporting span batch, retrying in %.2fs.", - reason, - backoff_seconds, - ) - shutdown = self._shutdown_in_progress.wait(backoff_seconds) - if shutdown: - _logger.warning("Shutdown in progress, aborting retry.") - break - return SpanExportResult.FAILURE + success = _export_with_retries( + self._session, + self._endpoint, + serialized_data, + self._compression, + self._certificate_file, + self._client_cert, + self._timeout, + self._shutdown_in_progress, + result, + "span", + ) + return SpanExportResult.SUCCESS if success else SpanExportResult.FAILURE def shutdown(self): if self._shutdown: 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 ca5bdbce38..25d5d61499 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 @@ -1267,7 +1267,7 @@ def test_exponential_explicit_bucket_histogram(self): ExplicitBucketHistogramAggregation, ) - @patch("opentelemetry.exporter.otlp.proto.http.metric_exporter._export", return_value=Mock(ok=True)) + @patch("opentelemetry.exporter.otlp.proto.http._common._export", return_value=Mock(ok=True)) def test_2xx_status_code(self, mock_otlp_metric_exporter): """ Test that any HTTP 2XX code returns a successful result 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 2dcbe27c05..5c8bf6a823 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 @@ -458,7 +458,7 @@ def _get_sdk_log_data() -> List[ReadWriteLogRecord]: return [log1, log2, log3, log4] - @patch("opentelemetry.exporter.otlp.proto.http._log_exporter._export", return_value=Mock(ok=True)) + @patch("opentelemetry.exporter.otlp.proto.http._common._export", return_value=Mock(ok=True)) def test_2xx_status_code(self, mock_otlp_metric_exporter): """ Test that any HTTP 2XX code returns a successful result 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 3c786fcc20..d0fd695a29 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 @@ -279,7 +279,7 @@ def test_headers_parse_from_env(self): ), ) - @patch("opentelemetry.exporter.otlp.proto.http.trace_exporter._export", return_value=Mock(ok=True)) + @patch("opentelemetry.exporter.otlp.proto.http._common._export", return_value=Mock(ok=True)) def test_2xx_status_code(self, mock_otlp_metric_exporter): """ Test that any HTTP 2XX code returns a successful result From 44a9b4774b95e31459b683ad8bfb849da3d7538f Mon Sep 17 00:00:00 2001 From: Lorenzo Ronzani Date: Wed, 29 Apr 2026 07:13:06 +0000 Subject: [PATCH 5/8] Extracted compression_env method --- .../otlp/proto/http/_common/__init__.py | 19 +++++++++++++++ .../otlp/proto/http/_log_exporter/__init__.py | 24 ++++++------------- .../proto/http/metric_exporter/__init__.py | 18 ++++---------- .../proto/http/trace_exporter/__init__.py | 18 ++++---------- 4 files changed, 34 insertions(+), 45 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py index 0e26b3de40..8368b32b32 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py @@ -27,6 +27,7 @@ from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER, + OTEL_EXPORTER_OTLP_COMPRESSION, ) from opentelemetry.exporter.otlp.proto.http import ( _OTLP_HTTP_HEADERS, @@ -235,3 +236,21 @@ def _export_with_retries( _logger.warning("Shutdown in progress, aborting retry.") break return False + + +def _compression_from_env( + signal_compression_envvar: Literal[ + "OTEL_EXPORTER_OTLP_LOGS_COMPRESSION", + "OTEL_EXPORTER_OTLP_METRICS_COMPRESSION", + "OTEL_EXPORTER_OTLP_TRACES_COMPRESSION", + ], +) -> Compression: + compression = ( + environ.get( + signal_compression_envvar, + environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none"), + ) + .lower() + .strip() + ) + return Compression(compression) 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 cd404187c9..25d5af3799 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 @@ -28,6 +28,7 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( + _compression_from_env, _export_with_retries, setup_session, DEFAULT_ENDPOINT, @@ -45,7 +46,6 @@ OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_KEY, - OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, @@ -84,7 +84,7 @@ def __init__( *, meter_provider: Optional[MeterProvider] = None, ): - self._shutdown_is_occuring = threading.Event() + self._shutdown_in_progress = threading.Event() self._endpoint = endpoint or environ.get( OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, _append_logs_path( @@ -122,7 +122,9 @@ def __init__( environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT), ) ) - self._compression = compression or _compression_from_env() + self._compression = compression or _compression_from_env( + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION + ) self._session = setup_session( session, _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER, @@ -155,7 +157,7 @@ def export( self._certificate_file, self._client_cert, self._timeout, - self._shutdown_is_occuring, + self._shutdown_in_progress, result, "logs", ) @@ -170,22 +172,10 @@ def shutdown(self): _logger.warning("Exporter already shutdown, ignoring call") return self._shutdown = True - self._shutdown_is_occuring.set() + self._shutdown_in_progress.set() self._session.close() -def _compression_from_env() -> Compression: - compression = ( - environ.get( - OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, - environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none"), - ) - .lower() - .strip() - ) - return Compression(compression) - - def _append_logs_path(endpoint: str) -> str: if endpoint.endswith("/"): return endpoint + DEFAULT_LOGS_EXPORT_PATH 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 128990f7dc..0aa76ffc61 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 @@ -44,6 +44,7 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( + _compression_from_env, _export_with_retries, setup_session, DEFAULT_ENDPOINT, @@ -70,7 +71,6 @@ OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_KEY, - OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, @@ -182,7 +182,9 @@ def __init__( environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT), ) ) - self._compression = compression or _compression_from_env() + self._compression = compression or _compression_from_env( + OTEL_EXPORTER_OTLP_METRICS_COMPRESSION + ) self._session = setup_session( session, _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER, @@ -594,18 +596,6 @@ def get_resource_data( return _get_resource_data(sdk_resource_scope_data, resource_class, name) -def _compression_from_env() -> Compression: - compression = ( - environ.get( - OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, - environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none"), - ) - .lower() - .strip() - ) - return Compression(compression) - - def _append_metrics_path(endpoint: str) -> str: if endpoint.endswith("/"): return endpoint + DEFAULT_METRICS_EXPORT_PATH 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 385cbf9937..7b9564fa76 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 @@ -30,6 +30,7 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( + _compression_from_env, _export_with_retries, setup_session, DEFAULT_ENDPOINT, @@ -41,7 +42,6 @@ OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_KEY, - OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TIMEOUT, @@ -117,7 +117,9 @@ def __init__( environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT), ) ) - self._compression = compression or _compression_from_env() + self._compression = compression or _compression_from_env( + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION + ) self._session = setup_session( session, _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER, @@ -167,18 +169,6 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: return True -def _compression_from_env() -> Compression: - compression = ( - environ.get( - OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, - environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none"), - ) - .lower() - .strip() - ) - return Compression(compression) - - def _append_trace_path(endpoint: str) -> str: if endpoint.endswith("/"): return endpoint + DEFAULT_TRACES_EXPORT_PATH From 6cdbc1745034ae3c7946d8c68d8428285ababac9 Mon Sep 17 00:00:00 2001 From: Lorenzo Ronzani Date: Wed, 29 Apr 2026 07:33:41 +0000 Subject: [PATCH 6/8] Fixing ci checks --- .../exporter/otlp/proto/http/_common/__init__.py | 11 ++++------- .../otlp/proto/http/_log_exporter/__init__.py | 9 +++++---- .../otlp/proto/http/metric_exporter/__init__.py | 9 +++++---- .../otlp/proto/http/trace_exporter/__init__.py | 9 +++++---- .../tests/metrics/test_otlp_metrics_exporter.py | 4 +--- .../tests/test_proto_log_exporter.py | 4 +--- .../tests/test_proto_span_exporter.py | 4 +--- 7 files changed, 22 insertions(+), 28 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py index 8368b32b32..a358bf1b97 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py @@ -40,10 +40,7 @@ _logger = logging.getLogger(__name__) -DEFAULT_COMPRESSION = Compression.NoCompression -DEFAULT_ENDPOINT = "http://localhost:4318/" -DEFAULT_TIMEOUT = 10 # in seconds -_MAX_RETRYS = 6 +_MAX_RETRIES = 6 def _is_retryable(resp: requests.Response) -> bool: @@ -89,7 +86,7 @@ def _load_session_from_envvar( return None -def setup_session( +def _setup_session( session: Optional[requests.Session], cred_envvar: Literal[ "OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER", @@ -169,7 +166,7 @@ def _export_with_retries( batch_name: str, ) -> bool: deadline_sec = time() + timeout - for retry_num in range(_MAX_RETRYS): + for retry_num in range(_MAX_RETRIES): # 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) export_error: Optional[Exception] = None @@ -213,7 +210,7 @@ def _export_with_retries( return False if ( - retry_num + 1 == _MAX_RETRYS + retry_num + 1 == _MAX_RETRIES or backoff_seconds > (deadline_sec - time()) or shutdown_event.is_set() ): 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 25d5af3799..2b630bac2e 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 @@ -30,9 +30,7 @@ from opentelemetry.exporter.otlp.proto.http._common import ( _compression_from_env, _export_with_retries, - setup_session, - DEFAULT_ENDPOINT, - DEFAULT_TIMEOUT, + _setup_session, ) from opentelemetry.metrics import MeterProvider from opentelemetry.sdk._logs import ReadableLogRecord @@ -67,6 +65,9 @@ _logger.addFilter(DuplicateFilter()) +DEFAULT_COMPRESSION = Compression.NoCompression +DEFAULT_ENDPOINT = "http://localhost:4318/" +DEFAULT_TIMEOUT = 10 # in seconds DEFAULT_LOGS_EXPORT_PATH = "v1/logs" @@ -125,7 +126,7 @@ def __init__( self._compression = compression or _compression_from_env( OTEL_EXPORTER_OTLP_LOGS_COMPRESSION ) - self._session = setup_session( + self._session = _setup_session( session, _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER, self._headers, 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 0aa76ffc61..b9dc8b8d2c 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 @@ -46,9 +46,7 @@ from opentelemetry.exporter.otlp.proto.http._common import ( _compression_from_env, _export_with_retries, - setup_session, - DEFAULT_ENDPOINT, - DEFAULT_TIMEOUT, + _setup_session, ) from opentelemetry.metrics import MeterProvider from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( # noqa: F401 @@ -103,6 +101,9 @@ _logger = logging.getLogger(__name__) +DEFAULT_COMPRESSION = Compression.NoCompression +DEFAULT_ENDPOINT = "http://localhost:4318/" +DEFAULT_TIMEOUT = 10 # in seconds DEFAULT_METRICS_EXPORT_PATH = "v1/metrics" @@ -185,7 +186,7 @@ def __init__( self._compression = compression or _compression_from_env( OTEL_EXPORTER_OTLP_METRICS_COMPRESSION ) - self._session = setup_session( + self._session = _setup_session( session, _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER, self._headers, 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 7b9564fa76..a3c67a753b 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 @@ -32,9 +32,7 @@ from opentelemetry.exporter.otlp.proto.http._common import ( _compression_from_env, _export_with_retries, - setup_session, - DEFAULT_ENDPOINT, - DEFAULT_TIMEOUT, + _setup_session, ) from opentelemetry.metrics import MeterProvider from opentelemetry.sdk.environment_variables import ( @@ -63,6 +61,9 @@ _logger = logging.getLogger(__name__) +DEFAULT_COMPRESSION = Compression.NoCompression +DEFAULT_ENDPOINT = "http://localhost:4318/" +DEFAULT_TIMEOUT = 10 # in seconds DEFAULT_TRACES_EXPORT_PATH = "v1/traces" @@ -120,7 +121,7 @@ def __init__( self._compression = compression or _compression_from_env( OTEL_EXPORTER_OTLP_TRACES_COMPRESSION ) - self._session = setup_session( + self._session = _setup_session( session, _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER, self._headers, 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 25d5d61499..880a015295 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 @@ -30,12 +30,10 @@ encode_metrics, ) from opentelemetry.exporter.otlp.proto.http import Compression -from opentelemetry.exporter.otlp.proto.http._common import ( +from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( DEFAULT_COMPRESSION, DEFAULT_ENDPOINT, DEFAULT_TIMEOUT, -) -from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( DEFAULT_METRICS_EXPORT_PATH, OTLPMetricExporter, _get_split_resource_metrics_pb2, 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 5c8bf6a823..ecc9234215 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 @@ -29,12 +29,10 @@ from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.exporter.otlp.proto.http import Compression -from opentelemetry.exporter.otlp.proto.http._common import ( +from opentelemetry.exporter.otlp.proto.http._log_exporter import ( DEFAULT_COMPRESSION, DEFAULT_ENDPOINT, DEFAULT_TIMEOUT, -) -from opentelemetry.exporter.otlp.proto.http._log_exporter import ( DEFAULT_LOGS_EXPORT_PATH, OTLPLogExporter, ) 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 d0fd695a29..95bf62a24f 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 @@ -24,12 +24,10 @@ from requests.models import Response from opentelemetry.exporter.otlp.proto.http import Compression -from opentelemetry.exporter.otlp.proto.http._common import ( +from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( DEFAULT_COMPRESSION, DEFAULT_ENDPOINT, DEFAULT_TIMEOUT, -) -from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( DEFAULT_TRACES_EXPORT_PATH, OTLPSpanExporter, ) From 4405d3e24d3a579bd4bde2a6a5f3036b1d3a4658 Mon Sep 17 00:00:00 2001 From: Lorenzo Ronzani Date: Wed, 29 Apr 2026 07:37:19 +0000 Subject: [PATCH 7/8] Code formatted --- .../exporter/otlp/proto/http/_common/__init__.py | 12 +++++------- .../otlp/proto/http/_log_exporter/__init__.py | 6 +++++- .../otlp/proto/http/metric_exporter/__init__.py | 10 ++++++++-- .../otlp/proto/http/trace_exporter/__init__.py | 4 +++- .../tests/metrics/test_otlp_metrics_exporter.py | 7 +++++-- .../tests/test_proto_log_exporter.py | 7 +++++-- .../tests/test_proto_span_exporter.py | 5 ++++- 7 files changed, 35 insertions(+), 16 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py index a358bf1b97..b72726ce30 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py @@ -25,14 +25,14 @@ import requests from requests.exceptions import ConnectionError -from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_COMPRESSION, -) from opentelemetry.exporter.otlp.proto.http import ( _OTLP_HTTP_HEADERS, Compression, ) +from opentelemetry.sdk.environment_variables import ( + _OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER, + OTEL_EXPORTER_OTLP_COMPRESSION, +) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_RESPONSE_STATUS_CODE, ) @@ -97,9 +97,7 @@ def _setup_session( compression: Compression, ) -> requests.Session: configured_session = ( - session - or _load_session_from_envvar(cred_envvar) - or requests.Session() + session or _load_session_from_envvar(cred_envvar) or requests.Session() ) configured_session.headers.update(headers) configured_session.headers.update(_OTLP_HTTP_HEADERS) 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 2b630bac2e..b7bab8b438 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 @@ -162,7 +162,11 @@ def export( result, "logs", ) - return LogRecordExportResult.SUCCESS if success else LogRecordExportResult.FAILURE + return ( + LogRecordExportResult.SUCCESS + if success + else LogRecordExportResult.FAILURE + ) def force_flush(self, timeout_millis: float = 10_000) -> bool: """Nothing is buffered in this exporter, so this method does nothing.""" 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 b9dc8b8d2c..7c5838d2ae 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 @@ -242,10 +242,16 @@ def _do_export(request: ExportMetricsServiceRequest) -> bool: # If no batch size configured, export as single batch with retries as configured if self._max_export_batch_size is None: - return MetricExportResult.SUCCESS if _do_export(export_request) else MetricExportResult.FAILURE + return ( + MetricExportResult.SUCCESS + if _do_export(export_request) + else MetricExportResult.FAILURE + ) # Else, export in batches of configured size - for split_request in _split_metrics_data(export_request, self._max_export_batch_size): + for split_request in _split_metrics_data( + export_request, self._max_export_batch_size + ): if not _do_export(split_request): 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 a3c67a753b..97aada9e27 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 @@ -155,7 +155,9 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: result, "span", ) - return SpanExportResult.SUCCESS if success else SpanExportResult.FAILURE + return ( + SpanExportResult.SUCCESS if success else SpanExportResult.FAILURE + ) def shutdown(self): if self._shutdown: 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 880a015295..c95a733f0b 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 @@ -33,8 +33,8 @@ from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( DEFAULT_COMPRESSION, DEFAULT_ENDPOINT, - DEFAULT_TIMEOUT, DEFAULT_METRICS_EXPORT_PATH, + DEFAULT_TIMEOUT, OTLPMetricExporter, _get_split_resource_metrics_pb2, _split_metrics_data, @@ -1265,7 +1265,10 @@ def test_exponential_explicit_bucket_histogram(self): ExplicitBucketHistogramAggregation, ) - @patch("opentelemetry.exporter.otlp.proto.http._common._export", return_value=Mock(ok=True)) + @patch( + "opentelemetry.exporter.otlp.proto.http._common._export", + return_value=Mock(ok=True), + ) def test_2xx_status_code(self, mock_otlp_metric_exporter): """ Test that any HTTP 2XX code returns a successful result 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 ecc9234215..a13e9f8da9 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 @@ -32,8 +32,8 @@ from opentelemetry.exporter.otlp.proto.http._log_exporter import ( DEFAULT_COMPRESSION, DEFAULT_ENDPOINT, - DEFAULT_TIMEOUT, DEFAULT_LOGS_EXPORT_PATH, + DEFAULT_TIMEOUT, OTLPLogExporter, ) from opentelemetry.exporter.otlp.proto.http.version import __version__ @@ -456,7 +456,10 @@ def _get_sdk_log_data() -> List[ReadWriteLogRecord]: return [log1, log2, log3, log4] - @patch("opentelemetry.exporter.otlp.proto.http._common._export", return_value=Mock(ok=True)) + @patch( + "opentelemetry.exporter.otlp.proto.http._common._export", + return_value=Mock(ok=True), + ) def test_2xx_status_code(self, mock_otlp_metric_exporter): """ Test that any HTTP 2XX code returns a successful result 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 95bf62a24f..546d163bf8 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 @@ -277,7 +277,10 @@ def test_headers_parse_from_env(self): ), ) - @patch("opentelemetry.exporter.otlp.proto.http._common._export", return_value=Mock(ok=True)) + @patch( + "opentelemetry.exporter.otlp.proto.http._common._export", + return_value=Mock(ok=True), + ) def test_2xx_status_code(self, mock_otlp_metric_exporter): """ Test that any HTTP 2XX code returns a successful result From 39723c8ec2283c838e15baffc3f6da36024411f9 Mon Sep 17 00:00:00 2001 From: Lorenzo Ronzani Date: Wed, 29 Apr 2026 08:16:22 +0000 Subject: [PATCH 8/8] updated changelog.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 614f240d4e..40e9bb74f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- `opentelemetry-exporter-otlp-proto-http`: refactor shared HTTP exporter logic into common module, extract `_setup_session`, `_export`, + `_export_with_retries`, and `_compression_from_env` from trace/log/metric exporters into `_common` + ([#2990](https://github.com/open-telemetry/opentelemetry-python/pull/5160)) - `opentelemetry-sdk`: add `additional_properties` support to generated config models via custom `datamodel-codegen` template, enabling plugin/custom component names to flow through typed dataclasses ([#5131](https://github.com/open-telemetry/opentelemetry-python/pull/5131)) - Fix incorrect code example in `create_tracer()` docstring