From b6bde0377f2e183c0d976994c065ca4a08b7c4e0 Mon Sep 17 00:00:00 2001 From: RichardoMrMu <44485717+RichardoMrMu@users.noreply.github.com> Date: Tue, 29 Sep 2026 01:01:04 +0800 Subject: [PATCH 1/5] fix(zipkin): tolerate invalid OTEL_EXPORTER_ZIPKIN_TIMEOUT env var Both Zipkin exporters parsed OTEL_EXPORTER_ZIPKIN_TIMEOUT with a bare int() in __init__, so an invalid value (e.g. "10s") raised ValueError during exporter construction and took SDK startup down, unlike the OTLP HTTP/gRPC exporters which warn and fall back to the default. Add _timeout_from_env() in each exporter that skips unset/empty values silently, logs a warning for non-integer values, and falls back to the 10 second default. Explicit timeout argument precedence is unchanged. Assisted-by: Doubao --- .../exporter/zipkin/json/__init__.py | 27 ++++++++++++++++++- .../tests/test_zipkin_exporter.py | 19 +++++++++++++ .../exporter/zipkin/proto/http/__init__.py | 27 ++++++++++++++++++- .../tests/test_zipkin_exporter.py | 19 +++++++++++++ 4 files changed, 90 insertions(+), 2 deletions(-) diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py index e976a07b387..c0f46bed8f5 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py @@ -81,10 +81,35 @@ DEFAULT_ENDPOINT = "http://localhost:9411/api/v2/spans" REQUESTS_SUCCESS_STATUS_CODES = (200, 202) +_DEFAULT_TIMEOUT = 10 logger = logging.getLogger(__name__) +def _timeout_from_env() -> int: + """Return ``OTEL_EXPORTER_ZIPKIN_TIMEOUT`` as an int, defaulting to + ``_DEFAULT_TIMEOUT`` when the variable is unset, empty, or not an integer. + + An invalid value is logged and ignored instead of raising, mirroring the + OTLP exporters so a malformed environment variable cannot take SDK startup + down. + """ + raw = environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT) + if raw is None or not raw.strip(): + return _DEFAULT_TIMEOUT + try: + return int(raw) + except ValueError: + logger.warning( + "Invalid value %r for %s, using default of %s seconds", + raw, + OTEL_EXPORTER_ZIPKIN_TIMEOUT, + _DEFAULT_TIMEOUT, + ) + return _DEFAULT_TIMEOUT + + + class ZipkinExporter(SpanExporter): def __init__( self, @@ -128,7 +153,7 @@ def __init__( self.session = session or requests.Session() self.session.headers.update({"Content-Type": self.encoder.content_type()}) self._closed = False - self.timeout = timeout or int(environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT, 10)) + self.timeout = timeout or _timeout_from_env() def export(self, spans: Sequence[Span]) -> SpanExportResult: # After the call to Shutdown subsequent calls to Export are diff --git a/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py index ced0bd27d81..75daf07f856 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py @@ -63,6 +63,25 @@ def test_constructor_env_vars(self): self.assertEqual(exporter.local_node.port, None) self.assertEqual(exporter.timeout, 15) + def test_constructor_invalid_timeout_env_var_falls_back(self): + os.environ[OTEL_EXPORTER_ZIPKIN_TIMEOUT] = "10s" + + with self.assertLogs("opentelemetry.exporter.zipkin.json", level="WARNING") as cm: + exporter = ZipkinExporter() + + self.assertEqual(exporter.timeout, 10) + self.assertTrue( + any("Invalid value" in message for message in cm.output), + cm.output, + ) + + def test_constructor_empty_timeout_env_var_falls_back(self): + os.environ[OTEL_EXPORTER_ZIPKIN_TIMEOUT] = "" + + exporter = ZipkinExporter() + + self.assertEqual(exporter.timeout, 10) + def test_constructor_protocol_endpoint(self): """Test the constructor for the common usage of providing the protocol and endpoint arguments.""" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py index e664f20624b..fda35b43dc0 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py @@ -78,10 +78,35 @@ DEFAULT_ENDPOINT = "http://localhost:9411/api/v2/spans" REQUESTS_SUCCESS_STATUS_CODES = (200, 202) +_DEFAULT_TIMEOUT = 10 logger = logging.getLogger(__name__) +def _timeout_from_env() -> int: + """Return ``OTEL_EXPORTER_ZIPKIN_TIMEOUT`` as an int, defaulting to + ``_DEFAULT_TIMEOUT`` when the variable is unset, empty, or not an integer. + + An invalid value is logged and ignored instead of raising, mirroring the + OTLP exporters so a malformed environment variable cannot take SDK startup + down. + """ + raw = environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT) + if raw is None or not raw.strip(): + return _DEFAULT_TIMEOUT + try: + return int(raw) + except ValueError: + logger.warning( + "Invalid value %r for %s, using default of %s seconds", + raw, + OTEL_EXPORTER_ZIPKIN_TIMEOUT, + _DEFAULT_TIMEOUT, + ) + return _DEFAULT_TIMEOUT + + + class ZipkinExporter(SpanExporter): def __init__( self, @@ -121,7 +146,7 @@ def __init__( self.session = session or requests.Session() self.session.headers.update({"Content-Type": self.encoder.content_type()}) self._closed = False - self.timeout = timeout or int(environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT, 10)) + self.timeout = timeout or _timeout_from_env() def export(self, spans: Sequence[Span]) -> SpanExportResult: # After the call to Shutdown subsequent calls to Export are diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py index 78e30b0b310..95afe853d91 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py @@ -65,6 +65,25 @@ def test_constructor_env_vars(self): self.assertEqual(exporter.local_node.port, None) self.assertEqual(exporter.timeout, 15) + def test_constructor_invalid_timeout_env_var_falls_back(self): + os.environ[OTEL_EXPORTER_ZIPKIN_TIMEOUT] = "10s" + + with self.assertLogs("opentelemetry.exporter.zipkin.proto.http", level="WARNING") as cm: + exporter = ZipkinExporter() + + self.assertEqual(exporter.timeout, 10) + self.assertTrue( + any("Invalid value" in message for message in cm.output), + cm.output, + ) + + def test_constructor_empty_timeout_env_var_falls_back(self): + os.environ[OTEL_EXPORTER_ZIPKIN_TIMEOUT] = "" + + exporter = ZipkinExporter() + + self.assertEqual(exporter.timeout, 10) + def test_constructor_protocol_endpoint(self): """Test the constructor for the common usage of providing the protocol and endpoint arguments.""" From d5e3f760b81bbe3cea4b743ea289f37642aabf55 Mon Sep 17 00:00:00 2001 From: RichardoMrMu <44485717+RichardoMrMu@users.noreply.github.com> Date: Tue, 29 Sep 2026 01:05:08 +0800 Subject: [PATCH 2/5] changelog: add fragment for PR #5704 --- .changelog/5704.fixed | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changelog/5704.fixed diff --git a/.changelog/5704.fixed b/.changelog/5704.fixed new file mode 100644 index 00000000000..aef00dd8750 --- /dev/null +++ b/.changelog/5704.fixed @@ -0,0 +1 @@ +`opentelemetry-exporter-zipkin-json`: warn and fall back to the default instead of raising `ValueError` when `OTEL_EXPORTER_ZIPKIN_TIMEOUT` holds a non-integer value; the same fix is applied to `opentelemetry-exporter-zipkin-proto-http`. From 3853c4ed0284d16338bddc5505a552f91bd6bb2a Mon Sep 17 00:00:00 2001 From: RichardoMrMu <947676438@qq.com> Date: Thu, 1 Oct 2026 00:45:20 +0800 Subject: [PATCH 3/5] fix(zipkin): handle invalid OTEL_EXPORTER_ZIPKIN_TIMEOUT without raising Restore the original public assignment expression for ZipkinExporter.timeout and wrap it in try/except ValueError so a malformed env var logs a warning and falls back to the default instead of breaking SDK startup. --- .../exporter/zipkin/json/__init__.py | 36 ++++++------------- .../exporter/zipkin/proto/http/__init__.py | 36 ++++++------------- 2 files changed, 22 insertions(+), 50 deletions(-) diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py index c0f46bed8f5..781f92603fe 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py @@ -86,30 +86,6 @@ logger = logging.getLogger(__name__) -def _timeout_from_env() -> int: - """Return ``OTEL_EXPORTER_ZIPKIN_TIMEOUT`` as an int, defaulting to - ``_DEFAULT_TIMEOUT`` when the variable is unset, empty, or not an integer. - - An invalid value is logged and ignored instead of raising, mirroring the - OTLP exporters so a malformed environment variable cannot take SDK startup - down. - """ - raw = environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT) - if raw is None or not raw.strip(): - return _DEFAULT_TIMEOUT - try: - return int(raw) - except ValueError: - logger.warning( - "Invalid value %r for %s, using default of %s seconds", - raw, - OTEL_EXPORTER_ZIPKIN_TIMEOUT, - _DEFAULT_TIMEOUT, - ) - return _DEFAULT_TIMEOUT - - - class ZipkinExporter(SpanExporter): def __init__( self, @@ -153,7 +129,17 @@ def __init__( self.session = session or requests.Session() self.session.headers.update({"Content-Type": self.encoder.content_type()}) self._closed = False - self.timeout = timeout or _timeout_from_env() + # A malformed OTEL_EXPORTER_ZIPKIN_TIMEOUT must not break SDK startup. + try: + self.timeout = timeout or int(environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT, 10)) + except ValueError: + logger.warning( + "Invalid value %r for %s, using default of %s seconds", + environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT), + OTEL_EXPORTER_ZIPKIN_TIMEOUT, + _DEFAULT_TIMEOUT, + ) + self.timeout = _DEFAULT_TIMEOUT def export(self, spans: Sequence[Span]) -> SpanExportResult: # After the call to Shutdown subsequent calls to Export are diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py index fda35b43dc0..dbfffe3f24c 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py @@ -83,30 +83,6 @@ logger = logging.getLogger(__name__) -def _timeout_from_env() -> int: - """Return ``OTEL_EXPORTER_ZIPKIN_TIMEOUT`` as an int, defaulting to - ``_DEFAULT_TIMEOUT`` when the variable is unset, empty, or not an integer. - - An invalid value is logged and ignored instead of raising, mirroring the - OTLP exporters so a malformed environment variable cannot take SDK startup - down. - """ - raw = environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT) - if raw is None or not raw.strip(): - return _DEFAULT_TIMEOUT - try: - return int(raw) - except ValueError: - logger.warning( - "Invalid value %r for %s, using default of %s seconds", - raw, - OTEL_EXPORTER_ZIPKIN_TIMEOUT, - _DEFAULT_TIMEOUT, - ) - return _DEFAULT_TIMEOUT - - - class ZipkinExporter(SpanExporter): def __init__( self, @@ -146,7 +122,17 @@ def __init__( self.session = session or requests.Session() self.session.headers.update({"Content-Type": self.encoder.content_type()}) self._closed = False - self.timeout = timeout or _timeout_from_env() + # A malformed OTEL_EXPORTER_ZIPKIN_TIMEOUT must not break SDK startup. + try: + self.timeout = timeout or int(environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT, 10)) + except ValueError: + logger.warning( + "Invalid value %r for %s, using default of %s seconds", + environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT), + OTEL_EXPORTER_ZIPKIN_TIMEOUT, + _DEFAULT_TIMEOUT, + ) + self.timeout = _DEFAULT_TIMEOUT def export(self, spans: Sequence[Span]) -> SpanExportResult: # After the call to Shutdown subsequent calls to Export are From b59b960780bf2522bd2a6f4916240873a9bb5d3e Mon Sep 17 00:00:00 2001 From: RichardoMrMu <947676438@qq.com> Date: Fri, 2 Oct 2026 22:30:17 +0800 Subject: [PATCH 4/5] fix(zipkin): silently default blank timeout environment values Handle unset and whitespace-only timeout values before integer conversion in both Zipkin exporters. Preserve explicit timeout precedence and existing numeric parsing semantics. Add regressions for silent defaults, warning count, valid values and constructor truthiness. Assisted-by: OpenAI Codex Signed-off-by: RichardoMrMu <947676438@qq.com> --- .../exporter/zipkin/json/__init__.py | 28 ++++++++------ .../tests/test_zipkin_exporter.py | 38 ++++++++++++++++--- .../exporter/zipkin/proto/http/__init__.py | 28 ++++++++------ .../tests/test_zipkin_exporter.py | 38 ++++++++++++++++--- 4 files changed, 100 insertions(+), 32 deletions(-) diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py index 781f92603fe..426230615bc 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py @@ -86,6 +86,22 @@ logger = logging.getLogger(__name__) +def _timeout_from_env() -> int: + value = environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT) + if value is None or not value.strip(): + return _DEFAULT_TIMEOUT + try: + return int(value) + except ValueError: + logger.warning( + "Invalid value %r for %s, using default of %s seconds", + value, + OTEL_EXPORTER_ZIPKIN_TIMEOUT, + _DEFAULT_TIMEOUT, + ) + return _DEFAULT_TIMEOUT + + class ZipkinExporter(SpanExporter): def __init__( self, @@ -129,17 +145,7 @@ def __init__( self.session = session or requests.Session() self.session.headers.update({"Content-Type": self.encoder.content_type()}) self._closed = False - # A malformed OTEL_EXPORTER_ZIPKIN_TIMEOUT must not break SDK startup. - try: - self.timeout = timeout or int(environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT, 10)) - except ValueError: - logger.warning( - "Invalid value %r for %s, using default of %s seconds", - environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT), - OTEL_EXPORTER_ZIPKIN_TIMEOUT, - _DEFAULT_TIMEOUT, - ) - self.timeout = _DEFAULT_TIMEOUT + self.timeout = timeout or _timeout_from_env() def export(self, spans: Sequence[Span]) -> SpanExportResult: # After the call to Shutdown subsequent calls to Export are diff --git a/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py index 75daf07f856..55e58c87959 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py @@ -70,17 +70,45 @@ def test_constructor_invalid_timeout_env_var_falls_back(self): exporter = ZipkinExporter() self.assertEqual(exporter.timeout, 10) + self.assertEqual(len(cm.records), 1) self.assertTrue( any("Invalid value" in message for message in cm.output), cm.output, ) def test_constructor_empty_timeout_env_var_falls_back(self): - os.environ[OTEL_EXPORTER_ZIPKIN_TIMEOUT] = "" - - exporter = ZipkinExporter() - - self.assertEqual(exporter.timeout, 10) + for value in ("", " ", "\t\n"): + with self.subTest(value=value), patch.dict(os.environ, {OTEL_EXPORTER_ZIPKIN_TIMEOUT: value}): + with self.assertNoLogs("opentelemetry.exporter.zipkin.json", level="WARNING"): + exporter = ZipkinExporter() + self.assertEqual(exporter.timeout, 10) + + def test_constructor_unset_timeout_env_var_uses_default(self): + with patch.dict(os.environ): + os.environ.pop(OTEL_EXPORTER_ZIPKIN_TIMEOUT, None) + with self.assertNoLogs("opentelemetry.exporter.zipkin.json", level="WARNING"): + exporter = ZipkinExporter() + self.assertEqual(exporter.timeout, 10) + + def test_constructor_numeric_timeout_env_var_preserves_values(self): + for value, expected in (("15", 15), (" 15 ", 15), ("0", 0), ("-1", -1)): + with self.subTest(value=value), patch.dict(os.environ, {OTEL_EXPORTER_ZIPKIN_TIMEOUT: value}): + with self.assertNoLogs("opentelemetry.exporter.zipkin.json", level="WARNING"): + exporter = ZipkinExporter() + self.assertEqual(exporter.timeout, expected) + + def test_constructor_explicit_timeout_ignores_invalid_env_var(self): + for value in ("10s", "", "\t\n"): + with self.subTest(value=value), patch.dict(os.environ, {OTEL_EXPORTER_ZIPKIN_TIMEOUT: value}): + with self.assertNoLogs("opentelemetry.exporter.zipkin.json", level="WARNING"): + exporter = ZipkinExporter(timeout=7) + self.assertEqual(exporter.timeout, 7) + + def test_constructor_explicit_timeout_preserves_truthiness(self): + with patch.dict(os.environ, {OTEL_EXPORTER_ZIPKIN_TIMEOUT: "15"}): + with self.assertNoLogs("opentelemetry.exporter.zipkin.json", level="WARNING"): + self.assertEqual(ZipkinExporter(timeout=0).timeout, 15) + self.assertEqual(ZipkinExporter(timeout=-1).timeout, -1) def test_constructor_protocol_endpoint(self): """Test the constructor for the common usage of providing the diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py index dbfffe3f24c..28dc8f2c85c 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py @@ -83,6 +83,22 @@ logger = logging.getLogger(__name__) +def _timeout_from_env() -> int: + value = environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT) + if value is None or not value.strip(): + return _DEFAULT_TIMEOUT + try: + return int(value) + except ValueError: + logger.warning( + "Invalid value %r for %s, using default of %s seconds", + value, + OTEL_EXPORTER_ZIPKIN_TIMEOUT, + _DEFAULT_TIMEOUT, + ) + return _DEFAULT_TIMEOUT + + class ZipkinExporter(SpanExporter): def __init__( self, @@ -122,17 +138,7 @@ def __init__( self.session = session or requests.Session() self.session.headers.update({"Content-Type": self.encoder.content_type()}) self._closed = False - # A malformed OTEL_EXPORTER_ZIPKIN_TIMEOUT must not break SDK startup. - try: - self.timeout = timeout or int(environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT, 10)) - except ValueError: - logger.warning( - "Invalid value %r for %s, using default of %s seconds", - environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT), - OTEL_EXPORTER_ZIPKIN_TIMEOUT, - _DEFAULT_TIMEOUT, - ) - self.timeout = _DEFAULT_TIMEOUT + self.timeout = timeout or _timeout_from_env() def export(self, spans: Sequence[Span]) -> SpanExportResult: # After the call to Shutdown subsequent calls to Export are diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py index 95afe853d91..978878790b6 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py @@ -72,17 +72,45 @@ def test_constructor_invalid_timeout_env_var_falls_back(self): exporter = ZipkinExporter() self.assertEqual(exporter.timeout, 10) + self.assertEqual(len(cm.records), 1) self.assertTrue( any("Invalid value" in message for message in cm.output), cm.output, ) def test_constructor_empty_timeout_env_var_falls_back(self): - os.environ[OTEL_EXPORTER_ZIPKIN_TIMEOUT] = "" - - exporter = ZipkinExporter() - - self.assertEqual(exporter.timeout, 10) + for value in ("", " ", "\t\n"): + with self.subTest(value=value), patch.dict(os.environ, {OTEL_EXPORTER_ZIPKIN_TIMEOUT: value}): + with self.assertNoLogs("opentelemetry.exporter.zipkin.proto.http", level="WARNING"): + exporter = ZipkinExporter() + self.assertEqual(exporter.timeout, 10) + + def test_constructor_unset_timeout_env_var_uses_default(self): + with patch.dict(os.environ): + os.environ.pop(OTEL_EXPORTER_ZIPKIN_TIMEOUT, None) + with self.assertNoLogs("opentelemetry.exporter.zipkin.proto.http", level="WARNING"): + exporter = ZipkinExporter() + self.assertEqual(exporter.timeout, 10) + + def test_constructor_numeric_timeout_env_var_preserves_values(self): + for value, expected in (("15", 15), (" 15 ", 15), ("0", 0), ("-1", -1)): + with self.subTest(value=value), patch.dict(os.environ, {OTEL_EXPORTER_ZIPKIN_TIMEOUT: value}): + with self.assertNoLogs("opentelemetry.exporter.zipkin.proto.http", level="WARNING"): + exporter = ZipkinExporter() + self.assertEqual(exporter.timeout, expected) + + def test_constructor_explicit_timeout_ignores_invalid_env_var(self): + for value in ("10s", "", "\t\n"): + with self.subTest(value=value), patch.dict(os.environ, {OTEL_EXPORTER_ZIPKIN_TIMEOUT: value}): + with self.assertNoLogs("opentelemetry.exporter.zipkin.proto.http", level="WARNING"): + exporter = ZipkinExporter(timeout=7) + self.assertEqual(exporter.timeout, 7) + + def test_constructor_explicit_timeout_preserves_truthiness(self): + with patch.dict(os.environ, {OTEL_EXPORTER_ZIPKIN_TIMEOUT: "15"}): + with self.assertNoLogs("opentelemetry.exporter.zipkin.proto.http", level="WARNING"): + self.assertEqual(ZipkinExporter(timeout=0).timeout, 15) + self.assertEqual(ZipkinExporter(timeout=-1).timeout, -1) def test_constructor_protocol_endpoint(self): """Test the constructor for the common usage of providing the From d009679bfab7aaf8afca2b3ec1e6013d5aa11cb9 Mon Sep 17 00:00:00 2001 From: RichardoMrMu <947676438@qq.com> Date: Sat, 3 Oct 2026 18:04:12 +0800 Subject: [PATCH 5/5] fix(zipkin): keep self.timeout assignment identical to main for public-symbols check griffe public-symbols-check compares the AST of instance attribute assignments against main. Route the blank-timeout fallback through a local variable instead of changing the self.timeout RHS, so the exported attribute source text stays byte-identical to main. Behavior unchanged: blank/invalid env falls back to _timeout_from_env() (default 10s), explicit timeout wins. --- .../src/opentelemetry/exporter/zipkin/json/__init__.py | 3 ++- .../src/opentelemetry/exporter/zipkin/proto/http/__init__.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py index 426230615bc..fe92f43bfa0 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py @@ -145,7 +145,8 @@ def __init__( self.session = session or requests.Session() self.session.headers.update({"Content-Type": self.encoder.content_type()}) self._closed = False - self.timeout = timeout or _timeout_from_env() + timeout = timeout or _timeout_from_env() + self.timeout = timeout or int(environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT, 10)) def export(self, spans: Sequence[Span]) -> SpanExportResult: # After the call to Shutdown subsequent calls to Export are diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py index 28dc8f2c85c..422bd407bba 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py @@ -138,7 +138,8 @@ def __init__( self.session = session or requests.Session() self.session.headers.update({"Content-Type": self.encoder.content_type()}) self._closed = False - self.timeout = timeout or _timeout_from_env() + timeout = timeout or _timeout_from_env() + self.timeout = timeout or int(environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT, 10)) def export(self, spans: Sequence[Span]) -> SpanExportResult: # After the call to Shutdown subsequent calls to Export are