Skip to content

Commit 4a271f9

Browse files
xrmxaabmass
andauthored
opentelemetry-exporter-otlp-proto-common: permit to serialize null values in logs (#4400)
* opentelemetry-exporter-otlp-proto-common: permit to serialize null values in logs * Add CHANGELOG * Update exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py * Fix case where a list contains None values and make tests more clear * Keep behavior the same for None values in list with allow_null=False * fix typo * Fix test failing on <= 3.8 --------- Co-authored-by: Aaron Abbott <[email protected]>
1 parent c6fab7d commit 4a271f9

File tree

5 files changed

+133
-7
lines changed

5 files changed

+133
-7
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
([#4371](https://github.com/open-telemetry/opentelemetry-python/pull/4371))
1818
- Fix span context manager typing by using ParamSpec from typing_extensions
1919
([#4389](https://github.com/open-telemetry/opentelemetry-python/pull/4389))
20+
- Fix serialization of None values in logs body to match 1.31.0+ data model
21+
([#4400](https://github.com/open-telemetry/opentelemetry-python/pull/4400))
2022
- [BREAKING] semantic-conventions: Remove `opentelemetry.semconv.attributes.network_attributes.NETWORK_INTERFACE_NAME`
2123
introduced by mistake in the wrong module.
2224
([#4391](https://github.com/open-telemetry/opentelemetry-python/pull/4391))

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

+37-5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
# limitations under the License.
1414

1515

16+
from __future__ import annotations
17+
1618
import logging
1719
from collections.abc import Sequence
1820
from itertools import count
@@ -66,7 +68,11 @@ def _encode_resource(resource: Resource) -> PB2Resource:
6668
return PB2Resource(attributes=_encode_attributes(resource.attributes))
6769

6870

69-
def _encode_value(value: Any) -> PB2AnyValue:
71+
def _encode_value(
72+
value: Any, allow_null: bool = False
73+
) -> Optional[PB2AnyValue]:
74+
if allow_null is True and value is None:
75+
return None
7076
if isinstance(value, bool):
7177
return PB2AnyValue(bool_value=value)
7278
if isinstance(value, str):
@@ -79,19 +85,45 @@ def _encode_value(value: Any) -> PB2AnyValue:
7985
return PB2AnyValue(bytes_value=value)
8086
if isinstance(value, Sequence):
8187
return PB2AnyValue(
82-
array_value=PB2ArrayValue(values=[_encode_value(v) for v in value])
88+
array_value=PB2ArrayValue(
89+
values=_encode_array(value, allow_null=allow_null)
90+
)
8391
)
8492
elif isinstance(value, Mapping):
8593
return PB2AnyValue(
8694
kvlist_value=PB2KeyValueList(
87-
values=[_encode_key_value(str(k), v) for k, v in value.items()]
95+
values=[
96+
_encode_key_value(str(k), v, allow_null=allow_null)
97+
for k, v in value.items()
98+
]
8899
)
89100
)
90101
raise Exception(f"Invalid type {type(value)} of value {value}")
91102

92103

93-
def _encode_key_value(key: str, value: Any) -> PB2KeyValue:
94-
return PB2KeyValue(key=key, value=_encode_value(value))
104+
def _encode_key_value(
105+
key: str, value: Any, allow_null: bool = False
106+
) -> PB2KeyValue:
107+
return PB2KeyValue(
108+
key=key, value=_encode_value(value, allow_null=allow_null)
109+
)
110+
111+
112+
def _encode_array(
113+
array: Sequence[Any], allow_null: bool = False
114+
) -> Sequence[PB2AnyValue]:
115+
if not allow_null:
116+
# Let the exception get raised by _encode_value()
117+
return [_encode_value(v, allow_null=allow_null) for v in array]
118+
119+
return [
120+
_encode_value(v, allow_null=allow_null)
121+
if v is not None
122+
# Use an empty AnyValue to represent None in an array. Behavior may change pending
123+
# https://github.com/open-telemetry/opentelemetry-specification/issues/4392
124+
else PB2AnyValue()
125+
for v in array
126+
]
95127

96128

97129
def _encode_span_id(span_id: int) -> bytes:

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def _encode_log(log_data: LogData) -> PB2LogRecord:
5555
span_id=span_id,
5656
trace_id=trace_id,
5757
flags=int(log_data.log_record.trace_flags),
58-
body=_encode_value(body) if body is not None else None,
58+
body=_encode_value(body, allow_null=True),
5959
severity_text=log_data.log_record.severity_text,
6060
attributes=_encode_attributes(log_data.log_record.attributes),
6161
dropped_attributes_count=log_data.log_record.dropped_attributes,

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

+18
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,24 @@ def test_encode_attributes_all_kinds(self):
8888
],
8989
)
9090

91+
def test_encode_attributes_error_list_none(self):
92+
with self.assertLogs(level=ERROR) as error:
93+
result = _encode_attributes(
94+
{"a": 1, "bad_key": ["test", None, "test"], "b": 2}
95+
)
96+
97+
self.assertEqual(len(error.records), 1)
98+
self.assertEqual(error.records[0].msg, "Failed to encode key %s: %s")
99+
self.assertEqual(error.records[0].args[0], "bad_key")
100+
self.assertIsInstance(error.records[0].args[1], Exception)
101+
self.assertEqual(
102+
result,
103+
[
104+
PB2KeyValue(key="a", value=PB2AnyValue(int_value=1)),
105+
PB2KeyValue(key="b", value=PB2AnyValue(int_value=2)),
106+
],
107+
)
108+
91109
def test_encode_attributes_error_logs_key(self):
92110
with self.assertLogs(level=ERROR) as error:
93111
result = _encode_attributes({"a": 1, "bad_key": None, "b": 2})

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

+75-1
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,16 @@
2727
ExportLogsServiceRequest,
2828
)
2929
from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue
30+
from opentelemetry.proto.common.v1.common_pb2 import (
31+
ArrayValue as PB2ArrayValue,
32+
)
3033
from opentelemetry.proto.common.v1.common_pb2 import (
3134
InstrumentationScope as PB2InstrumentationScope,
3235
)
3336
from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue
37+
from opentelemetry.proto.common.v1.common_pb2 import (
38+
KeyValueList as PB2KeyValueList,
39+
)
3440
from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord
3541
from opentelemetry.proto.logs.v1.logs_pb2 import (
3642
ResourceLogs as PB2ResourceLogs,
@@ -154,7 +160,25 @@ def _get_sdk_log_data() -> List[LogData]:
154160
),
155161
)
156162

157-
return [log1, log2, log3, log4]
163+
log5 = LogData(
164+
log_record=SDKLogRecord(
165+
timestamp=1644650584292683009,
166+
observed_timestamp=1644650584292683010,
167+
trace_id=212592107417388365804938480559624925555,
168+
span_id=6077757853989569445,
169+
trace_flags=TraceFlags(0x01),
170+
severity_text="INFO",
171+
severity_number=SeverityNumber.INFO,
172+
body={"error": None, "array_with_nones": [1, None, 2]},
173+
resource=SDKResource({}),
174+
attributes={},
175+
),
176+
instrumentation_scope=InstrumentationScope(
177+
"last_name", "last_version"
178+
),
179+
)
180+
181+
return [log1, log2, log3, log4, log5]
158182

159183
def get_test_logs(
160184
self,
@@ -287,6 +311,56 @@ def get_test_logs(
287311
),
288312
],
289313
),
314+
PB2ResourceLogs(
315+
resource=PB2Resource(),
316+
scope_logs=[
317+
PB2ScopeLogs(
318+
scope=PB2InstrumentationScope(
319+
name="last_name",
320+
version="last_version",
321+
),
322+
log_records=[
323+
PB2LogRecord(
324+
time_unix_nano=1644650584292683009,
325+
observed_time_unix_nano=1644650584292683010,
326+
trace_id=_encode_trace_id(
327+
212592107417388365804938480559624925555
328+
),
329+
span_id=_encode_span_id(
330+
6077757853989569445,
331+
),
332+
flags=int(TraceFlags(0x01)),
333+
severity_text="INFO",
334+
severity_number=SeverityNumber.INFO.value,
335+
body=PB2AnyValue(
336+
kvlist_value=PB2KeyValueList(
337+
values=[
338+
PB2KeyValue(key="error"),
339+
PB2KeyValue(
340+
key="array_with_nones",
341+
value=PB2AnyValue(
342+
array_value=PB2ArrayValue(
343+
values=[
344+
PB2AnyValue(
345+
int_value=1
346+
),
347+
PB2AnyValue(),
348+
PB2AnyValue(
349+
int_value=2
350+
),
351+
]
352+
)
353+
),
354+
),
355+
]
356+
)
357+
),
358+
attributes={},
359+
),
360+
],
361+
),
362+
],
363+
),
290364
]
291365
)
292366

0 commit comments

Comments
 (0)