Skip to content

Commit 742171e

Browse files
authored
logs: fix serialization of Extended attributes (open-telemetry#4342)
* logs: introduce LogAttributes type Logs attribute accepts AnyValue as AttributeValue add a type to describe that and start using it. * LogAttributes -> ExtendedAttributes * Handle ExtendedAttributes in BoundedAttributes * opentelemetry-sdk: serialize extended attributes * Add changelog * Fix typing * Fix handling of not attribute values inside sequences * Please mypy * Please lint * More typing * Even more typing fixes * Fix docs * Fix mypy * Update LogRecord attributes typing to match reality * More typing * Move changelog to unreleased * ExtendedAttributes -> _ExtendedAttributes * opentelemetry-sdk: keep instrumentation scope attributes as Attributes * exporter/otlp: allow export of none values in logs attributes
1 parent e5a9307 commit 742171e

File tree

15 files changed

+370
-70
lines changed

15 files changed

+370
-70
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
- Fix serialization of extended attributes for logs signal
11+
([#4342](https://github.com/open-telemetry/opentelemetry-python/pull/4342))
12+
1013
## Version 1.32.0/0.53b0 (2025-04-10)
1114

1215
- Fix user agent in OTLP HTTP metrics exporter

docs/conf.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@
154154
"py:class",
155155
"_contextvars.Token",
156156
),
157+
(
158+
"py:class",
159+
"AnyValue",
160+
),
157161
]
158162

159163
# Add any paths that contain templates here, relative to this directory.

exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
)
4646
from opentelemetry.sdk.trace import Resource
4747
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
48-
from opentelemetry.util.types import Attributes
48+
from opentelemetry.util.types import _ExtendedAttributes
4949

5050
_logger = logging.getLogger(__name__)
5151

@@ -136,14 +136,17 @@ def _encode_trace_id(trace_id: int) -> bytes:
136136

137137

138138
def _encode_attributes(
139-
attributes: Attributes,
139+
attributes: _ExtendedAttributes,
140+
allow_null: bool = False,
140141
) -> Optional[List[PB2KeyValue]]:
141142
if attributes:
142143
pb2_attributes = []
143144
for key, value in attributes.items():
144145
# pylint: disable=broad-exception-caught
145146
try:
146-
pb2_attributes.append(_encode_key_value(key, value))
147+
pb2_attributes.append(
148+
_encode_key_value(key, value, allow_null=allow_null)
149+
)
147150
except Exception as error:
148151
_logger.exception("Failed to encode key %s: %s", key, error)
149152
else:

exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ def _encode_log(log_data: LogData) -> PB2LogRecord:
5757
flags=int(log_data.log_record.trace_flags),
5858
body=_encode_value(body, allow_null=True),
5959
severity_text=log_data.log_record.severity_text,
60-
attributes=_encode_attributes(log_data.log_record.attributes),
60+
attributes=_encode_attributes(
61+
log_data.log_record.attributes, allow_null=True
62+
),
6163
dropped_attributes_count=log_data.log_record.dropped_attributes,
6264
severity_number=log_data.log_record.severity_number.value,
6365
)

exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,28 @@ def _get_sdk_log_data() -> List[LogData]:
225225
),
226226
)
227227

228-
return [log1, log2, log3, log4, log5, log6, log7]
228+
log8 = LogData(
229+
log_record=SDKLogRecord(
230+
timestamp=1644650584292683044,
231+
observed_timestamp=1644650584292683044,
232+
trace_id=212592107417388365804938480559624925566,
233+
span_id=6077757853989569466,
234+
trace_flags=TraceFlags(0x01),
235+
severity_text="INFO",
236+
severity_number=SeverityNumber.INFO,
237+
body="Test export of extended attributes",
238+
resource=SDKResource({}),
239+
attributes={
240+
"extended": {
241+
"sequence": [{"inner": "mapping", "none": None}]
242+
}
243+
},
244+
),
245+
instrumentation_scope=InstrumentationScope(
246+
"extended_name", "extended_version"
247+
),
248+
)
249+
return [log1, log2, log3, log4, log5, log6, log7, log8]
229250

