From d3e80608df40b2996650580b87e9a5c9876ed983 Mon Sep 17 00:00:00 2001 From: Gaurav Mishra Date: Thu, 21 May 2026 02:16:07 +0000 Subject: [PATCH 1/5] feat: support pathlib.Path in OTLP --- .changelog/5210.fixed | 1 + .../otlp/json/common/_internal/__init__.py | 9 +++++++ .../tests/test_common_encoder.py | 26 +++++++++++++++++++ .../otlp/proto/common/_internal/__init__.py | 9 +++++++ .../tests/test_attribute_encoder.py | 26 +++++++++++++++++++ 5 files changed, 71 insertions(+) create mode 100644 .changelog/5210.fixed diff --git a/.changelog/5210.fixed b/.changelog/5210.fixed new file mode 100644 index 00000000000..aacf82e83e4 --- /dev/null +++ b/.changelog/5210.fixed @@ -0,0 +1 @@ +`opentelemetry-exporter-otlp-proto-common`, `opentelemetry-exporter-otlp-json-common`: support non-standard attribute value types (e.g. `pathlib.Path`) by coercing to string \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/__init__.py b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/__init__.py index 7670c23304b..f580a23f9cf 100644 --- a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/__init__.py @@ -83,6 +83,15 @@ def _encode_value(value: Any, allow_null: bool = False) -> JSONAnyValue | None: ] ) ) + # Third-party instrumentation can inject arbitrary types that cannot be exhaustively + # whitelisted; stringify as a best-effort fallback so telemetry is not silently lost. + # None is excluded — it must be handled via allow_null=True at the call site. + if value is not None: + # pylint: disable=broad-exception-caught + try: + return JSONAnyValue(string_value=str(value)) + except Exception: + pass raise TypeError(f"Invalid type {type(value)} of value {value}") diff --git a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py index 70a9e746fce..05a0094bf79 100644 --- a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py @@ -6,6 +6,7 @@ import base64 import unittest from logging import ERROR +from pathlib import Path from opentelemetry.exporter.otlp.json.common._internal import ( _encode_array, @@ -326,3 +327,28 @@ def test_encode_instrumentation_scope_none(self): result = _encode_instrumentation_scope(None) self.assertEqual(result, JSONInstrumentationScope()) self.assertEqual(result.to_dict(), {}) + + def test_encode_value_pathlib_path(self): + result = _encode_value(Path("/models/my-model")) + self.assertEqual(result, JSONAnyValue(string_value="/models/my-model")) + self.assertEqual(result.to_dict(), {"stringValue": "/models/my-model"}) + + def test_encode_attributes_pathlib_path(self): + result = _encode_attributes({"model_path": Path("/models/my-model")}) + self.assertEqual( + result, + [ + JSONKeyValue( + key="model_path", + value=JSONAnyValue(string_value="/models/my-model"), + ) + ], + ) + + def test_encode_value_unstringable_type_raises(self): + class _Unstringable: + def __str__(self): + raise RuntimeError("cannot convert") + + with self.assertRaises((TypeError, Exception)): + _encode_value(_Unstringable()) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py index 8dbbc212f74..35bdfb54dd2 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py @@ -79,6 +79,15 @@ def _encode_value(value: Any, allow_null: bool = False) -> PB2AnyValue | None: ] ) ) + # Third-party instrumentation can inject arbitrary types that cannot be exhaustively + # whitelisted; stringify as a best-effort fallback so telemetry is not silently lost. + # None is excluded — it must be handled via allow_null=True at the call site. + if value is not None: + # pylint: disable=broad-exception-caught + try: + return PB2AnyValue(string_value=str(value)) + except Exception: + pass raise Exception(f"Invalid type {type(value)} of value {value}") diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py index 58ce2b337e4..cc0effff403 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py @@ -3,9 +3,11 @@ import unittest from logging import ERROR +from pathlib import Path from opentelemetry.exporter.otlp.proto.common._internal import ( _encode_attributes, + _encode_value, ) from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue from opentelemetry.proto.common.v1.common_pb2 import ( @@ -110,3 +112,27 @@ def test_encode_attributes_error_logs_key(self): PB2KeyValue(key="b", value=PB2AnyValue(int_value=2)), ], ) + + def test_encode_value_pathlib_path(self): + result = _encode_value(Path("/models/my-model")) + self.assertEqual(result, PB2AnyValue(string_value="/models/my-model")) + + def test_encode_attributes_pathlib_path(self): + result = _encode_attributes({"model_path": Path("/models/my-model")}) + self.assertEqual( + result, + [ + PB2KeyValue( + key="model_path", + value=PB2AnyValue(string_value="/models/my-model"), + ) + ], + ) + + def test_encode_value_unstringable_type_raises(self): + class _Unstringable: + def __str__(self): + raise RuntimeError("cannot convert") + + with self.assertRaises(Exception): + _encode_value(_Unstringable()) From 3c182dd449f96d3dbb50f7810bec7cb65cd06ba3 Mon Sep 17 00:00:00 2001 From: Gaurav Mishra Date: Thu, 21 May 2026 02:27:50 +0000 Subject: [PATCH 2/5] refactor: changelog --- .changelog/{5210.fixed => 5239.fixed} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .changelog/{5210.fixed => 5239.fixed} (73%) diff --git a/.changelog/5210.fixed b/.changelog/5239.fixed similarity index 73% rename from .changelog/5210.fixed rename to .changelog/5239.fixed index aacf82e83e4..a4a50718193 100644 --- a/.changelog/5210.fixed +++ b/.changelog/5239.fixed @@ -1 +1 @@ -`opentelemetry-exporter-otlp-proto-common`, `opentelemetry-exporter-otlp-json-common`: support non-standard attribute value types (e.g. `pathlib.Path`) by coercing to string \ No newline at end of file +`opentelemetry-exporter-otlp-proto-common`, `opentelemetry-exporter-otlp-json-common`: support non-standard attribute value types (e.g. `pathlib.Path`) by coercing to string From 00dab65de05b69b5fbe2c55ee7e709814e53cd8a Mon Sep 17 00:00:00 2001 From: Gaurav Mishra Date: Thu, 21 May 2026 02:33:45 +0000 Subject: [PATCH 3/5] fix(tests): use platform-independent path and suppress too-many-public-methods - Use str(Path(...)) as expected value so tests pass on Windows (backslash separator) - Add pylint disable for too-many-public-methods on TestCommonEncoder (21 methods) --- .../tests/test_common_encoder.py | 14 ++++++++------ .../tests/test_attribute_encoder.py | 10 ++++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py index 05a0094bf79..59bd0318c72 100644 --- a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py @@ -36,7 +36,7 @@ from opentelemetry.sdk.util.instrumentation import InstrumentationScope -class TestCommonEncoder(unittest.TestCase): +class TestCommonEncoder(unittest.TestCase): # pylint: disable=too-many-public-methods def test_encode_value(self): cases = [ ( @@ -329,18 +329,20 @@ def test_encode_instrumentation_scope_none(self): self.assertEqual(result.to_dict(), {}) def test_encode_value_pathlib_path(self): - result = _encode_value(Path("/models/my-model")) - self.assertEqual(result, JSONAnyValue(string_value="/models/my-model")) - self.assertEqual(result.to_dict(), {"stringValue": "/models/my-model"}) + path = Path("/models/my-model") + result = _encode_value(path) + self.assertEqual(result, JSONAnyValue(string_value=str(path))) + self.assertEqual(result.to_dict(), {"stringValue": str(path)}) def test_encode_attributes_pathlib_path(self): - result = _encode_attributes({"model_path": Path("/models/my-model")}) + path = Path("/models/my-model") + result = _encode_attributes({"model_path": path}) self.assertEqual( result, [ JSONKeyValue( key="model_path", - value=JSONAnyValue(string_value="/models/my-model"), + value=JSONAnyValue(string_value=str(path)), ) ], ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py index cc0effff403..216bcdb93a5 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py @@ -114,17 +114,19 @@ def test_encode_attributes_error_logs_key(self): ) def test_encode_value_pathlib_path(self): - result = _encode_value(Path("/models/my-model")) - self.assertEqual(result, PB2AnyValue(string_value="/models/my-model")) + path = Path("/models/my-model") + result = _encode_value(path) + self.assertEqual(result, PB2AnyValue(string_value=str(path))) def test_encode_attributes_pathlib_path(self): - result = _encode_attributes({"model_path": Path("/models/my-model")}) + path = Path("/models/my-model") + result = _encode_attributes({"model_path": path}) self.assertEqual( result, [ PB2KeyValue( key="model_path", - value=PB2AnyValue(string_value="/models/my-model"), + value=PB2AnyValue(string_value=str(path)), ) ], ) From cabd22e415e45f334aadfcd8af7c21a494d813f5 Mon Sep 17 00:00:00 2001 From: Gaurav Mishra Date: Thu, 21 May 2026 20:38:49 +0000 Subject: [PATCH 4/5] log error when encoding non-standard attribute type --- .../otlp/json/common/_internal/__init__.py | 5 +++++ .../tests/test_common_encoder.py | 18 +++++++++++++++--- .../otlp/proto/common/_internal/__init__.py | 5 +++++ .../tests/test_attribute_encoder.py | 18 +++++++++++++++--- 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/__init__.py b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/__init__.py index f580a23f9cf..ff3f842dd0f 100644 --- a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/__init__.py @@ -87,6 +87,11 @@ def _encode_value(value: Any, allow_null: bool = False) -> JSONAnyValue | None: # whitelisted; stringify as a best-effort fallback so telemetry is not silently lost. # None is excluded — it must be handled via allow_null=True at the call site. if value is not None: + _logger.error( + "Invalid type %s for OTLP value; encoding as string. " + "This is a bug in the instrumentation.", + type(value), + ) # pylint: disable=broad-exception-caught try: return JSONAnyValue(string_value=str(value)) diff --git a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py index 59bd0318c72..7901a7872fa 100644 --- a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py @@ -330,9 +330,15 @@ def test_encode_instrumentation_scope_none(self): def test_encode_value_pathlib_path(self): path = Path("/models/my-model") - result = _encode_value(path) + with self.assertLogs( + "opentelemetry.exporter.otlp.json.common._internal", level=ERROR + ) as log_cm: + result = _encode_value(path) self.assertEqual(result, JSONAnyValue(string_value=str(path))) self.assertEqual(result.to_dict(), {"stringValue": str(path)}) + self.assertTrue( + any("Invalid type" in msg for msg in log_cm.output) + ) def test_encode_attributes_pathlib_path(self): path = Path("/models/my-model") @@ -352,5 +358,11 @@ class _Unstringable: def __str__(self): raise RuntimeError("cannot convert") - with self.assertRaises((TypeError, Exception)): - _encode_value(_Unstringable()) + with self.assertLogs( + "opentelemetry.exporter.otlp.json.common._internal", level=ERROR + ) as log_cm: + with self.assertRaises((TypeError, Exception)): + _encode_value(_Unstringable()) + self.assertTrue( + any("Invalid type" in msg for msg in log_cm.output) + ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py index 35bdfb54dd2..137be3b5510 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py @@ -83,6 +83,11 @@ def _encode_value(value: Any, allow_null: bool = False) -> PB2AnyValue | None: # whitelisted; stringify as a best-effort fallback so telemetry is not silently lost. # None is excluded — it must be handled via allow_null=True at the call site. if value is not None: + _logger.error( + "Invalid type %s for OTLP value; encoding as string. " + "This is a bug in the instrumentation.", + type(value), + ) # pylint: disable=broad-exception-caught try: return PB2AnyValue(string_value=str(value)) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py index 216bcdb93a5..9bdc3b4a8bf 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py @@ -115,8 +115,14 @@ def test_encode_attributes_error_logs_key(self): def test_encode_value_pathlib_path(self): path = Path("/models/my-model") - result = _encode_value(path) + with self.assertLogs( + "opentelemetry.exporter.otlp.proto.common._internal", level=ERROR + ) as log_cm: + result = _encode_value(path) self.assertEqual(result, PB2AnyValue(string_value=str(path))) + self.assertTrue( + any("Invalid type" in msg for msg in log_cm.output) + ) def test_encode_attributes_pathlib_path(self): path = Path("/models/my-model") @@ -136,5 +142,11 @@ class _Unstringable: def __str__(self): raise RuntimeError("cannot convert") - with self.assertRaises(Exception): - _encode_value(_Unstringable()) + with self.assertLogs( + "opentelemetry.exporter.otlp.proto.common._internal", level=ERROR + ) as log_cm: + with self.assertRaises(Exception): + _encode_value(_Unstringable()) + self.assertTrue( + any("Invalid type" in msg for msg in log_cm.output) + ) From ed1470cc5590e4d19d9f570c244b095d0505e5a6 Mon Sep 17 00:00:00 2001 From: Gaurav Mishra Date: Thu, 21 May 2026 20:52:10 +0000 Subject: [PATCH 5/5] style: apply ruff format to test files --- .../tests/test_common_encoder.py | 8 ++------ .../tests/test_attribute_encoder.py | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py index 7901a7872fa..22d58088178 100644 --- a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py @@ -336,9 +336,7 @@ def test_encode_value_pathlib_path(self): result = _encode_value(path) self.assertEqual(result, JSONAnyValue(string_value=str(path))) self.assertEqual(result.to_dict(), {"stringValue": str(path)}) - self.assertTrue( - any("Invalid type" in msg for msg in log_cm.output) - ) + self.assertTrue(any("Invalid type" in msg for msg in log_cm.output)) def test_encode_attributes_pathlib_path(self): path = Path("/models/my-model") @@ -363,6 +361,4 @@ def __str__(self): ) as log_cm: with self.assertRaises((TypeError, Exception)): _encode_value(_Unstringable()) - self.assertTrue( - any("Invalid type" in msg for msg in log_cm.output) - ) + self.assertTrue(any("Invalid type" in msg for msg in log_cm.output)) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py index 9bdc3b4a8bf..dda76beaabb 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py @@ -120,9 +120,7 @@ def test_encode_value_pathlib_path(self): ) as log_cm: result = _encode_value(path) self.assertEqual(result, PB2AnyValue(string_value=str(path))) - self.assertTrue( - any("Invalid type" in msg for msg in log_cm.output) - ) + self.assertTrue(any("Invalid type" in msg for msg in log_cm.output)) def test_encode_attributes_pathlib_path(self): path = Path("/models/my-model") @@ -147,6 +145,4 @@ def __str__(self): ) as log_cm: with self.assertRaises(Exception): _encode_value(_Unstringable()) - self.assertTrue( - any("Invalid type" in msg for msg in log_cm.output) - ) + self.assertTrue(any("Invalid type" in msg for msg in log_cm.output))