Skip to content

Commit ab18b30

Browse files
committed
Respond to comments. Handle case where bytes is
set in LogRecord.body but is not a proto struct.
1 parent 4bfd7b1 commit ab18b30

File tree

7 files changed

+61
-55
lines changed

7 files changed

+61
-55
lines changed

opentelemetry-exporter-gcp-logging/README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Usage
6565
# so telemetry is collected only for the application
6666
logger1 = logging.getLogger("myapp.area1")
6767
68-
logger1.error({'structured_log_will_go_to_json_payload': 'value'}, extra={'this_will_go_to_LogEntry_labels_field': 'value'})
68+
logger1.warning("string log %s", "here")
6969
7070
References
7171
----------

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

Lines changed: 53 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,17 @@
1313
# limitations under the License.
1414
from __future__ import annotations
1515

16+
import base64
1617
import datetime
1718
import json
1819
import logging
1920
import urllib.parse
2021
from typing import Any, Mapping, Optional, Sequence
2122

2223
import google.auth
23-
from google.api.monitored_resource_pb2 import MonitoredResource # type: ignore
24+
from google.api.monitored_resource_pb2 import ( # pylint: disable = no-name-in-module
25+
MonitoredResource,
26+
)
2427
from google.cloud.logging_v2.services.logging_service_v2 import (
2528
LoggingServiceV2Client,
2629
)
@@ -29,9 +32,15 @@
2932
)
3033
from google.cloud.logging_v2.types.log_entry import LogEntry
3134
from google.cloud.logging_v2.types.logging import WriteLogEntriesRequest
32-
from google.logging.type.log_severity_pb2 import LogSeverity # type: ignore
33-
from google.protobuf.struct_pb2 import Struct
34-
from google.protobuf.timestamp_pb2 import Timestamp
35+
from google.logging.type.log_severity_pb2 import ( # pylint: disable = no-name-in-module
36+
LogSeverity,
37+
)
38+
from google.protobuf.struct_pb2 import ( # pylint: disable = no-name-in-module
39+
Struct,
40+
)
41+
from google.protobuf.timestamp_pb2 import ( # pylint: disable = no-name-in-module
42+
Timestamp,
43+
)
3544
from opentelemetry.exporter.cloud_logging.version import __version__
3645
from opentelemetry.resourcedetector.gcp_resource_detector._mapping import (
3746
get_monitored_resource,
@@ -94,12 +103,21 @@
94103

95104

96105
def convert_any_value_to_string(value: Any) -> str:
97-
t = type(value)
98-
if t is bool or t is int or t is float or t is str:
106+
if isinstance(value, bool):
107+
return "true" if value else "false"
108+
if isinstance(value, bytes):
109+
return base64.b64encode(value).decode()
110+
if (
111+
isinstance(value, int)
112+
or isinstance(value, float)
113+
or isinstance(value, str)
114+
):
99115
return str(value)
100-
if t is list or t is tuple:
116+
if isinstance(value, list) or isinstance(value, tuple):
101117
return json.dumps(value)
102-
logging.warning("Unknown type %s found, cannot convert to string.", t)
118+
logging.warning(
119+
"Unknown value %s found, cannot convert to string.", type(value)
120+
)
103121
return ""
104122

105123

@@ -132,6 +150,7 @@ def export(self, batch: Sequence[LogData]):
132150
now = datetime.datetime.now()
133151
log_entries = []
134152
for log_data in batch:
153+
log_entry = LogEntry()
135154
log_record = log_data.log_record
136155
attributes = log_record.attributes or {}
137156
project_id = str(
@@ -144,18 +163,7 @@ def export(self, batch: Sequence[LogData]):
144163
)
145164
)
146165
)
147-
monitored_resource_data = get_monitored_resource(
148-
log_record.resource or Resource({})
149-
)
150-
# convert it to proto
151-
monitored_resource: Optional[MonitoredResource] = (
152-
MonitoredResource(
153-
type=monitored_resource_data.type,
154-
labels=monitored_resource_data.labels,
155-
)
156-
if monitored_resource_data
157-
else None
158-
)
166+
log_entry.log_name = f"projects/{project_id}/logs/{log_suffix}"
159167
# If timestamp is unset fall back to observed_time_unix_nano as recommended,
160168
# see https://github.com/open-telemetry/opentelemetry-proto/blob/4abbb78/opentelemetry/proto/logs/v1/logs.proto#L176-L179
161169
ts = Timestamp()
@@ -165,12 +173,15 @@ def export(self, batch: Sequence[LogData]):
165173
)
166174
else:
167175
ts.FromDatetime(now)
168-
log_name = f"projects/{project_id}/logs/{log_suffix}"
169-
log_entry = LogEntry()
170176
log_entry.timestamp = ts
171-
log_entry.log_name = log_name
172-
if monitored_resource:
173-
log_entry.resource = monitored_resource
177+
monitored_resource_data = get_monitored_resource(
178+
log_record.resource or Resource({})
179+
)
180+
if monitored_resource_data:
181+
log_entry.resource = MonitoredResource(
182+
type=monitored_resource_data.type,
183+
labels=monitored_resource_data.labels,
184+
)
174185
log_entry.trace_sampled = (
175186
log_record.trace_flags is not None
176187
and log_record.trace_flags.sampled
@@ -190,30 +201,27 @@ def export(self, batch: Sequence[LogData]):
190201
k: convert_any_value_to_string(v)
191202
for k, v in attributes.items()
192203
}
193-
if isinstance(log_record.body, Mapping):
194-
s = Struct()
195-
s.update(log_record.body)
196-
log_entry.json_payload = s
197-
elif type(log_record.body) is bytes:
198-
json_str = log_record.body.decode("utf8")
199-
json_dict = json.loads(json_str)
200-
if isinstance(json_dict, Mapping):
201-
s = Struct()
202-
s.update(json_dict)
203-
log_entry.json_payload = s
204-
else:
205-
logging.warning(
206-
"LogRecord.body was bytes type and json.loads turned body into type %s, expected a dictionary.",
207-
type(json_dict),
208-
)
209-
else:
210-
log_entry.text_payload = convert_any_value_to_string(
211-
log_record.body
212-
)
204+
self._set_payload_in_log_entry(log_entry, log_record.body)
213205
log_entries.append(log_entry)
214206

