Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/5239.fixed
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instrumentation should not do this, and ideally has type checking or something to ensure that it isn't doing this.. I would log an error here too

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added _logger.error() before the str() fallback in both json-common and proto-common

# 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}")


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 = [
(
Expand Down Expand Up @@ -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))
Original file line number Diff line number Diff line change
Expand Up @@ -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}")


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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))
Loading