From f0acaaa7d7432137652f5e456143c6798893b07e Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Mon, 16 Feb 2026 23:38:52 -0500 Subject: [PATCH 1/9] feat: make retryable gRPC error codes configurable for gRPC exporters --- .../otlp/proto/grpc/_log_exporter/__init__.py | 6 ++- .../exporter/otlp/proto/grpc/exporter.py | 11 +++- .../proto/grpc/metric_exporter/__init__.py | 4 +- .../proto/grpc/trace_exporter/__init__.py | 5 +- .../tests/test_otlp_exporter_mixin.py | 51 ++++++++++++++++++- 5 files changed, 70 insertions(+), 7 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py index 63d8ac9cfb0..89b943e6b8a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py @@ -10,12 +10,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +from collections.abc import Iterable from os import environ from typing import Dict, Literal, Optional, Sequence, Tuple, Union from typing import Sequence as TypingSequence -from grpc import ChannelCredentials, Compression +from grpc import ChannelCredentials, Compression, StatusCode from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs from opentelemetry.exporter.otlp.proto.grpc.exporter import ( OTLPExporterMixin, @@ -66,6 +66,7 @@ def __init__( timeout: Optional[float] = None, compression: Optional[Compression] = None, channel_options: Optional[Tuple[Tuple[str, str]]] = None, + retryable_error_codes: Optional[Iterable[StatusCode]] = None, ): insecure_logs = environ.get(OTEL_EXPORTER_OTLP_LOGS_INSECURE) if insecure is None and insecure_logs is not None: @@ -105,6 +106,7 @@ def __init__( stub=LogsServiceStub, result=LogRecordExportResult, channel_options=channel_options, + retryable_error_codes=retryable_error_codes, ) def _translate_data( diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index 89c2608c30a..854d89e4d56 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -23,7 +23,7 @@ import random import threading from abc import ABC, abstractmethod -from collections.abc import Sequence # noqa: F401 +from collections.abc import Iterable, Sequence # noqa: F401 from logging import getLogger from os import environ from time import time @@ -299,6 +299,7 @@ def __init__( timeout: Optional[float] = None, compression: Optional[Compression] = None, channel_options: Optional[Tuple[Tuple[str, str]]] = None, + retryable_error_codes: Optional[Iterable[StatusCode]] = None, ): super().__init__() self._result = result @@ -357,6 +358,12 @@ def __init__( else compression ) or Compression.NoCompression + self._retryable_error_codes = ( + frozenset(retryable_error_codes) + if retryable_error_codes is not None + else _RETRYABLE_ERROR_CODES + ) + self._channel = None self._client = None @@ -460,7 +467,7 @@ def _export( self._initialize_channel_and_stub() if ( - error.code() not in _RETRYABLE_ERROR_CODES # type: ignore [reportAttributeAccessIssue] + error.code() not in self._retryable_error_codes # type: ignore [reportAttributeAccessIssue] or retry_num + 1 == _MAX_RETRYS or backoff_seconds > (deadline_sec - time()) or self._shutdown diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py index af77f6d1239..718f9f40761 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py @@ -19,7 +19,7 @@ from typing import Iterable, List, Tuple, Union from typing import Sequence as TypingSequence -from grpc import ChannelCredentials, Compression +from grpc import ChannelCredentials, Compression, StatusCode from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import ( OTLPMetricExporterMixin, ) @@ -109,6 +109,7 @@ def __init__( preferred_aggregation: dict[type, Aggregation] | None = None, max_export_batch_size: int | None = None, channel_options: Tuple[Tuple[str, str]] | None = None, + retryable_error_codes: Iterable[StatusCode] | None = None, ): insecure_metrics = environ.get(OTEL_EXPORTER_OTLP_METRICS_INSECURE) if insecure is None and insecure_metrics is not None: @@ -153,6 +154,7 @@ def __init__( timeout=timeout or environ_timeout, compression=compression, channel_options=channel_options, + retryable_error_codes=retryable_error_codes, ) self._max_export_batch_size: int | None = max_export_batch_size diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index 19b189e5b9c..bf6c080e25a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -14,11 +14,12 @@ """OTLP Span Exporter""" import logging +from collections.abc import Iterable from os import environ from typing import Dict, Optional, Sequence, Tuple, Union from typing import Sequence as TypingSequence -from grpc import ChannelCredentials, Compression +from grpc import ChannelCredentials, Compression, StatusCode from opentelemetry.exporter.otlp.proto.common.trace_encoder import ( encode_spans, ) @@ -95,6 +96,7 @@ def __init__( timeout: Optional[float] = None, compression: Optional[Compression] = None, channel_options: Optional[Tuple[Tuple[str, str]]] = None, + retryable_error_codes: Iterable[StatusCode] | None = None, ): insecure_spans = environ.get(OTEL_EXPORTER_OTLP_TRACES_INSECURE) if insecure is None and insecure_spans is not None: @@ -135,6 +137,7 @@ def __init__( timeout=timeout or environ_timeout, compression=compression, channel_options=channel_options, + retryable_error_codes=retryable_error_codes, ) def _translate_data( diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py index de27d0fe792..d1ab452652a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py @@ -37,6 +37,7 @@ encode_spans, ) from opentelemetry.exporter.otlp.proto.grpc.exporter import ( # noqa: F401 + _RETRYABLE_ERROR_CODES, InvalidCompressionValueException, OTLPExporterMixin, environ_to_compression, @@ -154,6 +155,7 @@ def join(self, timeout: Optional[float] = None) -> Any: return self._return +# pylint: disable-next=too-many-public-methods class TestOTLPExporterMixin(TestCase): def setUp(self): self.server = server(ThreadPoolExecutor(max_workers=10)) @@ -570,4 +572,51 @@ def test_unavailable_reconnects(self): # Since the initial channel was created in setUp (unpatched), this call # must be from the reconnection logic. self.assertTrue(mock_insecure_channel.called) - # Verify that reconnection enabled flag is set + + def test_retryable_error_codes_initialization(self): + # pylint: disable=protected-access + self.assertEqual( + self.exporter._retryable_error_codes, _RETRYABLE_ERROR_CODES + ) + custom_codes = [StatusCode.INTERNAL, StatusCode.UNKNOWN] + exporter = OTLPSpanExporterForTesting( + insecure=True, retryable_error_codes=custom_codes + ) + self.assertEqual( + exporter._retryable_error_codes, frozenset(custom_codes) + ) + + @unittest.skipIf( + system() == "Windows", + "For gRPC + windows there's some added delay in the RPCs which breaks the assertion over amount of time passed.", + ) + def test_retryable_error_codes_custom(self): + # Test that a custom error code is retried if specified + custom_codes = [StatusCode.INTERNAL] + mock_trace_service = TraceServiceServicerWithExportParams( + StatusCode.INTERNAL, + optional_retry_nanos=200000000, # .2 seconds + ) + add_TraceServiceServicer_to_server( + mock_trace_service, + self.server, + ) + exporter = OTLPSpanExporterForTesting( + insecure=True, retryable_error_codes=custom_codes, timeout=10 + ) + + self.assertEqual( + exporter.export([self.span]), + SpanExportResult.FAILURE, + ) + + self.assertEqual(mock_trace_service.num_requests, 6) + + # Test that a default retryable code is NOT retried if not in custom_codes + mock_trace_service.num_requests = 0 + mock_trace_service.export_result = StatusCode.UNAVAILABLE + self.assertEqual( + exporter.export([self.span]), + SpanExportResult.FAILURE, + ) + self.assertEqual(mock_trace_service.num_requests, 1) From 1a4e682e0a7aad8d7d63fd11bbf295796c11edce Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Tue, 17 Feb 2026 00:20:07 -0500 Subject: [PATCH 2/9] update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 540f7b9d347..74e61ad0fc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4973](https://github.com/open-telemetry/opentelemetry-python/pull/4973)) - `opentelemetry-exporter-prometheus`: Fix metric name prefix ([#4895](https://github.com/open-telemetry/opentelemetry-python/pull/4895)) +- `opentelemetry-exporter-otlp-proto-grpc`: make retryable gRPC error codes configurable for gRPC exporters + ([#4917](https://github.com/open-telemetry/opentelemetry-python/pull/4917)) ## Version 1.40.0/0.61b0 (2026-03-04) From 8e4537a360d2bf02520d73536434a214892ed66e Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Tue, 17 Feb 2026 00:22:34 -0500 Subject: [PATCH 3/9] fix typing in exporters --- .../exporter/otlp/proto/grpc/trace_exporter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index bf6c080e25a..1dd09b870ca 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -96,7 +96,7 @@ def __init__( timeout: Optional[float] = None, compression: Optional[Compression] = None, channel_options: Optional[Tuple[Tuple[str, str]]] = None, - retryable_error_codes: Iterable[StatusCode] | None = None, + retryable_error_codes: Optional[Iterable[StatusCode]] = None, ): insecure_spans = environ.get(OTEL_EXPORTER_OTLP_TRACES_INSECURE) if insecure is None and insecure_spans is not None: From 95b303c77955bf9b1385d9f8222e6a1e36e792eb Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Wed, 18 Feb 2026 15:34:37 -0500 Subject: [PATCH 4/9] add ability to configure OTLP gRPC retryable error codes via environment variables --- .../exporter/otlp/proto/grpc/exporter.py | 15 +++++++++++++-- .../otlp/proto/grpc/trace_exporter/__init__.py | 4 +++- .../tests/test_otlp_exporter_mixin.py | 18 ++++++++++++++++++ .../sdk/environment_variables/__init__.py | 12 ++++++++++++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index 854d89e4d56..9e28a966bb1 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -91,6 +91,7 @@ from opentelemetry.sdk._shared_internal import DuplicateFilter from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER, + _OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES, OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_KEY, @@ -299,7 +300,7 @@ def __init__( timeout: Optional[float] = None, compression: Optional[Compression] = None, channel_options: Optional[Tuple[Tuple[str, str]]] = None, - retryable_error_codes: Optional[Iterable[StatusCode]] = None, + retryable_error_codes: Optional[Union[Iterable[StatusCode]]] = None, ): super().__init__() self._result = result @@ -361,8 +362,18 @@ def __init__( self._retryable_error_codes = ( frozenset(retryable_error_codes) if retryable_error_codes is not None - else _RETRYABLE_ERROR_CODES + else environ.get( + _OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES + ) ) + if isinstance(self._retryable_error_codes, str): + self._retryable_error_codes = frozenset( + StatusCode[code.strip().upper()] + for code in self._retryable_error_codes.split(",") + if code.strip() + ) + if self._retryable_error_codes is None: + self._retryable_error_codes = _RETRYABLE_ERROR_CODES self._channel = None self._client = None diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index 1dd09b870ca..69a1ce9785b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -96,7 +96,9 @@ def __init__( timeout: Optional[float] = None, compression: Optional[Compression] = None, channel_options: Optional[Tuple[Tuple[str, str]]] = None, - retryable_error_codes: Optional[Iterable[StatusCode]] = None, + retryable_error_codes: Optional[ + Union[Iterable[StatusCode], str] + ] = None, ): insecure_spans = environ.get(OTEL_EXPORTER_OTLP_TRACES_INSECURE) if insecure is None and insecure_spans is not None: diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py index d1ab452652a..058ba4584f6 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py @@ -586,6 +586,24 @@ def test_retryable_error_codes_initialization(self): exporter._retryable_error_codes, frozenset(custom_codes) ) + @patch.dict( + "os.environ", + { + "OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES": ",INTERNAL, unknown,,,dEAdline_Exceeded " + }, + ) + def test_retryable_error_codes_initialization_from_env(self): + expected_codes = frozenset( + { + StatusCode.INTERNAL, + StatusCode.UNKNOWN, + StatusCode.DEADLINE_EXCEEDED, + } + ) + exporter = OTLPSpanExporterForTesting() + # pylint: disable=protected-access + self.assertEqual(exporter._retryable_error_codes, expected_codes) + @unittest.skipIf( system() == "Windows", "For gRPC + windows there's some added delay in the RPCs which breaks the assertion over amount of time passed.", diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index f049415a15b..2b5b9ca3de3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -494,6 +494,18 @@ def channel_credential_provider() -> grpc.ChannelCredentials: Note: This environment variable is experimental and subject to change. """ +_OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES = ( + "OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES" +) +""" +.. envvar:: OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES + +The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES` stores a comma-separated list of gRPC error codes +that are considered retryable for the OTLP gRPC exporters. + +Note: This environment variable is experimental and subject to change. +""" + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE = "OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE From 197c4dcabcec8f823ac67c3c57d5bf5091be070f Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Wed, 18 Feb 2026 15:36:56 -0500 Subject: [PATCH 5/9] update OTLP gRPC exporter constructor type hints --- .../exporter/otlp/proto/grpc/_log_exporter/__init__.py | 4 +++- .../exporter/otlp/proto/grpc/metric_exporter/__init__.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py index 89b943e6b8a..0101cafb97c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py @@ -66,7 +66,9 @@ def __init__( timeout: Optional[float] = None, compression: Optional[Compression] = None, channel_options: Optional[Tuple[Tuple[str, str]]] = None, - retryable_error_codes: Optional[Iterable[StatusCode]] = None, + retryable_error_codes: Optional[ + Union[Iterable[StatusCode], str] + ] = None, ): insecure_logs = environ.get(OTEL_EXPORTER_OTLP_LOGS_INSECURE) if insecure is None and insecure_logs is not None: diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py index 718f9f40761..c3674889fab 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py @@ -109,7 +109,7 @@ def __init__( preferred_aggregation: dict[type, Aggregation] | None = None, max_export_batch_size: int | None = None, channel_options: Tuple[Tuple[str, str]] | None = None, - retryable_error_codes: Iterable[StatusCode] | None = None, + retryable_error_codes: Union[Iterable[StatusCode], str] | None = None, ): insecure_metrics = environ.get(OTEL_EXPORTER_OTLP_METRICS_INSECURE) if insecure is None and insecure_metrics is not None: From 8636b5a8c3a8a15f5479f5296c1098f69d2b8694 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Wed, 18 Feb 2026 15:40:25 -0500 Subject: [PATCH 6/9] update retryable error codes initialization logic --- .../exporter/otlp/proto/grpc/exporter.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index 9e28a966bb1..f8acb6cb240 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -20,6 +20,7 @@ """ +import os import random import threading from abc import ABC, abstractmethod @@ -359,12 +360,8 @@ def __init__( else compression ) or Compression.NoCompression - self._retryable_error_codes = ( - frozenset(retryable_error_codes) - if retryable_error_codes is not None - else environ.get( - _OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES - ) + self._retryable_error_codes = retryable_error_codes or os.environ.get( + _OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES ) if isinstance(self._retryable_error_codes, str): self._retryable_error_codes = frozenset( @@ -372,7 +369,11 @@ def __init__( for code in self._retryable_error_codes.split(",") if code.strip() ) - if self._retryable_error_codes is None: + elif self._retryable_error_codes is not None: + self._retryable_error_codes = frozenset( + self._retryable_error_codes + ) + else: self._retryable_error_codes = _RETRYABLE_ERROR_CODES self._channel = None From bea8de39bef44f54c7b9042d4b8954f082e57bbc Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Wed, 18 Feb 2026 16:02:44 -0500 Subject: [PATCH 7/9] fix typechecking error --- .../src/opentelemetry/exporter/otlp/proto/grpc/exporter.py | 4 +++- .../src/opentelemetry/sdk/environment_variables/__init__.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index f8acb6cb240..a1a789c7d93 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -301,7 +301,9 @@ def __init__( timeout: Optional[float] = None, compression: Optional[Compression] = None, channel_options: Optional[Tuple[Tuple[str, str]]] = None, - retryable_error_codes: Optional[Union[Iterable[StatusCode]]] = None, + retryable_error_codes: Optional[ + Union[Iterable[StatusCode], str] + ] = None, ): super().__init__() self._result = result diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index 2b5b9ca3de3..38756f0df32 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -500,7 +500,7 @@ def channel_credential_provider() -> grpc.ChannelCredentials: """ .. envvar:: OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES -The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES` stores a comma-separated list of gRPC error codes +The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES` stores a comma-separated list of gRPC error codes that are considered retryable for the OTLP gRPC exporters. Note: This environment variable is experimental and subject to change. From 1993f7126ec22da066474ec1acefeb48c4e041f4 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Thu, 19 Feb 2026 15:38:53 -0500 Subject: [PATCH 8/9] update environment variables docstring --- .../src/opentelemetry/sdk/environment_variables/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index 38756f0df32..d6cdc575c7b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -500,8 +500,9 @@ def channel_credential_provider() -> grpc.ChannelCredentials: """ .. envvar:: OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES -The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES` stores a comma-separated list of gRPC error codes -that are considered retryable for the OTLP gRPC exporters. +The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES` stores a comma-separated list of human-readable +gRPC error codes that are considered retryable for the OTLP gRPC exporters (e.g. `UNAVAILABLE, DEADLINE_EXCEEDED`). +Supported error codes are defined in `grpc.StatusCode` and are parsed in a case-insensitive manner. Note: This environment variable is experimental and subject to change. """ From f0df87ef43a79afee44dfeff6393fdd83bc6aa13 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Thu, 19 Feb 2026 15:40:53 -0500 Subject: [PATCH 9/9] remove trailing whitespace --- .../src/opentelemetry/sdk/environment_variables/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index d6cdc575c7b..e679f879085 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -500,7 +500,7 @@ def channel_credential_provider() -> grpc.ChannelCredentials: """ .. envvar:: OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES -The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES` stores a comma-separated list of human-readable +The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES` stores a comma-separated list of human-readable gRPC error codes that are considered retryable for the OTLP gRPC exporters (e.g. `UNAVAILABLE, DEADLINE_EXCEEDED`). Supported error codes are defined in `grpc.StatusCode` and are parsed in a case-insensitive manner.