215207
self._write_log_entries(log_entries)
216208

209+
def _set_payload_in_log_entry(self, log_entry: LogEntry, body: Any | None):
210+
struct = Struct()
211+
if isinstance(body, Mapping):
212+
struct.update(body)
213+
log_entry.json_payload = struct
214+
elif isinstance(body, bytes):
215+
json_str = body.decode("utf-8", errors="replace")
216+
json_dict = json.loads(json_str)
217+
if isinstance(json_dict, Mapping):
218+
struct.update(json_dict)
219+
log_entry.json_payload = struct
220+
else:
221+
log_entry.text_payload = base64.b64encode(body).decode()
222+
else:
223+
log_entry.text_payload = convert_any_value_to_string(body)
224+
217225
def _write_log_entries(self, log_entries: list[LogEntry]):
218226
batch: list[LogEntry] = []
219227
batch_byte_size = 0

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
__version__ = "0.9b0"
15+
__version__ = "1.9.0.dev0"

opentelemetry-exporter-gcp-logging/tests/__snapshots__/test_cloud_logging/test_convert_non_json_dict_bytes.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
},
1212
"type": "generic_node"
1313
},
14+
"textPayload": "MTIz",
1415
"timestamp": "2025-01-15T21:25:10.997977393Z"
1516
}
1617
],

opentelemetry-exporter-gcp-logging/tests/__snapshots__/test_cloud_logging/test_convert_otlp_dict_body.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
},
1717
"labels": {
1818
"event.name": "gen_ai.system.message",
19-
"gen_ai.system": "openai"
19+
"gen_ai.system": "true",
20+
"test": "23"
2021
},
2122
"logName": "projects/fakeproject/logs/test",
2223
"resource": {

opentelemetry-exporter-gcp-logging/tests/__snapshots__/test_cloud_logging/test_convert_various_types_of_bodies[bool].json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
},
1212
"type": "generic_node"
1313
},
14-
"textPayload": "True",
14+
"textPayload": "true",
1515
"timestamp": "2025-01-15T21:25:10.997977393Z"
1616
}
1717
],

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ def test_convert_otlp_dict_body(
7979
trace_id=25,
8080
span_id=22,
8181
attributes={
82-
"gen_ai.system": "openai",
82+
"gen_ai.system": True,
83+
"test": 23,
8384
"event.name": "gen_ai.system.message",
8485
},
8586
body={
@@ -136,7 +137,6 @@ def test_convert_otlp_various_different_types_in_attrs_and_bytes_body(
136137
def test_convert_non_json_dict_bytes(
137138
cloudloggingfake: CloudLoggingFake,
138139
snapshot_writelogentrycalls: List[WriteLogEntriesCall],
139-
caplog,
140140
) -> None:
141141
log_data = [
142142
LogData(
@@ -149,10 +149,6 @@ def test_convert_non_json_dict_bytes(
149149
]
150150
cloudloggingfake.exporter.export(log_data)
151151
assert cloudloggingfake.get_calls() == snapshot_writelogentrycalls
152-
assert (
153-
"LogRecord.body was bytes type and json.loads turned body into type <class 'int'>, expected a dictionary"
154-
in caplog.text
155-
)
156152

157153

158154
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)