Skip to content

Commit f4a864a

Browse files
authored
Update cloud logging exporter to correctly handle when LogRecord.body is of type Mapping[str, List[Mapping]] (#430)
* Update logic mappaing body to json payload * Update changelog * Fix linter
1 parent b02ad62 commit f4a864a

File tree

6 files changed

+116
-8
lines changed

6 files changed

+116
-8
lines changed

opentelemetry-exporter-gcp-logging/CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
## Unreleased
44

5-
Added support for when `bytes` or `list['bytes']` is in `LogRecord.body` and
6-
body is of type Mapping. Update opentelemetry-api/sdk dependencies to 1.3.
5+
- Added support for when a `Mapping[str, bytes]` or `Mapping[str, List[bytes]]` is in `LogRecord.body`.
6+
- Added support for when a `Mapping[str, List[Mapping]]` is in `LogRecord.body`.
7+
- Do not call `logging.warning` when `LogRecord.body` is of None type, instead leave `LogEntry.payload` empty.
8+
- Update opentelemetry-api/sdk dependencies to 1.3.
79

810
## Version 1.9.0a0
911

opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,19 @@ def _sanitized_body(
129129
# Should not be possible for a non-bytes value to be present. AnyValue requires Sequence be of one type, and above
130130
# we verified the first value is type bytes.
131131
new_body[key] = [
132-
base64.b64encode(v).decode()
132+
base64.b64encode(v).decode() if isinstance(v, bytes) else v
133133
for v in value
134-
if isinstance(v, bytes)
134+
]
135+
elif (
136+
isinstance(value, Sequence)
137+
and len(value) > 0
138+
and isinstance(value[0], Mapping)
139+
):
140+
# Should not be possible for a non-mapping value to be present. AnyValue requires Sequence be of one type, and above
141+
# we verified the first value is type mapping.
142+
new_body[key] = [
143+
_sanitized_body(x) if isinstance(x, Mapping) else x
144+
for x in value
135145
]
136146
elif isinstance(value, bytes):
137147
new_body[key] = base64.b64encode(value).decode()
@@ -145,8 +155,15 @@ def _sanitized_body(
145155
def _set_payload_in_log_entry(log_entry: LogEntry, body: AnyValue):
146156
struct = Struct()
147157
if isinstance(body, Mapping):
148-
struct.update(_sanitized_body(body))
149-
log_entry.json_payload = struct
158+
sanitized = _sanitized_body(body)
159+
try:
160+
struct.update(sanitized)
161+
log_entry.json_payload = struct
162+
except Exception as exc: # pylint: disable=broad-except
163+
logging.exception(
164+
"Error mapping LogRecord.body to Struct, will write log with empty body: %s",
165+
exc,
166+
)
150167
elif isinstance(body, bytes):
151168
json_str = body.decode("utf-8", errors="replace")
152169
json_dict = json.loads(json_str)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[
2+
{
3+
"entries": [
4+
{
5+
"jsonPayload": {
6+
"my_dict": [
7+
true,
8+
false,
9+
false,
10+
true
11+
]
12+
},
13+
"logName": "projects/fakeproject/logs/test",
14+
"resource": {
15+
"labels": {
16+
"location": "global",
17+
"namespace": "",
18+
"node_id": ""
19+
},
20+
"type": "generic_node"
21+
},
22+
"timestamp": "2025-01-15T21:25:10.997977393Z"
23+
}
24+
],
25+
"partialSuccess": true
26+
}
27+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[
2+
{
3+
"entries": [
4+
{
5+
"jsonPayload": {
6+
"my_dict": [
7+
{
8+
"key": "Ynl0ZXM="
9+
}
10+
]
11+
},
12+
"logName": "projects/fakeproject/logs/test",
13+
"resource": {
14+
"labels": {
15+
"location": "global",
16+
"namespace": "",
17+
"node_id": ""
18+
},
19+
"type": "generic_node"
20+
},
21+
"timestamp": "2025-01-15T21:25:10.997977393Z"
22+
}
23+
],
24+
"partialSuccess": true
25+
}
26+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[
2+
{
3+
"entries": [
4+
{
5+
"jsonPayload": {
6+
"my_dict": [
7+
true,
8+
"str",
9+
1.0,
10+
0.234
11+
]
12+
},
13+
"logName": "projects/fakeproject/logs/test",
14+
"resource": {
15+
"labels": {
16+
"location": "global",
17+
"namespace": "",
18+
"node_id": ""
19+
},
20+
"type": "generic_node"
21+
},
22+
"timestamp": "2025-01-15T21:25:10.997977393Z"
23+
}
24+
],
25+
"partialSuccess": true
26+
}
27+
]

opentelemetry-exporter-gcp-logging/tests/test_cloud_logging.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
Be sure to review the changes.
2727
"""
2828
import re
29-
from typing import List, Union
29+
from typing import List, Mapping, Union
3030

3131
import pytest
3232
from fixtures.cloud_logging_fake import CloudLoggingFake, WriteLogEntriesCall
@@ -159,12 +159,21 @@ def test_convert_non_json_dict_bytes(
159159
pytest.param("A text body", id="str"),
160160
pytest.param(True, id="bool"),
161161
pytest.param(None, id="None"),
162+
pytest.param(
163+
{"my_dict": [{"key": b"bytes"}]}, id="list_of_dicts_with_bytes"
164+
),
165+
pytest.param(
166+
{"my_dict": [True, False, False, True]}, id="list_of_bools"
167+
),
168+
pytest.param(
169+
{"my_dict": [True, "str", 1, 0.234]}, id="list_of_mixed_sequence"
170+
),
162171
],
163172
)
164173
def test_convert_various_types_of_bodies(
165174
cloudloggingfake: CloudLoggingFake,
166175
snapshot_writelogentrycalls: List[WriteLogEntriesCall],
167-
body: Union[str, bool, None],
176+
body: Union[str, bool, None, Mapping],
168177
) -> None:
169178
log_data = [
170179
LogData(

0 commit comments

Comments
 (0)