Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit f9fc491

Browse filesBrowse files
committed
feat: add support to custom JSON encoders
1 parent a62a0d6 commit f9fc491
Copy full SHA for f9fc491

File tree

Expand file treeCollapse file tree

2 files changed

+66
-3
lines changed
Filter options
Expand file treeCollapse file tree

2 files changed

+66
-3
lines changed

‎google/cloud/logging_v2/handlers/structured_log.py

Copy file name to clipboardExpand all lines: google/cloud/logging_v2/handlers/structured_log.py
+12-3Lines changed: 12 additions & 3 deletions
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

Copy file name to clipboardExpand all lines: tests/unit/handlers/test_structured_log.py
+54Lines changed: 54 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.