From 7dec4f57555648fa3cd39bae80064f45dade16fa Mon Sep 17 00:00:00 2001 From: Lorenzo Ronzani Date: Fri, 24 Apr 2026 09:50:03 +0000 Subject: [PATCH 01/17] 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 02/17] 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 03/17] 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 04/17] 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 05/17] 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 06/17] 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 07/17] 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 08/17] 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 From 41d324ed170b2138662b9a2fb0ce559a84e5f4dd Mon Sep 17 00:00:00 2001 From: Lorenzo Ronzani Date: Thu, 30 Apr 2026 11:59:34 +0000 Subject: [PATCH 09/17] Createing OTLPHttpClient parents of all the others --- .../otlp/proto/http/_common/__init__.py | 133 +++++++++++++++--- .../otlp/proto/http/_log_exporter/__init__.py | 125 +++++++--------- .../proto/http/metric_exporter/__init__.py | 125 +++++++--------- .../proto/http/trace_exporter/__init__.py | 124 +++++++--------- .../metrics/test_otlp_metrics_exporter.py | 6 +- .../tests/test_proto_log_exporter.py | 6 +- .../tests/test_proto_span_exporter.py | 4 +- 7 files changed, 268 insertions(+), 255 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 b72726ce30..d054871be7 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 @@ -17,31 +17,51 @@ import random import threading import zlib +from dataclasses import dataclass from io import BytesIO from os import environ from time import time -from typing import Any, Dict, Literal, Optional, Tuple, Union +from typing import Any, Dict, Optional, Tuple, Union +from urllib.parse import urlparse import requests from requests.exceptions import ConnectionError +from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( + ExporterMetrics, +) from opentelemetry.exporter.otlp.proto.http import ( _OTLP_HTTP_HEADERS, Compression, ) +from opentelemetry.metrics import MeterProvider from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER, + 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, +) +from opentelemetry.semconv._incubating.attributes.otel_attributes import ( + OtelComponentTypeValues, ) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_RESPONSE_STATUS_CODE, ) from opentelemetry.util._importlib_metadata import entry_points +from opentelemetry.util.re import parse_env_headers _logger = logging.getLogger(__name__) _MAX_RETRIES = 6 +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: @@ -52,11 +72,7 @@ def _is_retryable(resp: requests.Response) -> bool: def _load_session_from_envvar( - 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", - ], + cred_envvar: str, ) -> Optional[requests.Session]: _credential_env = environ.get( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER @@ -88,11 +104,7 @@ def _load_session_from_envvar( 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", - ], + cred_envvar: str, headers: Dict[str, str], compression: Compression, ) -> requests.Session: @@ -233,13 +245,7 @@ def _export_with_retries( 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: +def _compression_from_env(signal_compression_envvar: str) -> Compression: compression = ( environ.get( signal_compression_envvar, @@ -249,3 +255,94 @@ def _compression_from_env( .strip() ) return Compression(compression) + + +def _append_signal_path(endpoint: str, signal_path: str) -> str: + if endpoint.endswith("/"): + return endpoint + signal_path + return endpoint + f"/{signal_path}" + + +@dataclass(frozen=True) +class _SignalConfig: + endpoint_envvar: str + certificate_envvar: str + client_key_envvar: str + client_certificate_envvar: str + headers_envvar: str + timeout_envvar: str + compression_envvar: str + credential_envvar: str + default_export_path: str + component_type: OtelComponentTypeValues + signal_name: str + + +class OTLPHttpClient: + def __init__( + self, + endpoint: Optional[str], + certificate_file: Optional[str], + client_key_file: Optional[str], + client_certificate_file: Optional[str], + headers: Optional[Dict[str, str]], + timeout: Optional[float], + compression: Optional[Compression], + session: Optional[requests.Session], + meter_provider: Optional[MeterProvider], + signal_config: _SignalConfig, + ): + self._shutdown_in_progress = threading.Event() + self._endpoint = endpoint or environ.get( + signal_config.endpoint_envvar, + _append_signal_path( + environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT), + signal_config.default_export_path, + ), + ) + self._certificate_file = certificate_file or environ.get( + signal_config.certificate_envvar, + environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True), + ) + self._client_key_file = client_key_file or environ.get( + signal_config.client_key_envvar, + environ.get(OTEL_EXPORTER_OTLP_CLIENT_KEY, None), + ) + self._client_certificate_file = client_certificate_file or environ.get( + signal_config.client_certificate_envvar, + environ.get(OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, None), + ) + self._client_cert = ( + (self._client_certificate_file, self._client_key_file) + if self._client_certificate_file and self._client_key_file + else self._client_certificate_file + ) + headers_string = environ.get( + signal_config.headers_envvar, + environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), + ) + self._headers = headers or parse_env_headers( + headers_string, liberal=True + ) + self._timeout = timeout or float( + environ.get( + signal_config.timeout_envvar, + environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT), + ) + ) + self._compression = compression or _compression_from_env( + signal_config.compression_envvar + ) + self._session = _setup_session( + session, + signal_config.credential_envvar, + self._headers, + self._compression, + ) + self._shutdown = False + self._metrics = ExporterMetrics( + signal_config.component_type, + signal_config.signal_name, + urlparse(self._endpoint), + meter_provider, + ) 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 b7bab8b438..a44924839c 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,24 +13,19 @@ # limitations under the License. import logging -import threading -from os import environ +import warnings from typing import Dict, Optional, Sequence -from urllib.parse import urlparse import requests -from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( - ExporterMetrics, -) from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs from opentelemetry.exporter.otlp.proto.http import ( Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( - _compression_from_env, + OTLPHttpClient, + _SignalConfig, _export_with_retries, - _setup_session, ) from opentelemetry.metrics import MeterProvider from opentelemetry.sdk._logs import ReadableLogRecord @@ -41,11 +36,6 @@ from opentelemetry.sdk._shared_internal import DuplicateFilter from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY, - OTEL_EXPORTER_OTLP_ENDPOINT, - OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, @@ -53,25 +43,33 @@ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_HEADERS, OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, - OTEL_EXPORTER_OTLP_TIMEOUT, ) from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) -from opentelemetry.util.re import parse_env_headers _logger = logging.getLogger(__name__) # This prevents logs generated when a log fails to be written to generate another log which fails to be written etc. etc. _logger.addFilter(DuplicateFilter()) - -DEFAULT_COMPRESSION = Compression.NoCompression -DEFAULT_ENDPOINT = "http://localhost:4318/" -DEFAULT_TIMEOUT = 10 # in seconds DEFAULT_LOGS_EXPORT_PATH = "v1/logs" +_LOGS_CONFIG = _SignalConfig( + endpoint_envvar=OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + certificate_envvar=OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + client_key_envvar=OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, + client_certificate_envvar=OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, + headers_envvar=OTEL_EXPORTER_OTLP_LOGS_HEADERS, + timeout_envvar=OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, + compression_envvar=OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, + credential_envvar=_OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER, + default_export_path=DEFAULT_LOGS_EXPORT_PATH, + component_type=OtelComponentTypeValues.OTLP_HTTP_LOG_EXPORTER, + signal_name="logs", +) -class OTLPLogExporter(LogRecordExporter): + +class OTLPLogExporter(OTLPHttpClient, LogRecordExporter): def __init__( self, endpoint: Optional[str] = None, @@ -85,60 +83,18 @@ def __init__( *, meter_provider: Optional[MeterProvider] = None, ): - self._shutdown_in_progress = threading.Event() - self._endpoint = endpoint or environ.get( - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, - _append_logs_path( - environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT) - ), - ) - # Keeping these as instance variables because they are used in tests - self._certificate_file = certificate_file or environ.get( - OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, - environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True), - ) - self._client_key_file = client_key_file or environ.get( - OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, - environ.get(OTEL_EXPORTER_OTLP_CLIENT_KEY, None), - ) - self._client_certificate_file = client_certificate_file or environ.get( - OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, - environ.get(OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, None), - ) - self._client_cert = ( - (self._client_certificate_file, self._client_key_file) - if self._client_certificate_file and self._client_key_file - else self._client_certificate_file - ) - headers_string = environ.get( - OTEL_EXPORTER_OTLP_LOGS_HEADERS, - environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), - ) - self._headers = headers or parse_env_headers( - headers_string, liberal=True - ) - self._timeout = timeout or float( - environ.get( - OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, - environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT), - ) - ) - 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, - self._headers, - self._compression, - ) - self._shutdown = False - - self._metrics = ExporterMetrics( - OtelComponentTypeValues.OTLP_HTTP_LOG_EXPORTER, - "logs", - urlparse(self._endpoint), - meter_provider, + OTLPHttpClient.__init__( + self, + endpoint=endpoint, + certificate_file=certificate_file, + client_key_file=client_key_file, + client_certificate_file=client_certificate_file, + headers=headers, + timeout=timeout, + compression=compression, + session=session, + meter_provider=meter_provider, + signal_config=_LOGS_CONFIG, ) def export( @@ -181,7 +137,20 @@ def shutdown(self): self._session.close() -def _append_logs_path(endpoint: str) -> str: - if endpoint.endswith("/"): - return endpoint + DEFAULT_LOGS_EXPORT_PATH - return endpoint + f"/{DEFAULT_LOGS_EXPORT_PATH}" +_DEPRECATED_CONSTANTS = { + "DEFAULT_COMPRESSION": Compression.NoCompression, + "DEFAULT_ENDPOINT": "http://localhost:4318/", + "DEFAULT_TIMEOUT": 10, +} + + +def __getattr__(name: str) -> object: + if name in _DEPRECATED_CONSTANTS: + warnings.warn( + f"{name} is deprecated. Use the constant from " + f"opentelemetry.exporter.otlp.proto.http._common instead.", + DeprecationWarning, + stacklevel=2, + ) + return _DEPRECATED_CONSTANTS[name] + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") 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 7c5838d2ae..e7dd2aff89 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,8 +13,7 @@ from __future__ import annotations import logging -import threading -from os import environ +import warnings from typing import ( # noqa: F401 Any, Callable, @@ -44,9 +43,9 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( - _compression_from_env, + OTLPHttpClient, + _SignalConfig, _export_with_retries, - _setup_session, ) from opentelemetry.metrics import MeterProvider from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( # noqa: F401 @@ -66,11 +65,6 @@ ) from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY, - OTEL_EXPORTER_OTLP_ENDPOINT, - OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, @@ -78,7 +72,6 @@ OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, - OTEL_EXPORTER_OTLP_TIMEOUT, ) from opentelemetry.sdk.metrics._internal.aggregation import Aggregation from opentelemetry.sdk.metrics.export import ( # noqa: F401 @@ -96,18 +89,27 @@ from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) -from opentelemetry.util.re import parse_env_headers _logger = logging.getLogger(__name__) - -DEFAULT_COMPRESSION = Compression.NoCompression -DEFAULT_ENDPOINT = "http://localhost:4318/" -DEFAULT_TIMEOUT = 10 # in seconds DEFAULT_METRICS_EXPORT_PATH = "v1/metrics" +_METRICS_CONFIG = _SignalConfig( + endpoint_envvar=OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, + certificate_envvar=OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, + client_key_envvar=OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, + client_certificate_envvar=OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, + headers_envvar=OTEL_EXPORTER_OTLP_METRICS_HEADERS, + timeout_envvar=OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, + compression_envvar=OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, + credential_envvar=_OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER, + default_export_path=DEFAULT_METRICS_EXPORT_PATH, + component_type=OtelComponentTypeValues.OTLP_HTTP_METRIC_EXPORTER, + signal_name="metrics", +) + -class OTLPMetricExporter(MetricExporter, OTLPMetricExporterMixin): +class OTLPMetricExporter(OTLPHttpClient, MetricExporter, OTLPMetricExporterMixin): def __init__( self, endpoint: str | None = None, @@ -146,65 +148,21 @@ def __init__( If not set there is no limit to the number of data points in a request. If it is set and the number of data points exceeds the max, the request will be split. """ - self._shutdown_in_progress = threading.Event() - self._endpoint = endpoint or environ.get( - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, - _append_metrics_path( - environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT) - ), - ) - self._certificate_file = certificate_file or environ.get( - OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, - environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True), - ) - self._client_key_file = client_key_file or environ.get( - OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, - environ.get(OTEL_EXPORTER_OTLP_CLIENT_KEY, None), - ) - self._client_certificate_file = client_certificate_file or environ.get( - OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, - environ.get(OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, None), - ) - self._client_cert = ( - (self._client_certificate_file, self._client_key_file) - if self._client_certificate_file and self._client_key_file - else self._client_certificate_file - ) - headers_string = environ.get( - OTEL_EXPORTER_OTLP_METRICS_HEADERS, - environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), - ) - self._headers = headers or parse_env_headers( - headers_string, liberal=True - ) - self._timeout = timeout or float( - environ.get( - OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, - environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT), - ) - ) - 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, - self._headers, - self._compression, - ) - - self._common_configuration( - preferred_temporality, preferred_aggregation + OTLPHttpClient.__init__( + self, + endpoint=endpoint, + certificate_file=certificate_file, + client_key_file=client_key_file, + client_certificate_file=client_certificate_file, + headers=headers, + timeout=timeout, + compression=compression, + session=session, + meter_provider=meter_provider, + signal_config=_METRICS_CONFIG, ) + self._common_configuration(preferred_temporality, preferred_aggregation) self._max_export_batch_size: int | None = max_export_batch_size - self._shutdown = False - - self._metrics = ExporterMetrics( - OtelComponentTypeValues.OTLP_HTTP_METRIC_EXPORTER, - "metrics", - urlparse(self._endpoint), - meter_provider, - ) def export( self, @@ -603,7 +561,20 @@ def get_resource_data( return _get_resource_data(sdk_resource_scope_data, resource_class, name) -def _append_metrics_path(endpoint: str) -> str: - if endpoint.endswith("/"): - return endpoint + DEFAULT_METRICS_EXPORT_PATH - return endpoint + f"/{DEFAULT_METRICS_EXPORT_PATH}" +_DEPRECATED_CONSTANTS = { + "DEFAULT_COMPRESSION": Compression.NoCompression, + "DEFAULT_ENDPOINT": "http://localhost:4318/", + "DEFAULT_TIMEOUT": 10, +} + + +def __getattr__(name: str) -> object: + if name in _DEPRECATED_CONSTANTS: + warnings.warn( + f"{name} is deprecated. Use the constant from " + f"opentelemetry.exporter.otlp.proto.http._common instead.", + DeprecationWarning, + stacklevel=2, + ) + return _DEPRECATED_CONSTANTS[name] + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") 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 97aada9e27..d4461ff524 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,16 +13,11 @@ # limitations under the License. import logging -import threading -from os import environ +import warnings from typing import Dict, Optional, Sequence -from urllib.parse import urlparse import requests -from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( - ExporterMetrics, -) from opentelemetry.exporter.otlp.proto.common.trace_encoder import ( encode_spans, ) @@ -30,19 +25,13 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( - _compression_from_env, + OTLPHttpClient, + _SignalConfig, _export_with_retries, - _setup_session, ) from opentelemetry.metrics import MeterProvider from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY, - OTEL_EXPORTER_OTLP_ENDPOINT, - OTEL_EXPORTER_OTLP_HEADERS, - OTEL_EXPORTER_OTLP_TIMEOUT, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, @@ -56,18 +45,27 @@ from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) -from opentelemetry.util.re import parse_env_headers _logger = logging.getLogger(__name__) - -DEFAULT_COMPRESSION = Compression.NoCompression -DEFAULT_ENDPOINT = "http://localhost:4318/" -DEFAULT_TIMEOUT = 10 # in seconds DEFAULT_TRACES_EXPORT_PATH = "v1/traces" +_TRACES_CONFIG = _SignalConfig( + endpoint_envvar=OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + certificate_envvar=OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, + client_key_envvar=OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, + client_certificate_envvar=OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, + headers_envvar=OTEL_EXPORTER_OTLP_TRACES_HEADERS, + timeout_envvar=OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, + compression_envvar=OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, + credential_envvar=_OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER, + default_export_path=DEFAULT_TRACES_EXPORT_PATH, + component_type=OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER, + signal_name="traces", +) -class OTLPSpanExporter(SpanExporter): + +class OTLPSpanExporter(OTLPHttpClient, SpanExporter): def __init__( self, endpoint: Optional[str] = None, @@ -81,59 +79,18 @@ def __init__( *, meter_provider: Optional[MeterProvider] = None, ): - self._shutdown_in_progress = threading.Event() - self._endpoint = endpoint or environ.get( - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, - _append_trace_path( - environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT) - ), - ) - self._certificate_file = certificate_file or environ.get( - OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, - environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True), - ) - self._client_key_file = client_key_file or environ.get( - OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, - environ.get(OTEL_EXPORTER_OTLP_CLIENT_KEY, None), - ) - self._client_certificate_file = client_certificate_file or environ.get( - OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, - environ.get(OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, None), - ) - self._client_cert = ( - (self._client_certificate_file, self._client_key_file) - if self._client_certificate_file and self._client_key_file - else self._client_certificate_file - ) - headers_string = environ.get( - OTEL_EXPORTER_OTLP_TRACES_HEADERS, - environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), - ) - self._headers = headers or parse_env_headers( - headers_string, liberal=True - ) - self._timeout = timeout or float( - environ.get( - OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, - environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT), - ) - ) - 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, - self._headers, - self._compression, - ) - self._shutdown = False - - self._metrics = ExporterMetrics( - OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER, - "traces", - urlparse(self._endpoint), - meter_provider, + OTLPHttpClient.__init__( + self, + endpoint=endpoint, + certificate_file=certificate_file, + client_key_file=client_key_file, + client_certificate_file=client_certificate_file, + headers=headers, + timeout=timeout, + compression=compression, + session=session, + meter_provider=meter_provider, + signal_config=_TRACES_CONFIG, ) def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: @@ -172,7 +129,20 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: return True -def _append_trace_path(endpoint: str) -> str: - if endpoint.endswith("/"): - return endpoint + DEFAULT_TRACES_EXPORT_PATH - return endpoint + f"/{DEFAULT_TRACES_EXPORT_PATH}" +_DEPRECATED_CONSTANTS = { + "DEFAULT_COMPRESSION": Compression.NoCompression, + "DEFAULT_ENDPOINT": "http://localhost:4318/", + "DEFAULT_TIMEOUT": 10, +} + + +def __getattr__(name: str) -> object: + if name in _DEPRECATED_CONSTANTS: + warnings.warn( + f"{name} is deprecated. Use the constant from " + f"opentelemetry.exporter.otlp.proto.http._common instead.", + DeprecationWarning, + stacklevel=2, + ) + return _DEPRECATED_CONSTANTS[name] + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") 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 c95a733f0b..c8008f6b77 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 a13e9f8da9..7e4be28ee0 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 546d163bf8..2f5f9c9b2a 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 cb3c11c65910a4a9e625f68b1ccaa48142a1bbd6 Mon Sep 17 00:00:00 2001 From: Lorenzo Ronzani Date: Thu, 30 Apr 2026 12:02:47 +0000 Subject: [PATCH 10/17] rexported public constants --- .../otlp/proto/http/_log_exporter/__init__.py | 23 +++---------------- .../proto/http/metric_exporter/__init__.py | 23 +++---------------- .../proto/http/trace_exporter/__init__.py | 23 +++---------------- 3 files changed, 9 insertions(+), 60 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 a44924839c..c129c1c8d6 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,7 +13,6 @@ # limitations under the License. import logging -import warnings from typing import Dict, Optional, Sequence import requests @@ -23,6 +22,9 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( + DEFAULT_COMPRESSION, + DEFAULT_ENDPOINT, + DEFAULT_TIMEOUT, OTLPHttpClient, _SignalConfig, _export_with_retries, @@ -135,22 +137,3 @@ def shutdown(self): self._shutdown = True self._shutdown_in_progress.set() self._session.close() - - -_DEPRECATED_CONSTANTS = { - "DEFAULT_COMPRESSION": Compression.NoCompression, - "DEFAULT_ENDPOINT": "http://localhost:4318/", - "DEFAULT_TIMEOUT": 10, -} - - -def __getattr__(name: str) -> object: - if name in _DEPRECATED_CONSTANTS: - warnings.warn( - f"{name} is deprecated. Use the constant from " - f"opentelemetry.exporter.otlp.proto.http._common instead.", - DeprecationWarning, - stacklevel=2, - ) - return _DEPRECATED_CONSTANTS[name] - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") 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 e7dd2aff89..c1666736a5 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,7 +13,6 @@ from __future__ import annotations import logging -import warnings from typing import ( # noqa: F401 Any, Callable, @@ -43,6 +42,9 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( + DEFAULT_COMPRESSION, + DEFAULT_ENDPOINT, + DEFAULT_TIMEOUT, OTLPHttpClient, _SignalConfig, _export_with_retries, @@ -559,22 +561,3 @@ def get_resource_data( name: str, ) -> List[PB2Resource]: return _get_resource_data(sdk_resource_scope_data, resource_class, name) - - -_DEPRECATED_CONSTANTS = { - "DEFAULT_COMPRESSION": Compression.NoCompression, - "DEFAULT_ENDPOINT": "http://localhost:4318/", - "DEFAULT_TIMEOUT": 10, -} - - -def __getattr__(name: str) -> object: - if name in _DEPRECATED_CONSTANTS: - warnings.warn( - f"{name} is deprecated. Use the constant from " - f"opentelemetry.exporter.otlp.proto.http._common instead.", - DeprecationWarning, - stacklevel=2, - ) - return _DEPRECATED_CONSTANTS[name] - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") 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 d4461ff524..340cab5257 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,7 +13,6 @@ # limitations under the License. import logging -import warnings from typing import Dict, Optional, Sequence import requests @@ -25,6 +24,9 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( + DEFAULT_COMPRESSION, + DEFAULT_ENDPOINT, + DEFAULT_TIMEOUT, OTLPHttpClient, _SignalConfig, _export_with_retries, @@ -127,22 +129,3 @@ def shutdown(self): def force_flush(self, timeout_millis: int = 30000) -> bool: """Nothing is buffered in this exporter, so this method does nothing.""" return True - - -_DEPRECATED_CONSTANTS = { - "DEFAULT_COMPRESSION": Compression.NoCompression, - "DEFAULT_ENDPOINT": "http://localhost:4318/", - "DEFAULT_TIMEOUT": 10, -} - - -def __getattr__(name: str) -> object: - if name in _DEPRECATED_CONSTANTS: - warnings.warn( - f"{name} is deprecated. Use the constant from " - f"opentelemetry.exporter.otlp.proto.http._common instead.", - DeprecationWarning, - stacklevel=2, - ) - return _DEPRECATED_CONSTANTS[name] - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") From 250d7b9ef209496245619c29de8a97f7d4de82bf Mon Sep 17 00:00:00 2001 From: Lorenzo Ronzani Date: Thu, 30 Apr 2026 12:04:58 +0000 Subject: [PATCH 11/17] formatted and linted --- .../otlp/proto/http/_log_exporter/__init__.py | 5 +---- .../otlp/proto/http/metric_exporter/__init__.py | 13 +++++++------ .../otlp/proto/http/trace_exporter/__init__.py | 5 +---- 3 files changed, 9 insertions(+), 14 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 c129c1c8d6..89d6e16f01 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 @@ -22,12 +22,9 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( - DEFAULT_COMPRESSION, - DEFAULT_ENDPOINT, - DEFAULT_TIMEOUT, OTLPHttpClient, - _SignalConfig, _export_with_retries, + _SignalConfig, ) from opentelemetry.metrics import MeterProvider from opentelemetry.sdk._logs import ReadableLogRecord 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 c1666736a5..689bca19d6 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 @@ -42,12 +42,9 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( - DEFAULT_COMPRESSION, - DEFAULT_ENDPOINT, - DEFAULT_TIMEOUT, OTLPHttpClient, - _SignalConfig, _export_with_retries, + _SignalConfig, ) from opentelemetry.metrics import MeterProvider from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( # noqa: F401 @@ -111,7 +108,9 @@ ) -class OTLPMetricExporter(OTLPHttpClient, MetricExporter, OTLPMetricExporterMixin): +class OTLPMetricExporter( + OTLPHttpClient, MetricExporter, OTLPMetricExporterMixin +): def __init__( self, endpoint: str | None = None, @@ -163,7 +162,9 @@ def __init__( meter_provider=meter_provider, signal_config=_METRICS_CONFIG, ) - self._common_configuration(preferred_temporality, preferred_aggregation) + self._common_configuration( + preferred_temporality, preferred_aggregation + ) self._max_export_batch_size: int | None = max_export_batch_size def export( 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 340cab5257..f6659864a5 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 @@ -24,12 +24,9 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( - DEFAULT_COMPRESSION, - DEFAULT_ENDPOINT, - DEFAULT_TIMEOUT, OTLPHttpClient, - _SignalConfig, _export_with_retries, + _SignalConfig, ) from opentelemetry.metrics import MeterProvider from opentelemetry.sdk.environment_variables import ( From 2aa0053968d9b957c7dea605fb92dd640f89b948 Mon Sep 17 00:00:00 2001 From: Lorenzo Ronzani Date: Thu, 30 Apr 2026 12:27:30 +0000 Subject: [PATCH 12/17] Moved private methods inside parent class --- .../otlp/proto/http/_log_exporter/__init__.py | 14 +------------- .../otlp/proto/http/metric_exporter/__init__.py | 14 +------------- .../otlp/proto/http/trace_exporter/__init__.py | 14 +------------- .../tests/metrics/test_otlp_metrics_exporter.py | 2 +- .../tests/test_proto_log_exporter.py | 2 +- .../tests/test_proto_span_exporter.py | 2 +- 6 files changed, 6 insertions(+), 42 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 89d6e16f01..51a6f84692 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 @@ -23,7 +23,6 @@ ) from opentelemetry.exporter.otlp.proto.http._common import ( OTLPHttpClient, - _export_with_retries, _SignalConfig, ) from opentelemetry.metrics import MeterProvider @@ -105,18 +104,7 @@ def export( serialized_data = encode_logs(batch).SerializeToString() with self._metrics.export_operation(len(batch)) as result: - 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, - "logs", - ) + success = self._export_with_retries(serialized_data, result, "logs") return ( LogRecordExportResult.SUCCESS if success 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 689bca19d6..e50bca576b 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 @@ -43,7 +43,6 @@ ) from opentelemetry.exporter.otlp.proto.http._common import ( OTLPHttpClient, - _export_with_retries, _SignalConfig, ) from opentelemetry.metrics import MeterProvider @@ -188,18 +187,7 @@ def export( 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", - ) + return self._export_with_retries(serialized_data, result, "metrics") # If no batch size configured, export as single batch with retries as configured if self._max_export_batch_size is None: 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 f6659864a5..d2f487f5f1 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 @@ -25,7 +25,6 @@ ) from opentelemetry.exporter.otlp.proto.http._common import ( OTLPHttpClient, - _export_with_retries, _SignalConfig, ) from opentelemetry.metrics import MeterProvider @@ -99,18 +98,7 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: serialized_data = encode_spans(spans).SerializePartialToString() with self._metrics.export_operation(len(spans)) as result: - 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", - ) + success = self._export_with_retries(serialized_data, result, "span") return ( SpanExportResult.SUCCESS if success else SpanExportResult.FAILURE ) 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 c8008f6b77..048f92c1b5 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 @@ -1268,7 +1268,7 @@ def test_exponential_explicit_bucket_histogram(self): ) @patch( - "opentelemetry.exporter.otlp.proto.http._common._export", + "opentelemetry.exporter.otlp.proto.http._common.OTLPHttpClient._export", return_value=Mock(ok=True), ) def test_2xx_status_code(self, mock_otlp_metric_exporter): 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 7e4be28ee0..854efbca91 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 @@ -459,7 +459,7 @@ def _get_sdk_log_data() -> List[ReadWriteLogRecord]: return [log1, log2, log3, log4] @patch( - "opentelemetry.exporter.otlp.proto.http._common._export", + "opentelemetry.exporter.otlp.proto.http._common.OTLPHttpClient._export", return_value=Mock(ok=True), ) def test_2xx_status_code(self, mock_otlp_metric_exporter): 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 2f5f9c9b2a..7881bd4315 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 @@ -280,7 +280,7 @@ def test_headers_parse_from_env(self): ) @patch( - "opentelemetry.exporter.otlp.proto.http._common._export", + "opentelemetry.exporter.otlp.proto.http._common.OTLPHttpClient._export", return_value=Mock(ok=True), ) def test_2xx_status_code(self, mock_otlp_metric_exporter): From 09eec1d80edab5c2c546592f3b90e0b692fd1eb1 Mon Sep 17 00:00:00 2001 From: Lorenzo Ronzani Date: Thu, 30 Apr 2026 12:35:50 +0000 Subject: [PATCH 13/17] linterd and formatted --- .../otlp/proto/http/_common/__init__.py | 285 ++++++++---------- .../otlp/proto/http/_log_exporter/__init__.py | 12 +- .../proto/http/metric_exporter/__init__.py | 12 +- .../proto/http/trace_exporter/__init__.py | 12 +- 4 files changed, 143 insertions(+), 178 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 d054871be7..fe44a57f4e 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 @@ -21,7 +21,7 @@ from io import BytesIO from os import environ from time import time -from typing import Any, Dict, Optional, Tuple, Union +from typing import Any, Dict, Optional from urllib.parse import urlparse import requests @@ -102,153 +102,10 @@ def _load_session_from_envvar( return None -def _setup_session( - session: Optional[requests.Session], - cred_envvar: str, - 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 - - -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 - - -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_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 - 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_RETRIES - 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 - - -def _compression_from_env(signal_compression_envvar: str) -> Compression: +def _compression_from_env(compression_envvar: str) -> Compression: compression = ( environ.get( - signal_compression_envvar, + compression_envvar, environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none"), ) .lower() @@ -333,11 +190,8 @@ def __init__( self._compression = compression or _compression_from_env( signal_config.compression_envvar ) - self._session = _setup_session( - session, - signal_config.credential_envvar, - self._headers, - self._compression, + self._session = self._setup_session( + session, signal_config.credential_envvar ) self._shutdown = False self._metrics = ExporterMetrics( @@ -346,3 +200,132 @@ def __init__( urlparse(self._endpoint), meter_provider, ) + + def _setup_session( + self, + session: Optional[requests.Session], + cred_envvar: str, + ) -> requests.Session: + configured_session = ( + session + or _load_session_from_envvar(cred_envvar) + or requests.Session() + ) + configured_session.headers.update(self._headers) + configured_session.headers.update(_OTLP_HTTP_HEADERS) + # let users override our defaults + configured_session.headers.update(self._headers) + if self._compression is not Compression.NoCompression: + configured_session.headers.update( + {"Content-Encoding": self._compression.value} + ) + return configured_session + + def _export( + self, serialized_data: bytes, timeout_sec: float + ) -> requests.Response: + 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) + + # 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, + serialized_data: bytes, + result: Any, + batch_name: str, + ) -> bool: + deadline_sec = time() + self._timeout + 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 + try: + resp = self._export(serialized_data, 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_RETRIES + or backoff_seconds > (deadline_sec - time()) + or self._shutdown_in_progress.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 self._shutdown_in_progress.wait(backoff_seconds): + _logger.warning("Shutdown in progress, aborting retry.") + break + return False + + def shutdown(self, timeout_millis: Optional[float] = None) -> None: + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring call") + return + self._shutdown = True + self._shutdown_in_progress.set() + self._session.close() 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 51a6f84692..14b5d5cd6d 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 @@ -104,7 +104,9 @@ def export( serialized_data = encode_logs(batch).SerializeToString() with self._metrics.export_operation(len(batch)) as result: - success = self._export_with_retries(serialized_data, result, "logs") + success = self._export_with_retries( + serialized_data, result, "logs" + ) return ( LogRecordExportResult.SUCCESS if success @@ -114,11 +116,3 @@ def export( def force_flush(self, timeout_millis: float = 10_000) -> bool: """Nothing is buffered in this exporter, so this method does nothing.""" return True - - def shutdown(self): - if self._shutdown: - _logger.warning("Exporter already shutdown, ignoring call") - return - self._shutdown = True - self._shutdown_in_progress.set() - self._session.close() 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 e50bca576b..47fb5ff6e9 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 @@ -187,7 +187,9 @@ def export( def _do_export(request: ExportMetricsServiceRequest) -> bool: serialized_data = request.SerializeToString() with self._metrics.export_operation(num_items) as result: - return self._export_with_retries(serialized_data, result, "metrics") + return self._export_with_retries( + serialized_data, result, "metrics" + ) # If no batch size configured, export as single batch with retries as configured if self._max_export_batch_size is None: @@ -207,14 +209,6 @@ def _do_export(request: ExportMetricsServiceRequest) -> bool: # Only returns SUCCESS if all batches succeeded return MetricExportResult.SUCCESS - def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: - if self._shutdown: - _logger.warning("Exporter already shutdown, ignoring call") - return - self._shutdown = True - self._shutdown_in_progress.set() - self._session.close() - @property def _exporting(self) -> str: return "metrics" 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 d2f487f5f1..7edf5a2e36 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 @@ -98,19 +98,13 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: serialized_data = encode_spans(spans).SerializePartialToString() with self._metrics.export_operation(len(spans)) as result: - success = self._export_with_retries(serialized_data, result, "span") + success = self._export_with_retries( + serialized_data, result, "span" + ) return ( SpanExportResult.SUCCESS if success else SpanExportResult.FAILURE ) - def shutdown(self): - if self._shutdown: - _logger.warning("Exporter already shutdown, ignoring call") - return - self._shutdown = True - self._shutdown_in_progress.set() - self._session.close() - def force_flush(self, timeout_millis: int = 30000) -> bool: """Nothing is buffered in this exporter, so this method does nothing.""" return True From 0d9175dd6bde5977ee4bf94125bc28a4be351bc4 Mon Sep 17 00:00:00 2001 From: Lorenzo Ronzani Date: Thu, 30 Apr 2026 12:41:39 +0000 Subject: [PATCH 14/17] linted and formatted --- .../exporter/otlp/proto/http/_common/__init__.py | 7 +++++-- .../otlp/proto/http/_log_exporter/__init__.py | 11 ++++------- .../otlp/proto/http/trace_exporter/__init__.py | 9 ++++----- 3 files changed, 13 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 fe44a57f4e..af4f364f62 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 @@ -267,8 +267,11 @@ 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()) - if resp.ok: + if ( + resp := self._export( + serialized_data, deadline_sec - time() + ) + ).ok: return True except requests.exceptions.RequestException as error: reason = error 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 14b5d5cd6d..b4238ee108 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 @@ -104,14 +104,11 @@ def export( serialized_data = encode_logs(batch).SerializeToString() with self._metrics.export_operation(len(batch)) as result: - success = self._export_with_retries( - serialized_data, result, "logs" + return ( + LogRecordExportResult.SUCCESS + if self._export_with_retries(serialized_data, result, "logs") + 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/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 7edf5a2e36..7951452990 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 @@ -98,12 +98,11 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: serialized_data = encode_spans(spans).SerializePartialToString() with self._metrics.export_operation(len(spans)) as result: - success = self._export_with_retries( - serialized_data, result, "span" + return ( + SpanExportResult.SUCCESS + if self._export_with_retries(serialized_data, result, "span") + else SpanExportResult.FAILURE ) - return ( - SpanExportResult.SUCCESS if success else SpanExportResult.FAILURE - ) def force_flush(self, timeout_millis: int = 30000) -> bool: """Nothing is buffered in this exporter, so this method does nothing.""" From 29c7840be29beff8611060e309be72a147d22776 Mon Sep 17 00:00:00 2001 From: Lorenzo Ronzani Date: Thu, 30 Apr 2026 12:42:44 +0000 Subject: [PATCH 15/17] try --- .../exporter/otlp/proto/http/_log_exporter/__init__.py | 1 + 1 file changed, 1 insertion(+) 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 b4238ee108..f1237601ef 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 @@ -22,6 +22,7 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( + DEFAULT_COMPRESSION, OTLPHttpClient, _SignalConfig, ) From 338021506fa77d942887b39fc18e257e361a6f58 Mon Sep 17 00:00:00 2001 From: Lorenzo Ronzani Date: Thu, 30 Apr 2026 12:44:09 +0000 Subject: [PATCH 16/17] re export public consts --- .../exporter/otlp/proto/http/_log_exporter/__init__.py | 2 ++ .../exporter/otlp/proto/http/metric_exporter/__init__.py | 3 +++ .../exporter/otlp/proto/http/trace_exporter/__init__.py | 3 +++ 3 files changed, 8 insertions(+) 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 f1237601ef..a3ff0d192f 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 @@ -23,6 +23,8 @@ ) from opentelemetry.exporter.otlp.proto.http._common import ( DEFAULT_COMPRESSION, + DEFAULT_ENDPOINT, + DEFAULT_TIMEOUT, OTLPHttpClient, _SignalConfig, ) 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 47fb5ff6e9..deae0ed5e5 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 @@ -42,6 +42,9 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( + DEFAULT_COMPRESSION, + DEFAULT_ENDPOINT, + DEFAULT_TIMEOUT, OTLPHttpClient, _SignalConfig, ) 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 7951452990..645f36e8ec 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 @@ -24,6 +24,9 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( + DEFAULT_COMPRESSION, + DEFAULT_ENDPOINT, + DEFAULT_TIMEOUT, OTLPHttpClient, _SignalConfig, ) From 88fcdcb6139c822fafb47577a567ba32bde79e0b Mon Sep 17 00:00:00 2001 From: Lorenzo Ronzani Date: Thu, 30 Apr 2026 12:47:47 +0000 Subject: [PATCH 17/17] removed public constants --- .../exporter/otlp/proto/http/_log_exporter/__init__.py | 3 --- .../exporter/otlp/proto/http/metric_exporter/__init__.py | 3 --- .../exporter/otlp/proto/http/trace_exporter/__init__.py | 3 --- 3 files changed, 9 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 a3ff0d192f..b4238ee108 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 @@ -22,9 +22,6 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( - DEFAULT_COMPRESSION, - DEFAULT_ENDPOINT, - DEFAULT_TIMEOUT, OTLPHttpClient, _SignalConfig, ) 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 deae0ed5e5..47fb5ff6e9 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 @@ -42,9 +42,6 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( - DEFAULT_COMPRESSION, - DEFAULT_ENDPOINT, - DEFAULT_TIMEOUT, OTLPHttpClient, _SignalConfig, ) 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 645f36e8ec..7951452990 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 @@ -24,9 +24,6 @@ Compression, ) from opentelemetry.exporter.otlp.proto.http._common import ( - DEFAULT_COMPRESSION, - DEFAULT_ENDPOINT, - DEFAULT_TIMEOUT, OTLPHttpClient, _SignalConfig, )