From a254b6510d1a814ee8deff2144595d5239702a95 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 9 Dec 2024 12:41:30 +0100 Subject: [PATCH 01/19] logs: introduce LogAttributes type Logs attribute accepts AnyValue as AttributeValue add a type to describe that and start using it. --- .../opentelemetry/_logs/_internal/__init__.py | 22 +++++++++---------- .../src/opentelemetry/util/types.py | 2 ++ opentelemetry-api/tests/logs/test_proxy.py | 4 ++-- .../sdk/_logs/_internal/__init__.py | 10 ++++----- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index f20bd8507e5..de03cd636db 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -37,14 +37,14 @@ from logging import getLogger from os import environ from time import time_ns -from typing import Any, Optional, cast +from typing import Optional, cast from opentelemetry._logs.severity import SeverityNumber from opentelemetry.environment_variables import _OTEL_PYTHON_LOGGER_PROVIDER from opentelemetry.trace.span import TraceFlags from opentelemetry.util._once import Once from opentelemetry.util._providers import _load_provider -from opentelemetry.util.types import Attributes +from opentelemetry.util.types import AnyValue, LogAttributes _logger = getLogger(__name__) @@ -66,8 +66,8 @@ def __init__( trace_flags: Optional["TraceFlags"] = None, severity_text: Optional[str] = None, severity_number: Optional[SeverityNumber] = None, - body: Optional[Any] = None, - attributes: Optional["Attributes"] = None, + body: AnyValue = None, + attributes: Optional[LogAttributes] = None, ): self.timestamp = timestamp if observed_timestamp is None: @@ -78,7 +78,7 @@ def __init__( self.trace_flags = trace_flags self.severity_text = severity_text self.severity_number = severity_number - self.body = body # type: ignore + self.body = body self.attributes = attributes @@ -90,7 +90,7 @@ def __init__( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[Attributes] = None, + attributes: Optional[LogAttributes] = None, ) -> None: super().__init__() self._name = name @@ -119,7 +119,7 @@ def __init__( # pylint: disable=super-init-not-called name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[Attributes] = None, + attributes: Optional[LogAttributes] = None, ): self._name = name self._version = version @@ -158,7 +158,7 @@ def get_logger( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[Attributes] = None, + attributes: Optional[LogAttributes] = None, ) -> Logger: """Returns a `Logger` for use by the given instrumentation library. @@ -196,7 +196,7 @@ def get_logger( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[Attributes] = None, + attributes: Optional[LogAttributes] = None, ) -> Logger: """Returns a NoOpLogger.""" return NoOpLogger( @@ -210,7 +210,7 @@ def get_logger( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[Attributes] = None, + attributes: Optional[LogAttributes] = None, ) -> Logger: if _LOGGER_PROVIDER: return _LOGGER_PROVIDER.get_logger( @@ -273,7 +273,7 @@ def get_logger( instrumenting_library_version: str = "", logger_provider: Optional[LoggerProvider] = None, schema_url: Optional[str] = None, - attributes: Optional[Attributes] = None, + attributes: Optional[LogAttributes] = None, ) -> "Logger": """Returns a `Logger` for use within a python process. diff --git a/opentelemetry-api/src/opentelemetry/util/types.py b/opentelemetry-api/src/opentelemetry/util/types.py index be311faf555..5e319fbcb73 100644 --- a/opentelemetry-api/src/opentelemetry/util/types.py +++ b/opentelemetry-api/src/opentelemetry/util/types.py @@ -55,3 +55,5 @@ ], ..., ] + +LogAttributes = Mapping[str, "AnyValue"] diff --git a/opentelemetry-api/tests/logs/test_proxy.py b/opentelemetry-api/tests/logs/test_proxy.py index 8e87ceb96ea..512754df89e 100644 --- a/opentelemetry-api/tests/logs/test_proxy.py +++ b/opentelemetry-api/tests/logs/test_proxy.py @@ -19,7 +19,7 @@ import opentelemetry._logs._internal as _logs_internal from opentelemetry import _logs from opentelemetry.test.globals_test import LoggingGlobalsTest -from opentelemetry.util.types import Attributes +from opentelemetry.util.types import LogAttributes class TestProvider(_logs.NoOpLoggerProvider): @@ -28,7 +28,7 @@ def get_logger( name: str, version: typing.Optional[str] = None, schema_url: typing.Optional[str] = None, - attributes: typing.Optional[Attributes] = None, + attributes: typing.Optional[LogAttributes] = None, ) -> _logs.Logger: return LoggerTest(name) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 5d17c39f332..7dad4a09660 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -52,7 +52,7 @@ get_current_span, ) from opentelemetry.trace.span import TraceFlags -from opentelemetry.util.types import AnyValue, Attributes +from opentelemetry.util.types import AnyValue, LogAttributes _logger = logging.getLogger(__name__) @@ -182,7 +182,7 @@ def __init__( severity_number: SeverityNumber | None = None, body: AnyValue | None = None, resource: Resource | None = None, - attributes: Attributes | None = None, + attributes: LogAttributes | None = None, limits: LogLimits | None = _UnsetLogLimits, ): super().__init__( @@ -477,7 +477,7 @@ def __init__( self._logger_provider = logger_provider or get_logger_provider() @staticmethod - def _get_attributes(record: logging.LogRecord) -> Attributes: + def _get_attributes(record: logging.LogRecord) -> LogAttributes: attributes = { k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS } @@ -636,7 +636,7 @@ def _get_logger_no_cache( name: str, version: str | None = None, schema_url: str | None = None, - attributes: Attributes | None = None, + attributes: LogAttributes | None = None, ) -> Logger: return Logger( self._resource, @@ -670,7 +670,7 @@ def get_logger( name: str, version: str | None = None, schema_url: str | None = None, - attributes: Attributes | None = None, + attributes: LogAttributes | None = None, ) -> Logger: if self._disabled: return NoOpLogger( From 84a03ed16ccb29151c543141998f48930546c790 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 31 Mar 2025 15:04:02 +0200 Subject: [PATCH 02/19] LogAttributes -> ExtendedAttributes --- .../opentelemetry/_logs/_internal/__init__.py | 16 ++++++++-------- .../src/opentelemetry/util/types.py | 2 +- opentelemetry-api/tests/logs/test_proxy.py | 4 ++-- .../sdk/_logs/_internal/__init__.py | 10 +++++----- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index de03cd636db..d00e4ca651a 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -44,7 +44,7 @@ from opentelemetry.trace.span import TraceFlags from opentelemetry.util._once import Once from opentelemetry.util._providers import _load_provider -from opentelemetry.util.types import AnyValue, LogAttributes +from opentelemetry.util.types import AnyValue, ExtendedAttributes _logger = getLogger(__name__) @@ -67,7 +67,7 @@ def __init__( severity_text: Optional[str] = None, severity_number: Optional[SeverityNumber] = None, body: AnyValue = None, - attributes: Optional[LogAttributes] = None, + attributes: Optional[ExtendedAttributes] = None, ): self.timestamp = timestamp if observed_timestamp is None: @@ -90,7 +90,7 @@ def __init__( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[LogAttributes] = None, + attributes: Optional[ExtendedAttributes] = None, ) -> None: super().__init__() self._name = name @@ -119,7 +119,7 @@ def __init__( # pylint: disable=super-init-not-called name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[LogAttributes] = None, + attributes: Optional[ExtendedAttributes] = None, ): self._name = name self._version = version @@ -158,7 +158,7 @@ def get_logger( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[LogAttributes] = None, + attributes: Optional[ExtendedAttributes] = None, ) -> Logger: """Returns a `Logger` for use by the given instrumentation library. @@ -196,7 +196,7 @@ def get_logger( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[LogAttributes] = None, + attributes: Optional[ExtendedAttributes] = None, ) -> Logger: """Returns a NoOpLogger.""" return NoOpLogger( @@ -210,7 +210,7 @@ def get_logger( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[LogAttributes] = None, + attributes: Optional[ExtendedAttributes] = None, ) -> Logger: if _LOGGER_PROVIDER: return _LOGGER_PROVIDER.get_logger( @@ -273,7 +273,7 @@ def get_logger( instrumenting_library_version: str = "", logger_provider: Optional[LoggerProvider] = None, schema_url: Optional[str] = None, - attributes: Optional[LogAttributes] = None, + attributes: Optional[ExtendedAttributes] = None, ) -> "Logger": """Returns a `Logger` for use within a python process. diff --git a/opentelemetry-api/src/opentelemetry/util/types.py b/opentelemetry-api/src/opentelemetry/util/types.py index 5e319fbcb73..aac701008d6 100644 --- a/opentelemetry-api/src/opentelemetry/util/types.py +++ b/opentelemetry-api/src/opentelemetry/util/types.py @@ -56,4 +56,4 @@ ..., ] -LogAttributes = Mapping[str, "AnyValue"] +ExtendedAttributes = Mapping[str, "AnyValue"] diff --git a/opentelemetry-api/tests/logs/test_proxy.py b/opentelemetry-api/tests/logs/test_proxy.py index 512754df89e..2b25aa17576 100644 --- a/opentelemetry-api/tests/logs/test_proxy.py +++ b/opentelemetry-api/tests/logs/test_proxy.py @@ -19,7 +19,7 @@ import opentelemetry._logs._internal as _logs_internal from opentelemetry import _logs from opentelemetry.test.globals_test import LoggingGlobalsTest -from opentelemetry.util.types import LogAttributes +from opentelemetry.util.types import ExtendedAttributes class TestProvider(_logs.NoOpLoggerProvider): @@ -28,7 +28,7 @@ def get_logger( name: str, version: typing.Optional[str] = None, schema_url: typing.Optional[str] = None, - attributes: typing.Optional[LogAttributes] = None, + attributes: typing.Optional[ExtendedAttributes] = None, ) -> _logs.Logger: return LoggerTest(name) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 7dad4a09660..8b50b866d2d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -52,7 +52,7 @@ get_current_span, ) from opentelemetry.trace.span import TraceFlags -from opentelemetry.util.types import AnyValue, LogAttributes +from opentelemetry.util.types import AnyValue, ExtendedAttributes _logger = logging.getLogger(__name__) @@ -182,7 +182,7 @@ def __init__( severity_number: SeverityNumber | None = None, body: AnyValue | None = None, resource: Resource | None = None, - attributes: LogAttributes | None = None, + attributes: ExtendedAttributes | None = None, limits: LogLimits | None = _UnsetLogLimits, ): super().__init__( @@ -477,7 +477,7 @@ def __init__( self._logger_provider = logger_provider or get_logger_provider() @staticmethod - def _get_attributes(record: logging.LogRecord) -> LogAttributes: + def _get_attributes(record: logging.LogRecord) -> ExtendedAttributes: attributes = { k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS } @@ -636,7 +636,7 @@ def _get_logger_no_cache( name: str, version: str | None = None, schema_url: str | None = None, - attributes: LogAttributes | None = None, + attributes: ExtendedAttributes | None = None, ) -> Logger: return Logger( self._resource, @@ -670,7 +670,7 @@ def get_logger( name: str, version: str | None = None, schema_url: str | None = None, - attributes: LogAttributes | None = None, + attributes: ExtendedAttributes | None = None, ) -> Logger: if self._disabled: return NoOpLogger( From 52b9b8cfc616fb9953aeecf4c9cffee602cd00e4 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Tue, 1 Apr 2025 10:06:30 +0200 Subject: [PATCH 03/19] Handle ExtendedAttributes in BoundedAttributes --- .../src/opentelemetry/attributes/__init__.py | 123 ++++++++++++++++-- .../tests/attributes/test_attributes.py | 104 ++++++++++++++- 2 files changed, 213 insertions(+), 14 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index 71121f84697..fd104ffd07f 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -118,6 +118,96 @@ def _clean_attribute( return None +def _clean_extended_attribute_value( + value: types.AttributeValue, max_len: Optional[int] +) -> Optional[Union[types.AttributeValue, Tuple[Union[str, int, float], ...]]]: + # for primitive types just return the value and eventually shorten the string length + if value is None or isinstance(value, _VALID_ATTR_VALUE_TYPES): + if max_len is not None and isinstance(value, str): + value = value[:max_len] + return value + + if isinstance(value, Mapping): + cleaned_dict = {} + for key, element in value.items(): + # skip invalid keys + if not (key and isinstance(key, str)): + _logger.warning( + "invalid key `%s`. must be non-empty string.", key + ) + continue + + cleaned_dict[key] = _clean_extended_attribute( + key=key, value=element, max_len=max_len + ) + + return cleaned_dict + + if isinstance(value, Sequence): + sequence_first_valid_type = None + cleaned_seq = [] + + for element in value: + if element is None: + cleaned_seq.append(element) + continue + + if max_len is not None and isinstance(element, str): + element = element[:max_len] + + element_type = type(element) + if element_type not in _VALID_ATTR_VALUE_TYPES: + return _clean_extended_attribute(element, max_len=max_len) + + # The type of the sequence must be homogeneous. The first non-None + # element determines the type of the sequence + if sequence_first_valid_type is None: + sequence_first_valid_type = element_type + # use equality instead of isinstance as isinstance(True, int) evaluates to True + elif element_type != sequence_first_valid_type: + _logger.warning( + "Mixed types %s and %s in attribute value sequence", + sequence_first_valid_type.__name__, + type(element).__name__, + ) + return None + + cleaned_seq.append(element) + + # Freeze mutable sequences defensively + return tuple(cleaned_seq) + + raise TypeError( + "Invalid type %s for attribute value. Expected one of %s or a " + "sequence of those types", + type(value).__name__, + [valid_type.__name__ for valid_type in _VALID_ANY_VALUE_TYPES], + ) + + +def _clean_extended_attribute( + key: str, value: types.AttributeValue, max_len: Optional[int] +) -> Optional[Union[types.AttributeValue, Tuple[Union[str, int, float], ...]]]: + """Checks if attribute value is valid and cleans it if required. + + The function returns the cleaned value or None if the value is not valid. + + An attribute value is valid if it is an AnyValue. + An attribute needs cleansing if: + - Its length is greater than the maximum allowed length. + """ + + if not (key and isinstance(key, str)): + _logger.warning("invalid key `%s`. must be non-empty string.", key) + return None + + try: + return _clean_extended_attribute_value(value, max_len=max_len) + except TypeError as exception: + _logger.warning(f"Attribute {key}: {exception}") + return None + + def _clean_attribute_value( value: types.AttributeValue, limit: Optional[int] ) -> Optional[types.AttributeValue]: @@ -149,6 +239,7 @@ def __init__( attributes: types.Attributes = None, immutable: bool = True, max_value_len: Optional[int] = None, + extended_attributes: bool = False, ): if maxlen is not None: if not isinstance(maxlen, int) or maxlen < 0: @@ -158,6 +249,7 @@ def __init__( self.maxlen = maxlen self.dropped = 0 self.max_value_len = max_value_len + self._extended_attributes = extended_attributes # OrderedDict is not used until the maxlen is reached for efficiency. self._dict: Union[ @@ -184,19 +276,24 @@ def __setitem__(self, key: str, value: types.AttributeValue) -> None: self.dropped += 1 return - value = _clean_attribute(key, value, self.max_value_len) # type: ignore - if value is not None: - if key in self._dict: - del self._dict[key] - elif ( - self.maxlen is not None and len(self._dict) == self.maxlen - ): - if not isinstance(self._dict, OrderedDict): - self._dict = OrderedDict(self._dict) - self._dict.popitem(last=False) # type: ignore - self.dropped += 1 - - self._dict[key] = value # type: ignore + if self._extended_attributes: + value = _clean_extended_attribute( + key, value, self.max_value_len + ) # type: ignore + else: + value = _clean_attribute(key, value, self.max_value_len) # type: ignore + if value is None: + return + + if key in self._dict: + del self._dict[key] + elif self.maxlen is not None and len(self._dict) == self.maxlen: + if not isinstance(self._dict, OrderedDict): + self._dict = OrderedDict(self._dict) + self._dict.popitem(last=False) # type: ignore + self.dropped += 1 + + self._dict[key] = value # type: ignore def __delitem__(self, key: str) -> None: if getattr(self, "_immutable", False): # type: ignore diff --git a/opentelemetry-api/tests/attributes/test_attributes.py b/opentelemetry-api/tests/attributes/test_attributes.py index cf6aecb41fa..7b9fcdd339e 100644 --- a/opentelemetry-api/tests/attributes/test_attributes.py +++ b/opentelemetry-api/tests/attributes/test_attributes.py @@ -17,7 +17,11 @@ import unittest from typing import MutableSequence -from opentelemetry.attributes import BoundedAttributes, _clean_attribute +from opentelemetry.attributes import ( + BoundedAttributes, + _clean_attribute, + _clean_extended_attribute, +) class TestAttributes(unittest.TestCase): @@ -89,6 +93,94 @@ def test_sequence_attr_decode(self): ) +class TestExtendedAttributes(unittest.TestCase): + # pylint: disable=invalid-name + def assertValid(self, value, key="k"): + expected = value + if isinstance(value, MutableSequence): + expected = tuple(value) + self.assertEqual(_clean_extended_attribute(key, value, None), expected) + + def assertInvalid(self, value, key="k"): + self.assertIsNone(_clean_extended_attribute(key, value, None)) + + def test_attribute_key_validation(self): + # only non-empty strings are valid keys + self.assertInvalid(1, "") + self.assertInvalid(1, 1) + self.assertInvalid(1, {}) + self.assertInvalid(1, []) + self.assertInvalid(1, b"1") + self.assertValid(1, "k") + self.assertValid(1, "1") + + def test_clean_extended_attribute(self): + self.assertInvalid([1, 2, 3.4, "ss", 4]) + self.assertInvalid([{}, 1, 2, 3.4, 4]) + self.assertInvalid(["sw", "lf", 3.4, "ss"]) + self.assertInvalid([1, 2, 3.4, 5]) + self.assertInvalid([1, True]) + self.assertValid(None) + self.assertValid(True) + self.assertValid("hi") + self.assertValid(3.4) + self.assertValid(15) + self.assertValid([1, 2, 3, 5]) + self.assertValid([1.2, 2.3, 3.4, 4.5]) + self.assertValid([True, False]) + self.assertValid(["ss", "dw", "fw"]) + self.assertValid([]) + # None in sequences are valid + self.assertValid(["A", None, None]) + self.assertValid(["A", None, None, "B"]) + self.assertValid([None, None]) + self.assertInvalid(["A", None, 1]) + self.assertInvalid([None, "A", None, 1]) + # mappings + self.assertValid({}) + self.assertValid({"k": "v"}) + + # test keys + self.assertValid("value", "key") + self.assertInvalid("value", "") + self.assertInvalid("value", None) + + def test_sequence_attr_decode(self): + seq = [ + None, + b"Content-Disposition", + b"Content-Type", + b"\x81", + b"Keep-Alive", + ] + self.assertEqual( + _clean_extended_attribute("headers", seq, None), tuple(seq) + ) + + def test_mapping(self): + mapping = { + "": "invalid", + b"bytes": "invalid", + "none": {"": "invalid"}, + "valid_primitive": "str", + "valid_sequence": ["str"], + "invalid_sequence": ["str", 1], + "valid_mapping": {"str": 1}, + "invalid_mapping": {"": 1}, + } + expected = { + "none": {}, + "valid_primitive": "str", + "valid_sequence": ("str",), + "invalid_sequence": None, + "valid_mapping": {"str": 1}, + "invalid_mapping": {}, + } + self.assertEqual( + _clean_extended_attribute("headers", mapping, None), expected + ) + + class TestBoundedAttributes(unittest.TestCase): # pylint: disable=consider-using-dict-items base = { @@ -196,3 +288,13 @@ def test_locking(self): for num in range(100): self.assertEqual(bdict[str(num)], num) + + def test_extended_attributes(self): + bdict = BoundedAttributes(extended_attributes=True, immutable=False) + with unittest.mock.patch( + "opentelemetry.attributes._clean_extended_attribute", + return_value="mock_value", + ) as clean_extended_attribute_mock: + bdict["key"] = "value" + + clean_extended_attribute_mock.assert_called_once() From e2959e45a0a7d8dc616b8e57fda34fe5ab8d028f Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 2 Apr 2025 15:56:23 +0200 Subject: [PATCH 04/19] opentelemetry-sdk: serialize extended attributes --- .../opentelemetry/sdk/_logs/_internal/__init__.py | 1 + opentelemetry-sdk/tests/logs/test_log_record.py | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 8b50b866d2d..71aef976237 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -200,6 +200,7 @@ def __init__( attributes=attributes if bool(attributes) else None, immutable=False, max_value_len=limits.max_attribute_length, + extended_attributes=True, ), } ) diff --git a/opentelemetry-sdk/tests/logs/test_log_record.py b/opentelemetry-sdk/tests/logs/test_log_record.py index f42d3a26ea4..4a0d58dc9b1 100644 --- a/opentelemetry-sdk/tests/logs/test_log_record.py +++ b/opentelemetry-sdk/tests/logs/test_log_record.py @@ -33,7 +33,12 @@ def test_log_record_to_json(self): "body": "a log line", "severity_number": None, "severity_text": None, - "attributes": None, + "attributes": { + "mapping": {"key": "value"}, + "none": None, + "sequence": [1, 2], + "str": "string", + }, "dropped_attributes": 0, "timestamp": "1970-01-01T00:00:00.000000Z", "observed_timestamp": "1970-01-01T00:00:00.000000Z", @@ -52,12 +57,18 @@ def test_log_record_to_json(self): observed_timestamp=0, body="a log line", resource=Resource({"service.name": "foo"}), + attributes={ + "mapping": {"key": "value"}, + "none": None, + "sequence": [1, 2], + "str": "string", + }, ) self.assertEqual(expected, actual.to_json(indent=4)) self.assertEqual( actual.to_json(indent=None), - '{"body": "a log line", "severity_number": null, "severity_text": null, "attributes": null, "dropped_attributes": 0, "timestamp": "1970-01-01T00:00:00.000000Z", "observed_timestamp": "1970-01-01T00:00:00.000000Z", "trace_id": "", "span_id": "", "trace_flags": null, "resource": {"attributes": {"service.name": "foo"}, "schema_url": ""}}', + '{"body": "a log line", "severity_number": null, "severity_text": null, "attributes": {"mapping": {"key": "value"}, "none": null, "sequence": [1, 2], "str": "string"}, "dropped_attributes": 0, "timestamp": "1970-01-01T00:00:00.000000Z", "observed_timestamp": "1970-01-01T00:00:00.000000Z", "trace_id": "", "span_id": "", "trace_flags": null, "resource": {"attributes": {"service.name": "foo"}, "schema_url": ""}}', ) def test_log_record_to_json_serializes_severity_number_as_int(self): From ab47594a15bba666aa7c53c2de8d6e90cb3771be Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 9 Dec 2024 12:46:43 +0100 Subject: [PATCH 05/19] Add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38a77be0937..952b1071cd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4521](https://github.com/open-telemetry/opentelemetry-python/pull/4521)) - opentelemetry-sdk: Fix serialization of objects in log handler ([#4528](https://github.com/open-telemetry/opentelemetry-python/pull/4528)) +- Fix serialization of extended attributes for logs signal + ([#4342](https://github.com/open-telemetry/opentelemetry-python/pull/4342)) ## Version 1.31.0/0.52b0 (2025-03-12) From b3d9ac9e03b06f65ce1fad9fdcd665c37b8f1a04 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 2 Apr 2025 16:26:50 +0200 Subject: [PATCH 06/19] Fix typing --- .../src/opentelemetry/attributes/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index fd104ffd07f..4241df760fb 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -120,7 +120,7 @@ def _clean_attribute( def _clean_extended_attribute_value( value: types.AttributeValue, max_len: Optional[int] -) -> Optional[Union[types.AttributeValue, Tuple[Union[str, int, float], ...]]]: +) -> types.AnyValue: # for primitive types just return the value and eventually shorten the string length if value is None or isinstance(value, _VALID_ATTR_VALUE_TYPES): if max_len is not None and isinstance(value, str): @@ -128,7 +128,7 @@ def _clean_extended_attribute_value( return value if isinstance(value, Mapping): - cleaned_dict = {} + cleaned_dict: dict[str, types.AnyValue] = {} for key, element in value.items(): # skip invalid keys if not (key and isinstance(key, str)): @@ -157,7 +157,9 @@ def _clean_extended_attribute_value( element_type = type(element) if element_type not in _VALID_ATTR_VALUE_TYPES: - return _clean_extended_attribute(element, max_len=max_len) + return _clean_extended_attribute_value( + element, max_len=max_len + ) # The type of the sequence must be homogeneous. The first non-None # element determines the type of the sequence @@ -187,7 +189,7 @@ def _clean_extended_attribute_value( def _clean_extended_attribute( key: str, value: types.AttributeValue, max_len: Optional[int] -) -> Optional[Union[types.AttributeValue, Tuple[Union[str, int, float], ...]]]: +) -> types.AnyValue: """Checks if attribute value is valid and cleans it if required. The function returns the cleaned value or None if the value is not valid. From b0d72c571ebd15e07a99d6605c6ab3cb49a6dd76 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 2 Apr 2025 16:39:58 +0200 Subject: [PATCH 07/19] Fix handling of not attribute values inside sequences --- opentelemetry-api/src/opentelemetry/attributes/__init__.py | 3 ++- opentelemetry-api/tests/attributes/test_attributes.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index 4241df760fb..a61faee5103 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -157,9 +157,10 @@ def _clean_extended_attribute_value( element_type = type(element) if element_type not in _VALID_ATTR_VALUE_TYPES: - return _clean_extended_attribute_value( + element = _clean_extended_attribute_value( element, max_len=max_len ) + element_type = type(element) # The type of the sequence must be homogeneous. The first non-None # element determines the type of the sequence diff --git a/opentelemetry-api/tests/attributes/test_attributes.py b/opentelemetry-api/tests/attributes/test_attributes.py index 7b9fcdd339e..5b7c67b6c37 100644 --- a/opentelemetry-api/tests/attributes/test_attributes.py +++ b/opentelemetry-api/tests/attributes/test_attributes.py @@ -139,6 +139,8 @@ def test_clean_extended_attribute(self): # mappings self.assertValid({}) self.assertValid({"k": "v"}) + # mappings in sequences + self.assertValid([{"k": "v"}]) # test keys self.assertValid("value", "key") From ae53317492b62edbf19d96eed9e7baab784fe7f0 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 2 Apr 2025 16:53:16 +0200 Subject: [PATCH 08/19] Please mypy --- opentelemetry-api/src/opentelemetry/attributes/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index a61faee5103..1db2ca9e6a6 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -145,7 +145,7 @@ def _clean_extended_attribute_value( if isinstance(value, Sequence): sequence_first_valid_type = None - cleaned_seq = [] + cleaned_seq: list[types.AnyValue] = [] for element in value: if element is None: From 2456614d7f1005da5d3192d6a75d770f99b0ecf2 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 2 Apr 2025 16:58:33 +0200 Subject: [PATCH 09/19] Please lint --- opentelemetry-api/src/opentelemetry/attributes/__init__.py | 7 +++---- opentelemetry-api/tests/attributes/test_attributes.py | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index 1db2ca9e6a6..834b1825c6e 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -181,10 +181,9 @@ def _clean_extended_attribute_value( return tuple(cleaned_seq) raise TypeError( - "Invalid type %s for attribute value. Expected one of %s or a " + f"Invalid type {type(value).__name__} for attribute value. " + f"Expected one of {[valid_type.__name__ for valid_type in _VALID_ANY_VALUE_TYPES]} or a " "sequence of those types", - type(value).__name__, - [valid_type.__name__ for valid_type in _VALID_ANY_VALUE_TYPES], ) @@ -207,7 +206,7 @@ def _clean_extended_attribute( try: return _clean_extended_attribute_value(value, max_len=max_len) except TypeError as exception: - _logger.warning(f"Attribute {key}: {exception}") + _logger.warning("Attribute %s: %s", key, exception) return None diff --git a/opentelemetry-api/tests/attributes/test_attributes.py b/opentelemetry-api/tests/attributes/test_attributes.py index 5b7c67b6c37..8a653387254 100644 --- a/opentelemetry-api/tests/attributes/test_attributes.py +++ b/opentelemetry-api/tests/attributes/test_attributes.py @@ -291,6 +291,7 @@ def test_locking(self): for num in range(100): self.assertEqual(bdict[str(num)], num) + # pylint: disable=no-self-use def test_extended_attributes(self): bdict = BoundedAttributes(extended_attributes=True, immutable=False) with unittest.mock.patch( From 007a75f1160030e97e5807710003b55c7e9af565 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 7 Apr 2025 16:33:47 +0200 Subject: [PATCH 10/19] More typing --- .../src/opentelemetry/_events/__init__.py | 16 ++++++++-------- .../opentelemetry/_logs/_internal/__init__.py | 5 +++-- .../src/opentelemetry/attributes/__init__.py | 2 +- .../tests/events/test_proxy_event.py | 4 ++-- .../opentelemetry/sdk/util/instrumentation.py | 6 +++--- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/_events/__init__.py b/opentelemetry-api/src/opentelemetry/_events/__init__.py index e1e6a675a52..e621c0bccf0 100644 --- a/opentelemetry-api/src/opentelemetry/_events/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_events/__init__.py @@ -25,7 +25,7 @@ from opentelemetry.trace.span import TraceFlags from opentelemetry.util._once import Once from opentelemetry.util._providers import _load_provider -from opentelemetry.util.types import Attributes +from opentelemetry.util.types import ExtendedAttributes _logger = getLogger(__name__) @@ -40,7 +40,7 @@ def __init__( trace_flags: Optional["TraceFlags"] = None, body: Optional[Any] = None, severity_number: Optional[SeverityNumber] = None, - attributes: Optional[Attributes] = None, + attributes: Optional[ExtendedAttributes] = None, ): attributes = attributes or {} event_attributes = {**attributes, "event.name": name} @@ -62,7 +62,7 @@ def __init__( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[Attributes] = None, + attributes: Optional[ExtendedAttributes] = None, ): self._name = name self._version = version @@ -85,7 +85,7 @@ def __init__( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[Attributes] = None, + attributes: Optional[ExtendedAttributes] = None, ): super().__init__( name=name, @@ -122,7 +122,7 @@ def get_event_logger( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[Attributes] = None, + attributes: Optional[ExtendedAttributes] = None, ) -> EventLogger: """Returns an EventLoggerProvider for use.""" @@ -133,7 +133,7 @@ def get_event_logger( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[Attributes] = None, + attributes: Optional[ExtendedAttributes] = None, ) -> EventLogger: return NoOpEventLogger( name, version=version, schema_url=schema_url, attributes=attributes @@ -146,7 +146,7 @@ def get_event_logger( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[Attributes] = None, + attributes: Optional[ExtendedAttributes] = None, ) -> EventLogger: if _EVENT_LOGGER_PROVIDER: return _EVENT_LOGGER_PROVIDER.get_event_logger( @@ -208,7 +208,7 @@ def get_event_logger( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[Attributes] = None, + attributes: Optional[ExtendedAttributes] = None, event_logger_provider: Optional[EventLoggerProvider] = None, ) -> "EventLogger": if event_logger_provider is None: diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index d00e4ca651a..93ea8e446d8 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -40,6 +40,7 @@ from typing import Optional, cast from opentelemetry._logs.severity import SeverityNumber +from opentelemetry.attributes import BoundedAttributes from opentelemetry.environment_variables import _OTEL_PYTHON_LOGGER_PROVIDER from opentelemetry.trace.span import TraceFlags from opentelemetry.util._once import Once @@ -67,7 +68,7 @@ def __init__( severity_text: Optional[str] = None, severity_number: Optional[SeverityNumber] = None, body: AnyValue = None, - attributes: Optional[ExtendedAttributes] = None, + attributes: Optional[BoundedAttributes] = None, ): self.timestamp = timestamp if observed_timestamp is None: @@ -79,7 +80,7 @@ def __init__( self.severity_text = severity_text self.severity_number = severity_number self.body = body - self.attributes = attributes + self.attributes: Optional[BoundedAttributes] = attributes class Logger(ABC): diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index 834b1825c6e..a7feb6ea736 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -238,7 +238,7 @@ class BoundedAttributes(MutableMapping): # type: ignore def __init__( self, maxlen: Optional[int] = None, - attributes: types.Attributes = None, + attributes: Optional[types.ExtendedAttributes] = None, immutable: bool = True, max_value_len: Optional[int] = None, extended_attributes: bool = False, diff --git a/opentelemetry-api/tests/events/test_proxy_event.py b/opentelemetry-api/tests/events/test_proxy_event.py index 736dcf35d60..aa68db4a124 100644 --- a/opentelemetry-api/tests/events/test_proxy_event.py +++ b/opentelemetry-api/tests/events/test_proxy_event.py @@ -4,7 +4,7 @@ import opentelemetry._events as events from opentelemetry.test.globals_test import EventsGlobalsTest -from opentelemetry.util.types import Attributes +from opentelemetry.util.types import ExtendedAttributes class TestProvider(events.NoOpEventLoggerProvider): @@ -13,7 +13,7 @@ def get_event_logger( name: str, version: typing.Optional[str] = None, schema_url: typing.Optional[str] = None, - attributes: typing.Optional[Attributes] = None, + attributes: typing.Optional[ExtendedAttributes] = None, ) -> events.EventLogger: return LoggerTest(name) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py index 6b45bf2a827..23865f0c4ef 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py @@ -17,7 +17,7 @@ from deprecated import deprecated from opentelemetry.attributes import BoundedAttributes -from opentelemetry.util.types import Attributes +from opentelemetry.util.types import ExtendedAttributes class InstrumentationInfo: @@ -92,7 +92,7 @@ def __init__( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[Attributes] = None, + attributes: Optional[ExtendedAttributes] = None, ) -> None: self._name = name self._version = version @@ -150,7 +150,7 @@ def name(self) -> str: return self._name @property - def attributes(self) -> Attributes: + def attributes(self) -> ExtendedAttributes: return self._attributes def to_json(self, indent: Optional[int] = 4) -> str: From 879f20ee5f2ca0159e4a7cfaed313e20bd9df602 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 7 Apr 2025 16:47:14 +0200 Subject: [PATCH 11/19] Even more typing fixes --- .../src/opentelemetry/_events/__init__.py | 5 ++++- .../src/opentelemetry/attributes/__init__.py | 12 ++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/_events/__init__.py b/opentelemetry-api/src/opentelemetry/_events/__init__.py index e621c0bccf0..dea1a2332c9 100644 --- a/opentelemetry-api/src/opentelemetry/_events/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_events/__init__.py @@ -43,7 +43,10 @@ def __init__( attributes: Optional[ExtendedAttributes] = None, ): attributes = attributes or {} - event_attributes = {**attributes, "event.name": name} + event_attributes: ExtendedAttributes = { + **attributes, + "event.name": name, + } super().__init__( timestamp=timestamp, trace_id=trace_id, diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index a7feb6ea736..b09af6296f2 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -119,7 +119,7 @@ def _clean_attribute( def _clean_extended_attribute_value( - value: types.AttributeValue, max_len: Optional[int] + value: types.AnyValue, max_len: Optional[int] ) -> types.AnyValue: # for primitive types just return the value and eventually shorten the string length if value is None or isinstance(value, _VALID_ATTR_VALUE_TYPES): @@ -188,7 +188,7 @@ def _clean_extended_attribute_value( def _clean_extended_attribute( - key: str, value: types.AttributeValue, max_len: Optional[int] + key: str, value: types.AnyValue, max_len: Optional[int] ) -> types.AnyValue: """Checks if attribute value is valid and cleans it if required. @@ -255,8 +255,8 @@ def __init__( # OrderedDict is not used until the maxlen is reached for efficiency. self._dict: Union[ - MutableMapping[str, types.AttributeValue], - OrderedDict[str, types.AttributeValue], + MutableMapping[str, types.AnyValue], + OrderedDict[str, types.AnyValue], ] = {} self._lock = threading.RLock() if attributes: @@ -267,10 +267,10 @@ def __init__( def __repr__(self) -> str: return f"{dict(self._dict)}" - def __getitem__(self, key: str) -> types.AttributeValue: + def __getitem__(self, key: str) -> types.AnyValue: return self._dict[key] - def __setitem__(self, key: str, value: types.AttributeValue) -> None: + def __setitem__(self, key: str, value: types.AnyValue) -> None: if getattr(self, "_immutable", False): # type: ignore raise TypeError with self._lock: From 576027a47fa7669848e08769edfec98813c4a47b Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 7 Apr 2025 17:03:10 +0200 Subject: [PATCH 12/19] Fix docs --- docs/conf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 5e8037488bf..0a739269036 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -154,6 +154,10 @@ "py:class", "_contextvars.Token", ), + ( + "py:class", + "AnyValue", + ), ] # Add any paths that contain templates here, relative to this directory. From cec6ef309decccfc49d0146a75e97a8829b05ff7 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 7 Apr 2025 17:15:10 +0200 Subject: [PATCH 13/19] Fix mypy --- .../src/opentelemetry/_events/__init__.py | 12 ++++++------ .../src/opentelemetry/attributes/__init__.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/_events/__init__.py b/opentelemetry-api/src/opentelemetry/_events/__init__.py index dea1a2332c9..fc8057a3f9e 100644 --- a/opentelemetry-api/src/opentelemetry/_events/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_events/__init__.py @@ -15,7 +15,7 @@ from abc import ABC, abstractmethod from logging import getLogger from os import environ -from typing import Any, Optional, cast +from typing import Optional, cast from opentelemetry._logs import LogRecord from opentelemetry._logs.severity import SeverityNumber @@ -25,7 +25,7 @@ from opentelemetry.trace.span import TraceFlags from opentelemetry.util._once import Once from opentelemetry.util._providers import _load_provider -from opentelemetry.util.types import ExtendedAttributes +from opentelemetry.util.types import AnyValue, ExtendedAttributes _logger = getLogger(__name__) @@ -38,12 +38,12 @@ def __init__( trace_id: Optional[int] = None, span_id: Optional[int] = None, trace_flags: Optional["TraceFlags"] = None, - body: Optional[Any] = None, + body: Optional[AnyValue] = None, severity_number: Optional[SeverityNumber] = None, attributes: Optional[ExtendedAttributes] = None, ): attributes = attributes or {} - event_attributes: ExtendedAttributes = { + event_attributes = { **attributes, "event.name": name, } @@ -52,9 +52,9 @@ def __init__( trace_id=trace_id, span_id=span_id, trace_flags=trace_flags, - body=body, # type: ignore + body=body, severity_number=severity_number, - attributes=event_attributes, + attributes=event_attributes, # type: ignore ) self.name = name diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index b09af6296f2..bdbea0875e0 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -160,7 +160,7 @@ def _clean_extended_attribute_value( element = _clean_extended_attribute_value( element, max_len=max_len ) - element_type = type(element) + element_type = type(element) # type: ignore # The type of the sequence must be homogeneous. The first non-None # element determines the type of the sequence @@ -281,7 +281,7 @@ def __setitem__(self, key: str, value: types.AnyValue) -> None: if self._extended_attributes: value = _clean_extended_attribute( key, value, self.max_value_len - ) # type: ignore + ) else: value = _clean_attribute(key, value, self.max_value_len) # type: ignore if value is None: From f048a49bddc69db6591a7990bec7f8fc18210936 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Tue, 8 Apr 2025 11:06:13 +0200 Subject: [PATCH 14/19] Update LogRecord attributes typing to match reality --- opentelemetry-api/src/opentelemetry/_events/__init__.py | 2 +- .../src/opentelemetry/_logs/_internal/__init__.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/_events/__init__.py b/opentelemetry-api/src/opentelemetry/_events/__init__.py index fc8057a3f9e..b995da3cb36 100644 --- a/opentelemetry-api/src/opentelemetry/_events/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_events/__init__.py @@ -54,7 +54,7 @@ def __init__( trace_flags=trace_flags, body=body, severity_number=severity_number, - attributes=event_attributes, # type: ignore + attributes=event_attributes, ) self.name = name diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index 93ea8e446d8..872e2c36e9c 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -37,7 +37,7 @@ from logging import getLogger from os import environ from time import time_ns -from typing import Optional, cast +from typing import Optional, Union, cast from opentelemetry._logs.severity import SeverityNumber from opentelemetry.attributes import BoundedAttributes @@ -68,7 +68,9 @@ def __init__( severity_text: Optional[str] = None, severity_number: Optional[SeverityNumber] = None, body: AnyValue = None, - attributes: Optional[BoundedAttributes] = None, + attributes: Optional[ + Union[BoundedAttributes, ExtendedAttributes] + ] = None, ): self.timestamp = timestamp if observed_timestamp is None: @@ -80,7 +82,7 @@ def __init__( self.severity_text = severity_text self.severity_number = severity_number self.body = body - self.attributes: Optional[BoundedAttributes] = attributes + self.attributes = attributes class Logger(ABC): From d0486557f94aa90907a0b5452f1f0506d54f0748 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Tue, 8 Apr 2025 17:43:58 +0200 Subject: [PATCH 15/19] More typing --- .../src/opentelemetry/_logs/_internal/__init__.py | 7 ++----- .../src/opentelemetry/sdk/_events/__init__.py | 6 +++--- .../src/opentelemetry/sdk/_logs/_internal/__init__.py | 9 ++++++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index 872e2c36e9c..d00e4ca651a 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -37,10 +37,9 @@ from logging import getLogger from os import environ from time import time_ns -from typing import Optional, Union, cast +from typing import Optional, cast from opentelemetry._logs.severity import SeverityNumber -from opentelemetry.attributes import BoundedAttributes from opentelemetry.environment_variables import _OTEL_PYTHON_LOGGER_PROVIDER from opentelemetry.trace.span import TraceFlags from opentelemetry.util._once import Once @@ -68,9 +67,7 @@ def __init__( severity_text: Optional[str] = None, severity_number: Optional[SeverityNumber] = None, body: AnyValue = None, - attributes: Optional[ - Union[BoundedAttributes, ExtendedAttributes] - ] = None, + attributes: Optional[ExtendedAttributes] = None, ): self.timestamp = timestamp if observed_timestamp is None: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py index ae16302546d..9774b431144 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py @@ -21,7 +21,7 @@ from opentelemetry._events import EventLoggerProvider as APIEventLoggerProvider from opentelemetry._logs import NoOpLogger, SeverityNumber, get_logger_provider from opentelemetry.sdk._logs import Logger, LoggerProvider, LogRecord -from opentelemetry.util.types import Attributes +from opentelemetry.util.types import ExtendedAttributes _logger = logging.getLogger(__name__) @@ -33,7 +33,7 @@ def __init__( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[Attributes] = None, + attributes: Optional[ExtendedAttributes] = None, ): super().__init__( name=name, @@ -74,7 +74,7 @@ def get_event_logger( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[Attributes] = None, + attributes: Optional[ExtendedAttributes] = None, ) -> EventLogger: if not name: _logger.warning("EventLogger created with invalid name: %s", name) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 71aef976237..166a7ee0773 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -24,7 +24,7 @@ from os import environ from threading import Lock from time import time_ns -from typing import Any, Callable, Tuple, Union # noqa +from typing import Any, Callable, Tuple, Union, cast # noqa from opentelemetry._logs import Logger as APILogger from opentelemetry._logs import LoggerProvider as APILoggerProvider @@ -251,8 +251,11 @@ def to_json(self, indent: int | None = 4) -> str: @property def dropped_attributes(self) -> int: - if self.attributes: - return self.attributes.dropped + attributes: BoundedAttributes = cast( + BoundedAttributes, self.attributes + ) + if attributes: + return attributes.dropped return 0 From f4f99148b347f8067461b76f19a034ca63064b03 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 11 Apr 2025 12:41:42 +0200 Subject: [PATCH 16/19] Move changelog to unreleased --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 952b1071cd4..d9e89b18034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Fix serialization of extended attributes for logs signal + ([#4342](https://github.com/open-telemetry/opentelemetry-python/pull/4342)) + ## Version 1.32.0/0.53b0 (2025-04-10) - Fix user agent in OTLP HTTP metrics exporter @@ -27,8 +30,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4521](https://github.com/open-telemetry/opentelemetry-python/pull/4521)) - opentelemetry-sdk: Fix serialization of objects in log handler ([#4528](https://github.com/open-telemetry/opentelemetry-python/pull/4528)) -- Fix serialization of extended attributes for logs signal - ([#4342](https://github.com/open-telemetry/opentelemetry-python/pull/4342)) ## Version 1.31.0/0.52b0 (2025-03-12) From ea6cf003f14c8ca21bfdb222dee6239407c4ab35 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Tue, 15 Apr 2025 10:41:10 +0200 Subject: [PATCH 17/19] ExtendedAttributes -> _ExtendedAttributes --- .../src/opentelemetry/_events/__init__.py | 16 ++++++++-------- .../opentelemetry/_logs/_internal/__init__.py | 16 ++++++++-------- .../src/opentelemetry/attributes/__init__.py | 2 +- .../src/opentelemetry/util/types.py | 2 +- .../tests/events/test_proxy_event.py | 4 ++-- opentelemetry-api/tests/logs/test_proxy.py | 4 ++-- .../src/opentelemetry/sdk/_events/__init__.py | 6 +++--- .../sdk/_logs/_internal/__init__.py | 10 +++++----- 8 files changed, 30 insertions(+), 30 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/_events/__init__.py b/opentelemetry-api/src/opentelemetry/_events/__init__.py index b995da3cb36..f073b223345 100644 --- a/opentelemetry-api/src/opentelemetry/_events/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_events/__init__.py @@ -25,7 +25,7 @@ from opentelemetry.trace.span import TraceFlags from opentelemetry.util._once import Once from opentelemetry.util._providers import _load_provider -from opentelemetry.util.types import AnyValue, ExtendedAttributes +from opentelemetry.util.types import AnyValue, _ExtendedAttributes _logger = getLogger(__name__) @@ -40,7 +40,7 @@ def __init__( trace_flags: Optional["TraceFlags"] = None, body: Optional[AnyValue] = None, severity_number: Optional[SeverityNumber] = None, - attributes: Optional[ExtendedAttributes] = None, + attributes: Optional[_ExtendedAttributes] = None, ): attributes = attributes or {} event_attributes = { @@ -65,7 +65,7 @@ def __init__( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[ExtendedAttributes] = None, + attributes: Optional[_ExtendedAttributes] = None, ): self._name = name self._version = version @@ -88,7 +88,7 @@ def __init__( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[ExtendedAttributes] = None, + attributes: Optional[_ExtendedAttributes] = None, ): super().__init__( name=name, @@ -125,7 +125,7 @@ def get_event_logger( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[ExtendedAttributes] = None, + attributes: Optional[_ExtendedAttributes] = None, ) -> EventLogger: """Returns an EventLoggerProvider for use.""" @@ -136,7 +136,7 @@ def get_event_logger( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[ExtendedAttributes] = None, + attributes: Optional[_ExtendedAttributes] = None, ) -> EventLogger: return NoOpEventLogger( name, version=version, schema_url=schema_url, attributes=attributes @@ -149,7 +149,7 @@ def get_event_logger( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[ExtendedAttributes] = None, + attributes: Optional[_ExtendedAttributes] = None, ) -> EventLogger: if _EVENT_LOGGER_PROVIDER: return _EVENT_LOGGER_PROVIDER.get_event_logger( @@ -211,7 +211,7 @@ def get_event_logger( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[ExtendedAttributes] = None, + attributes: Optional[_ExtendedAttributes] = None, event_logger_provider: Optional[EventLoggerProvider] = None, ) -> "EventLogger": if event_logger_provider is None: diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index d00e4ca651a..71fc97b0aaa 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -44,7 +44,7 @@ from opentelemetry.trace.span import TraceFlags from opentelemetry.util._once import Once from opentelemetry.util._providers import _load_provider -from opentelemetry.util.types import AnyValue, ExtendedAttributes +from opentelemetry.util.types import AnyValue, _ExtendedAttributes _logger = getLogger(__name__) @@ -67,7 +67,7 @@ def __init__( severity_text: Optional[str] = None, severity_number: Optional[SeverityNumber] = None, body: AnyValue = None, - attributes: Optional[ExtendedAttributes] = None, + attributes: Optional[_ExtendedAttributes] = None, ): self.timestamp = timestamp if observed_timestamp is None: @@ -90,7 +90,7 @@ def __init__( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[ExtendedAttributes] = None, + attributes: Optional[_ExtendedAttributes] = None, ) -> None: super().__init__() self._name = name @@ -119,7 +119,7 @@ def __init__( # pylint: disable=super-init-not-called name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[ExtendedAttributes] = None, + attributes: Optional[_ExtendedAttributes] = None, ): self._name = name self._version = version @@ -158,7 +158,7 @@ def get_logger( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[ExtendedAttributes] = None, + attributes: Optional[_ExtendedAttributes] = None, ) -> Logger: """Returns a `Logger` for use by the given instrumentation library. @@ -196,7 +196,7 @@ def get_logger( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[ExtendedAttributes] = None, + attributes: Optional[_ExtendedAttributes] = None, ) -> Logger: """Returns a NoOpLogger.""" return NoOpLogger( @@ -210,7 +210,7 @@ def get_logger( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[ExtendedAttributes] = None, + attributes: Optional[_ExtendedAttributes] = None, ) -> Logger: if _LOGGER_PROVIDER: return _LOGGER_PROVIDER.get_logger( @@ -273,7 +273,7 @@ def get_logger( instrumenting_library_version: str = "", logger_provider: Optional[LoggerProvider] = None, schema_url: Optional[str] = None, - attributes: Optional[ExtendedAttributes] = None, + attributes: Optional[_ExtendedAttributes] = None, ) -> "Logger": """Returns a `Logger` for use within a python process. diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index bdbea0875e0..fc3d494631a 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -238,7 +238,7 @@ class BoundedAttributes(MutableMapping): # type: ignore def __init__( self, maxlen: Optional[int] = None, - attributes: Optional[types.ExtendedAttributes] = None, + attributes: Optional[types._ExtendedAttributes] = None, immutable: bool = True, max_value_len: Optional[int] = None, extended_attributes: bool = False, diff --git a/opentelemetry-api/src/opentelemetry/util/types.py b/opentelemetry-api/src/opentelemetry/util/types.py index aac701008d6..7455c741c93 100644 --- a/opentelemetry-api/src/opentelemetry/util/types.py +++ b/opentelemetry-api/src/opentelemetry/util/types.py @@ -56,4 +56,4 @@ ..., ] -ExtendedAttributes = Mapping[str, "AnyValue"] +_ExtendedAttributes = Mapping[str, "AnyValue"] diff --git a/opentelemetry-api/tests/events/test_proxy_event.py b/opentelemetry-api/tests/events/test_proxy_event.py index aa68db4a124..44121a97d46 100644 --- a/opentelemetry-api/tests/events/test_proxy_event.py +++ b/opentelemetry-api/tests/events/test_proxy_event.py @@ -4,7 +4,7 @@ import opentelemetry._events as events from opentelemetry.test.globals_test import EventsGlobalsTest -from opentelemetry.util.types import ExtendedAttributes +from opentelemetry.util.types import _ExtendedAttributes class TestProvider(events.NoOpEventLoggerProvider): @@ -13,7 +13,7 @@ def get_event_logger( name: str, version: typing.Optional[str] = None, schema_url: typing.Optional[str] = None, - attributes: typing.Optional[ExtendedAttributes] = None, + attributes: typing.Optional[_ExtendedAttributes] = None, ) -> events.EventLogger: return LoggerTest(name) diff --git a/opentelemetry-api/tests/logs/test_proxy.py b/opentelemetry-api/tests/logs/test_proxy.py index 2b25aa17576..64c024c3fa1 100644 --- a/opentelemetry-api/tests/logs/test_proxy.py +++ b/opentelemetry-api/tests/logs/test_proxy.py @@ -19,7 +19,7 @@ import opentelemetry._logs._internal as _logs_internal from opentelemetry import _logs from opentelemetry.test.globals_test import LoggingGlobalsTest -from opentelemetry.util.types import ExtendedAttributes +from opentelemetry.util.types import _ExtendedAttributes class TestProvider(_logs.NoOpLoggerProvider): @@ -28,7 +28,7 @@ def get_logger( name: str, version: typing.Optional[str] = None, schema_url: typing.Optional[str] = None, - attributes: typing.Optional[ExtendedAttributes] = None, + attributes: typing.Optional[_ExtendedAttributes] = None, ) -> _logs.Logger: return LoggerTest(name) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py index 9774b431144..c427a48e2f8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py @@ -21,7 +21,7 @@ from opentelemetry._events import EventLoggerProvider as APIEventLoggerProvider from opentelemetry._logs import NoOpLogger, SeverityNumber, get_logger_provider from opentelemetry.sdk._logs import Logger, LoggerProvider, LogRecord -from opentelemetry.util.types import ExtendedAttributes +from opentelemetry.util.types import _ExtendedAttributes _logger = logging.getLogger(__name__) @@ -33,7 +33,7 @@ def __init__( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[ExtendedAttributes] = None, + attributes: Optional[_ExtendedAttributes] = None, ): super().__init__( name=name, @@ -74,7 +74,7 @@ def get_event_logger( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[ExtendedAttributes] = None, + attributes: Optional[_ExtendedAttributes] = None, ) -> EventLogger: if not name: _logger.warning("EventLogger created with invalid name: %s", name) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 166a7ee0773..58872f68020 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -52,7 +52,7 @@ get_current_span, ) from opentelemetry.trace.span import TraceFlags -from opentelemetry.util.types import AnyValue, ExtendedAttributes +from opentelemetry.util.types import AnyValue, _ExtendedAttributes _logger = logging.getLogger(__name__) @@ -182,7 +182,7 @@ def __init__( severity_number: SeverityNumber | None = None, body: AnyValue | None = None, resource: Resource | None = None, - attributes: ExtendedAttributes | None = None, + attributes: _ExtendedAttributes | None = None, limits: LogLimits | None = _UnsetLogLimits, ): super().__init__( @@ -481,7 +481,7 @@ def __init__( self._logger_provider = logger_provider or get_logger_provider() @staticmethod - def _get_attributes(record: logging.LogRecord) -> ExtendedAttributes: + def _get_attributes(record: logging.LogRecord) -> _ExtendedAttributes: attributes = { k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS } @@ -640,7 +640,7 @@ def _get_logger_no_cache( name: str, version: str | None = None, schema_url: str | None = None, - attributes: ExtendedAttributes | None = None, + attributes: _ExtendedAttributes | None = None, ) -> Logger: return Logger( self._resource, @@ -674,7 +674,7 @@ def get_logger( name: str, version: str | None = None, schema_url: str | None = None, - attributes: ExtendedAttributes | None = None, + attributes: _ExtendedAttributes | None = None, ) -> Logger: if self._disabled: return NoOpLogger( From 879ab21873349a002501171a0a3bb3096be7d958 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Tue, 15 Apr 2025 10:41:33 +0200 Subject: [PATCH 18/19] opentelemetry-sdk: keep instrumentation scope attributes as Attributes --- .../src/opentelemetry/sdk/util/instrumentation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py index 23865f0c4ef..6b45bf2a827 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py @@ -17,7 +17,7 @@ from deprecated import deprecated from opentelemetry.attributes import BoundedAttributes -from opentelemetry.util.types import ExtendedAttributes +from opentelemetry.util.types import Attributes class InstrumentationInfo: @@ -92,7 +92,7 @@ def __init__( name: str, version: Optional[str] = None, schema_url: Optional[str] = None, - attributes: Optional[ExtendedAttributes] = None, + attributes: Optional[Attributes] = None, ) -> None: self._name = name self._version = version @@ -150,7 +150,7 @@ def name(self) -> str: return self._name @property - def attributes(self) -> ExtendedAttributes: + def attributes(self) -> Attributes: return self._attributes def to_json(self, indent: Optional[int] = 4) -> str: From 1e6192216c07d3f73e9471e9cc1ee1e049fc5e72 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 16 Apr 2025 17:58:19 +0200 Subject: [PATCH 19/19] exporter/otlp: allow export of none values in logs attributes --- .../otlp/proto/common/_internal/__init__.py | 9 ++- .../common/_internal/_log_encoder/__init__.py | 4 +- .../tests/test_log_encoder.py | 78 +++++++++++++++++-- 3 files changed, 80 insertions(+), 11 deletions(-) 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 d1793a734ad..2f49502cf1d 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 @@ -45,7 +45,7 @@ ) from opentelemetry.sdk.trace import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope -from opentelemetry.util.types import Attributes +from opentelemetry.util.types import _ExtendedAttributes _logger = logging.getLogger(__name__) @@ -136,14 +136,17 @@ def _encode_trace_id(trace_id: int) -> bytes: def _encode_attributes( - attributes: Attributes, + attributes: _ExtendedAttributes, + allow_null: bool = False, ) -> Optional[List[PB2KeyValue]]: if attributes: pb2_attributes = [] for key, value in attributes.items(): # pylint: disable=broad-exception-caught try: - pb2_attributes.append(_encode_key_value(key, value)) + pb2_attributes.append( + _encode_key_value(key, value, allow_null=allow_null) + ) except Exception as error: _logger.exception("Failed to encode key %s: %s", key, error) else: diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py index 9cd44844d06..9d713cb7ff0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py @@ -57,7 +57,9 @@ def _encode_log(log_data: LogData) -> PB2LogRecord: flags=int(log_data.log_record.trace_flags), body=_encode_value(body, allow_null=True), severity_text=log_data.log_record.severity_text, - attributes=_encode_attributes(log_data.log_record.attributes), + attributes=_encode_attributes( + log_data.log_record.attributes, allow_null=True + ), dropped_attributes_count=log_data.log_record.dropped_attributes, severity_number=log_data.log_record.severity_number.value, ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py index 2c4e39eab10..4c2b54aad2b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py @@ -225,7 +225,28 @@ def _get_sdk_log_data() -> List[LogData]: ), ) - return [log1, log2, log3, log4, log5, log6, log7] + log8 = LogData( + log_record=SDKLogRecord( + timestamp=1644650584292683044, + observed_timestamp=1644650584292683044, + trace_id=212592107417388365804938480559624925566, + span_id=6077757853989569466, + trace_flags=TraceFlags(0x01), + severity_text="INFO", + severity_number=SeverityNumber.INFO, + body="Test export of extended attributes", + resource=SDKResource({}), + attributes={ + "extended": { + "sequence": [{"inner": "mapping", "none": None}] + } + }, + ), + instrumentation_scope=InstrumentationScope( + "extended_name", "extended_version" + ), + ) + return [log1, log2, log3, log4, log5, log6, log7, log8] def get_test_logs( self, @@ -265,7 +286,8 @@ def get_test_logs( "Do not go gentle into that good night. Rage, rage against the dying of the light" ), attributes=_encode_attributes( - {"a": 1, "b": "c"} + {"a": 1, "b": "c"}, + allow_null=True, ), ) ], @@ -295,7 +317,8 @@ def get_test_logs( { "filename": "model.py", "func_name": "run_method", - } + }, + allow_null=True, ), ) ], @@ -326,7 +349,8 @@ def get_test_logs( { "filename": "model.py", "func_name": "run_method", - } + }, + allow_null=True, ), ) ], @@ -336,7 +360,8 @@ def get_test_logs( name="scope_with_attributes", version="scope_with_attributes_version", attributes=_encode_attributes( - {"one": 1, "two": "2"} + {"one": 1, "two": "2"}, + allow_null=True, ), ), schema_url="instrumentation_schema_url", @@ -360,7 +385,8 @@ def get_test_logs( { "filename": "model.py", "func_name": "run_method", - } + }, + allow_null=True, ), ) ], @@ -416,7 +442,8 @@ def get_test_logs( severity_number=SeverityNumber.DEBUG.value, body=_encode_value("To our galaxy"), attributes=_encode_attributes( - {"a": 1, "b": "c"} + {"a": 1, "b": "c"}, + allow_null=True, ), ), ], @@ -471,6 +498,43 @@ def get_test_logs( ), ], ), + PB2ScopeLogs( + scope=PB2InstrumentationScope( + name="extended_name", + version="extended_version", + ), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650584292683044, + observed_time_unix_nano=1644650584292683044, + trace_id=_encode_trace_id( + 212592107417388365804938480559624925566 + ), + span_id=_encode_span_id( + 6077757853989569466, + ), + flags=int(TraceFlags(0x01)), + severity_text="INFO", + severity_number=SeverityNumber.INFO.value, + body=_encode_value( + "Test export of extended attributes" + ), + attributes=_encode_attributes( + { + "extended": { + "sequence": [ + { + "inner": "mapping", + "none": None, + } + ] + } + }, + allow_null=True, + ), + ), + ], + ), ], ), ]