230251
def get_test_logs(
231252
self,
@@ -265,7 +286,8 @@ def get_test_logs(
265286
"Do not go gentle into that good night. Rage, rage against the dying of the light"
266287
),
267288
attributes=_encode_attributes(
268-
{"a": 1, "b": "c"}
289+
{"a": 1, "b": "c"},
290+
allow_null=True,
269291
),
270292
)
271293
],
@@ -295,7 +317,8 @@ def get_test_logs(
295317
{
296318
"filename": "model.py",
297319
"func_name": "run_method",
298-
}
320+
},
321+
allow_null=True,
299322
),
300323
)
301324
],
@@ -326,7 +349,8 @@ def get_test_logs(
326349
{
327350
"filename": "model.py",
328351
"func_name": "run_method",
329-
}
352+
},
353+
allow_null=True,
330354
),
331355
)
332356
],
@@ -336,7 +360,8 @@ def get_test_logs(
336360
name="scope_with_attributes",
337361
version="scope_with_attributes_version",
338362
attributes=_encode_attributes(
339-
{"one": 1, "two": "2"}
363+
{"one": 1, "two": "2"},
364+
allow_null=True,
340365
),
341366
),
342367
schema_url="instrumentation_schema_url",
@@ -360,7 +385,8 @@ def get_test_logs(
360385
{
361386
"filename": "model.py",
362387
"func_name": "run_method",
363-
}
388+
},
389+
allow_null=True,
364390
),
365391
)
366392
],
@@ -416,7 +442,8 @@ def get_test_logs(
416442
severity_number=SeverityNumber.DEBUG.value,
417443
body=_encode_value("To our galaxy"),
418444
attributes=_encode_attributes(
419-
{"a": 1, "b": "c"}
445+
{"a": 1, "b": "c"},
446+
allow_null=True,
420447
),
421448
),
422449
],
@@ -471,6 +498,43 @@ def get_test_logs(
471498
),
472499
],
473500
),
501+
PB2ScopeLogs(
502+
scope=PB2InstrumentationScope(
503+
name="extended_name",
504+
version="extended_version",
505+
),
506+
log_records=[
507+
PB2LogRecord(
508+
time_unix_nano=1644650584292683044,
509+
observed_time_unix_nano=1644650584292683044,
510+
trace_id=_encode_trace_id(
511+
212592107417388365804938480559624925566
512+
),
513+
span_id=_encode_span_id(
514+
6077757853989569466,
515+
),
516+
flags=int(TraceFlags(0x01)),
517+
severity_text="INFO",
518+
severity_number=SeverityNumber.INFO.value,
519+
body=_encode_value(
520+
"Test export of extended attributes"
521+
),
522+
attributes=_encode_attributes(
523+
{
524+
"extended": {
525+
"sequence": [
526+
{
527+
"inner": "mapping",
528+
"none": None,
529+
}
530+
]
531+
}
532+
},
533+
allow_null=True,
534+
),
535+
),
536+
],
537+
),
474538
],
475539
),
476540
]

opentelemetry-api/src/opentelemetry/_events/__init__.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from abc import ABC, abstractmethod
1616
from logging import getLogger
1717
from os import environ
18-
from typing import Any, Optional, cast
18+
from typing import Optional, cast
1919

2020
from opentelemetry._logs import LogRecord
2121
from opentelemetry._logs.severity import SeverityNumber
@@ -25,7 +25,7 @@
2525
from opentelemetry.trace.span import TraceFlags
2626
from opentelemetry.util._once import Once
2727
from opentelemetry.util._providers import _load_provider
28-
from opentelemetry.util.types import Attributes
28+
from opentelemetry.util.types import AnyValue, _ExtendedAttributes
2929

3030
_logger = getLogger(__name__)
3131

