diff --git a/.changelog/5239.fixed b/.changelog/5239.fixed new file mode 100644 index 00000000000..a4a50718193 --- /dev/null +++ b/.changelog/5239.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 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..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 @@ -83,6 +83,20 @@ 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: + _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)) + 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..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 @@ -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, @@ -35,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 = [ ( @@ -326,3 +327,38 @@ 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): + path = Path("/models/my-model") + 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") + result = _encode_attributes({"model_path": path}) + self.assertEqual( + result, + [ + JSONKeyValue( + key="model_path", + value=JSONAnyValue(string_value=str(path)), + ) + ], + ) + + def test_encode_value_unstringable_type_raises(self): + class _Unstringable: + def __str__(self): + raise RuntimeError("cannot convert") + + 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 8dbbc212f74..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 @@ -79,6 +79,20 @@ 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: + _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)) + 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..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 @@ -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,37 @@ def test_encode_attributes_error_logs_key(self): PB2KeyValue(key="b", value=PB2AnyValue(int_value=2)), ], ) + + def test_encode_value_pathlib_path(self): + path = Path("/models/my-model") + 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") + result = _encode_attributes({"model_path": path}) + self.assertEqual( + result, + [ + PB2KeyValue( + key="model_path", + value=PB2AnyValue(string_value=str(path)), + ) + ], + ) + + def test_encode_value_unstringable_type_raises(self): + class _Unstringable: + def __str__(self): + raise RuntimeError("cannot convert") + + 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))