diff --git a/CHANGELOG.md b/CHANGELOG.md index 38a77be0937..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 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. 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, + ), + ), + ], + ), ], ), ] diff --git a/opentelemetry-api/src/opentelemetry/_events/__init__.py b/opentelemetry-api/src/opentelemetry/_events/__init__.py index e1e6a675a52..f073b223345 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 Attributes +from opentelemetry.util.types import AnyValue, _ExtendedAttributes _logger = getLogger(__name__) @@ -38,18 +38,21 @@ 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[Attributes] = None, + attributes: Optional[_ExtendedAttributes] = None, ): attributes = attributes or {} - event_attributes = {**attributes, "event.name": name} + event_attributes = { + **attributes, + "event.name": name, + } super().__init__( timestamp=timestamp, 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, ) @@ -62,7 +65,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 +88,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 +125,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 +136,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 +149,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 +211,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 f20bd8507e5..71fc97b0aaa 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, _ExtendedAttributes _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[_ExtendedAttributes] = 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[_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[Attributes] = 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[Attributes] = 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[Attributes] = 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[Attributes] = 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[Attributes] = 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 71121f84697..fc3d494631a 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -118,6 +118,98 @@ def _clean_attribute( return None +def _clean_extended_attribute_value( + 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): + if max_len is not None and isinstance(value, str): + value = value[:max_len] + return value + + if isinstance(value, Mapping): + cleaned_dict: dict[str, types.AnyValue] = {} + 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: list[types.AnyValue] = [] + + 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: + element = _clean_extended_attribute_value( + element, max_len=max_len + ) + 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 + 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( + 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", + ) + + +def _clean_extended_attribute( + key: str, value: types.AnyValue, max_len: Optional[int] +) -> 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. + + 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("Attribute %s: %s", key, exception) + return None + + def _clean_attribute_value( value: types.AttributeValue, limit: Optional[int] ) -> Optional[types.AttributeValue]: @@ -146,9 +238,10 @@ 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, ): if maxlen is not None: if not isinstance(maxlen, int) or maxlen < 0: @@ -158,11 +251,12 @@ 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[ - MutableMapping[str, types.AttributeValue], - OrderedDict[str, types.AttributeValue], + MutableMapping[str, types.AnyValue], + OrderedDict[str, types.AnyValue], ] = {} self._lock = threading.RLock() if attributes: @@ -173,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: @@ -184,19 +278,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 + ) + 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/src/opentelemetry/util/types.py b/opentelemetry-api/src/opentelemetry/util/types.py index be311faf555..7455c741c93 100644 --- a/opentelemetry-api/src/opentelemetry/util/types.py +++ b/opentelemetry-api/src/opentelemetry/util/types.py @@ -55,3 +55,5 @@ ], ..., ] + +_ExtendedAttributes = Mapping[str, "AnyValue"] diff --git a/opentelemetry-api/tests/attributes/test_attributes.py b/opentelemetry-api/tests/attributes/test_attributes.py index cf6aecb41fa..8a653387254 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,96 @@ 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"}) + # mappings in sequences + 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 +290,14 @@ 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( + "opentelemetry.attributes._clean_extended_attribute", + return_value="mock_value", + ) as clean_extended_attribute_mock: + bdict["key"] = "value" + + clean_extended_attribute_mock.assert_called_once() diff --git a/opentelemetry-api/tests/events/test_proxy_event.py b/opentelemetry-api/tests/events/test_proxy_event.py index 736dcf35d60..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 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-api/tests/logs/test_proxy.py b/opentelemetry-api/tests/logs/test_proxy.py index 8e87ceb96ea..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 Attributes +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[Attributes] = 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 ae16302546d..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 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 5d17c39f332..58872f68020 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 @@ -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, _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: Attributes | None = None, + attributes: _ExtendedAttributes | None = None, limits: LogLimits | None = _UnsetLogLimits, ): super().__init__( @@ -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, ), } ) @@ -250,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 @@ -477,7 +481,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) -> _ExtendedAttributes: attributes = { k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS } @@ -636,7 +640,7 @@ def _get_logger_no_cache( name: str, version: str | None = None, schema_url: str | None = None, - attributes: Attributes | None = None, + attributes: _ExtendedAttributes | None = None, ) -> Logger: return Logger( self._resource, @@ -670,7 +674,7 @@ def get_logger( name: str, version: str | None = None, schema_url: str | None = None, - attributes: Attributes | None = None, + attributes: _ExtendedAttributes | None = None, ) -> Logger: if self._disabled: return NoOpLogger( 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):