@@ -38,18 +38,21 @@ def __init__(
3838
trace_id: Optional[int] = None,
3939
span_id: Optional[int] = None,
4040
trace_flags: Optional["TraceFlags"] = None,
41-
body: Optional[Any] = None,
41+
body: Optional[AnyValue] = None,
4242
severity_number: Optional[SeverityNumber] = None,
43-
attributes: Optional[Attributes] = None,
43+
attributes: Optional[_ExtendedAttributes] = None,
4444
):
4545
attributes = attributes or {}
46-
event_attributes = {**attributes, "event.name": name}
46+
event_attributes = {
47+
**attributes,
48+
"event.name": name,
49+
}
4750
super().__init__(
4851
timestamp=timestamp,
4952
trace_id=trace_id,
5053
span_id=span_id,
5154
trace_flags=trace_flags,
52-
body=body, # type: ignore
55+
body=body,
5356
severity_number=severity_number,
5457
attributes=event_attributes,
5558
)
@@ -62,7 +65,7 @@ def __init__(
6265
name: str,
6366
version: Optional[str] = None,
6467
schema_url: Optional[str] = None,
65-
attributes: Optional[Attributes] = None,
68+
attributes: Optional[_ExtendedAttributes] = None,
6669
):
6770
self._name = name
6871
self._version = version
@@ -85,7 +88,7 @@ def __init__(
8588
name: str,
8689
version: Optional[str] = None,
8790
schema_url: Optional[str] = None,
88-
attributes: Optional[Attributes] = None,
91+
attributes: Optional[_ExtendedAttributes] = None,
8992
):
9093
super().__init__(
9194
name=name,
@@ -122,7 +125,7 @@ def get_event_logger(
122125
name: str,
123126
version: Optional[str] = None,
124127
schema_url: Optional[str] = None,
125-
attributes: Optional[Attributes] = None,
128+
attributes: Optional[_ExtendedAttributes] = None,
126129
) -> EventLogger:
127130
"""Returns an EventLoggerProvider for use."""
128131

@@ -133,7 +136,7 @@ def get_event_logger(
133136
name: str,
134137
version: Optional[str] = None,
135138
schema_url: Optional[str] = None,
136-
attributes: Optional[Attributes] = None,
139+
attributes: Optional[_ExtendedAttributes] = None,
137140
) -> EventLogger:
138141
return NoOpEventLogger(
139142
name, version=version, schema_url=schema_url, attributes=attributes
@@ -146,7 +149,7 @@ def get_event_logger(
146149
name: str,
147150
version: Optional[str] = None,
148151
schema_url: Optional[str] = None,
149-
attributes: Optional[Attributes] = None,
152+
attributes: Optional[_ExtendedAttributes] = None,
150153
) -> EventLogger:
151154
if _EVENT_LOGGER_PROVIDER:
152155
return _EVENT_LOGGER_PROVIDER.get_event_logger(
@@ -208,7 +211,7 @@ def get_event_logger(
208211
name: str,
209212
version: Optional[str] = None,
210213
schema_url: Optional[str] = None,
211-
attributes: Optional[Attributes] = None,
214+
attributes: Optional[_ExtendedAttributes] = None,
212215
event_logger_provider: Optional[EventLoggerProvider] = None,
213216
) -> "EventLogger":
214217
if event_logger_provider is None:

opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@
3737
from logging import getLogger
3838
from os import environ
3939
from time import time_ns
40-
from typing import Any, Optional, cast
40+
from typing import Optional, cast
4141

4242
from opentelemetry._logs.severity import SeverityNumber
4343
from opentelemetry.environment_variables import _OTEL_PYTHON_LOGGER_PROVIDER
4444
from opentelemetry.trace.span import TraceFlags
4545
from opentelemetry.util._once import Once
4646
from opentelemetry.util._providers import _load_provider
47-
from opentelemetry.util.types import Attributes
47+
from opentelemetry.util.types import AnyValue, _ExtendedAttributes
4848

4949
_logger = getLogger(__name__)
5050

@@ -66,8 +66,8 @@ def __init__(
6666
trace_flags: Optional["TraceFlags"] = None,
6767
severity_text: Optional[str] = None,
6868
severity_number: Optional[SeverityNumber] = None,
69-
body: Optional[Any] = None,
70-
attributes: Optional["Attributes"] = None,
69+
body: AnyValue = None,
70+
attributes: Optional[_ExtendedAttributes] = None,
7171
):
7272
self.timestamp = timestamp
7373
if observed_timestamp is None:
@@ -78,7 +78,7 @@ def __init__(
7878
self.trace_flags = trace_flags
7979
self.severity_text = severity_text
8080
self.severity_number = severity_number
81-
self.body = body # type: ignore
81+
self.body = body
8282
self.attributes = attributes
8383

8484

@@ -90,7 +90,7 @@ def __init__(
9090
name: str,
9191
version: Optional[str] = None,
9292
schema_url: Optional[str] = None,
93-
attributes: Optional[Attributes] = None,
93+
attributes: Optional[_ExtendedAttributes] = None,
9494
) -> None:
9595
super().__init__()
9696
self._name = name
@@ -119,7 +119,7 @@ def __init__( # pylint: disable=super-init-not-called
119119
name: str,
120120
version: Optional[str] = None,
121121
schema_url: Optional[str] = None,
122-
attributes: Optional[Attributes] = None,
122+
attributes: Optional[_ExtendedAttributes] = None,
123123
):
124124
self._name = name
125125
self._version = version
@@ -158,7 +158,7 @@ def get_logger(
158158
name: str,
159159
version: Optional[str] = None,
160160
schema_url: Optional[str] = None,
161-
attributes: Optional[Attributes] = None,
161+
attributes: Optional[_ExtendedAttributes] = None,
162162
) -> Logger:
163163
"""Returns a `Logger` for use by the given instrumentation library.
164164
@@ -196,7 +196,7 @@ def get_logger(
196196
name: str,
197197
version: Optional[str] = None,
198198
schema_url: Optional[str] = None,
199-
attributes: Optional[Attributes] = None,
199+
attributes: Optional[_ExtendedAttributes] = None,
200200
) -> Logger:
201201
"""Returns a NoOpLogger."""
202202
return NoOpLogger(
@@ -210,7 +210,7 @@ def get_logger(
210210
name: str,
211211
version: Optional[str] = None,
212212
schema_url: Optional[str] = None,
213-
attributes: Optional[Attributes] = None,
213+
attributes: Optional[_ExtendedAttributes] = None,
214214
) -> Logger:
215215
if _LOGGER_PROVIDER:
216216
return _LOGGER_PROVIDER.get_logger(
@@ -273,7 +273,7 @@ def get_logger(
273273
instrumenting_library_version: str = "",
274274
logger_provider: Optional[LoggerProvider] = None,
275275
schema_url: Optional[str] = None,
276-
attributes: Optional[Attributes] = None,
276+
attributes: Optional[_ExtendedAttributes] = None,
277277
) -> "Logger":
278278
"""Returns a `Logger` for use within a python process.
279279

0 commit comments

Comments
 (0)