Skip to content

Commit 77e621c

Browse files
authored
feat: add support to custom JSON encoders (#657)
1 parent 061a381 commit 77e621c

File tree

2 files changed

+66
-3
lines changed

2 files changed

+66
-3
lines changed

google/cloud/logging_v2/handlers/structured_log.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,15 @@ class StructuredLogHandler(logging.StreamHandler):
6262
and write them to standard output
6363
"""
6464

65-
def __init__(self, *, labels=None, stream=None, project_id=None):
65+
def __init__(
66+
self, *, labels=None, stream=None, project_id=None, json_encoder_cls=None
67+
):
6668
"""
6769
Args:
6870
labels (Optional[dict]): Additional labels to attach to logs.
6971
stream (Optional[IO]): Stream to be used by the handler.
7072
project (Optional[str]): Project Id associated with the logs.
73+
json_encoder_cls (Optional[Type[JSONEncoder]]): Custom JSON encoder. Defaults to json.JSONEncoder
7174
"""
7275
super(StructuredLogHandler, self).__init__(stream=stream)
7376
self.project_id = project_id
@@ -79,6 +82,8 @@ def __init__(self, *, labels=None, stream=None, project_id=None):
7982
# make logs appear in GCP structured logging format
8083
self._gcp_formatter = logging.Formatter(GCP_FORMAT)
8184

85+
self._json_encoder_cls = json_encoder_cls or json.JSONEncoder
86+
8287
def format(self, record):
8388
"""Format the message into structured log JSON.
8489
Args:
@@ -95,14 +100,18 @@ def format(self, record):
95100
if key in GCP_STRUCTURED_LOGGING_FIELDS:
96101
del message[key]
97102
# if input is a dictionary, encode it as a json string
98-
encoded_msg = json.dumps(message, ensure_ascii=False)
103+
encoded_msg = json.dumps(
104+
message, ensure_ascii=False, cls=self._json_encoder_cls
105+
)
99106
# all json.dumps strings should start and end with parentheses
100107
# strip them out to embed these fields in the larger JSON payload
101108
if len(encoded_msg) > 2:
102109
payload = encoded_msg[1:-1] + ","
103110
elif message:
104111
# properly break any formatting in string to make it json safe
105-
encoded_message = json.dumps(message, ensure_ascii=False)
112+
encoded_message = json.dumps(
113+
message, ensure_ascii=False, cls=self._json_encoder_cls
114+
)
106115
payload = '"message": {},'.format(encoded_message)
107116

108117
record._payload_str = payload or ""

tests/unit/handlers/test_structured_log.py

+54
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ def test_ctor_w_project(self):
4646
handler = self._make_one(project_id="foo")
4747
self.assertEqual(handler.project_id, "foo")
4848

49+
def test_ctor_w_encoder(self):
50+
import json
51+
52+
class CustomJSONEncoder(json.JSONEncoder):
53+
pass
54+
55+
handler = self._make_one(json_encoder_cls=CustomJSONEncoder)
56+
self.assertEqual(handler._json_encoder_cls, CustomJSONEncoder)
57+
4958
def test_format(self):
5059
import logging
5160
import json
@@ -207,6 +216,51 @@ def test_format_with_custom_formatter(self):
207216
self.assertIn(expected_result, result)
208217
self.assertIn("message", result)
209218

219+
def test_format_with_custom_json_encoder(self):
220+
import json
221+
import logging
222+
223+
from pathlib import Path
224+
from typing import Any
225+
226+
class CustomJSONEncoder(json.JSONEncoder):
227+
def default(self, obj: Any) -> Any:
228+
if isinstance(obj, Path):
229+
return str(obj)
230+
return json.JSONEncoder.default(self, obj)
231+
232+
handler = self._make_one(json_encoder_cls=CustomJSONEncoder)
233+
234+
message = "hello world"
235+
json_fields = {"path": Path("/path")}
236+
record = logging.LogRecord(
237+
None,
238+
logging.INFO,
239+
None,
240+
None,
241+
message,
242+
None,
243+
None,
244+
)
245+
setattr(record, "json_fields", json_fields)
246+
expected_payload = {
247+
"message": message,
248+
"severity": "INFO",
249+
"logging.googleapis.com/trace": "",
250+
"logging.googleapis.com/spanId": "",
251+
"logging.googleapis.com/trace_sampled": False,
252+
"logging.googleapis.com/sourceLocation": {},
253+
"httpRequest": {},
254+
"logging.googleapis.com/labels": {},
255+
"path": "/path",
256+
}
257+
handler.filter(record)
258+
259+
result = json.loads(handler.format(record))
260+
261+
self.assertEqual(set(expected_payload.keys()), set(result.keys()))
262+
self.assertEqual(result["path"], "/path")
263+
210264
def test_format_with_reserved_json_field(self):
211265
# drop json_field data with reserved names
212266
# related issue: https://github.com/googleapis/python-logging/issues/543

0 commit comments

Comments
 (0)