Skip to content

Commit a760e02

Browse files
committed
feat: add json_fields extras argument for adding to jsonPayload (#447)
1 parent 83d9ca8 commit a760e02

File tree

5 files changed

+153
-3
lines changed

5 files changed

+153
-3
lines changed

google/cloud/logging_v2/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ def get_default_handler(self, **kw):
376376
if monitored_resource.type == _GAE_RESOURCE_TYPE:
377377
return CloudLoggingHandler(self, resource=monitored_resource, **kw)
378378
elif monitored_resource.type == _GKE_RESOURCE_TYPE:
379-
return ContainerEngineHandler(**kw)
379+
return StructuredLogHandler(**kw, project_id=self.project)
380380
elif monitored_resource.type == _GCF_RESOURCE_TYPE:
381381
# __stdout__ stream required to support structured logging on Python 3.7
382382
kw["stream"] = kw.get("stream", sys.__stdout__)

google/cloud/logging_v2/handlers/handlers.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,16 @@ def _format_and_parse_message(record, formatter_handler):
221221
record (logging.LogRecord): The record object representing the log
222222
formatter_handler (logging.Handler): The handler used to format the log
223223
"""
224-
# if message is a dictionary, return as-is
224+
passed_json_fields = getattr(record, "json_fields", {})
225+
# if message is a dictionary, use dictionary directly
225226
if isinstance(record.msg, collections.abc.Mapping):
226-
return record.msg
227+
payload = record.msg
228+
# attach any extra json fields if present
229+
if passed_json_fields and isinstance(
230+
passed_json_fields, collections.abc.Mapping
231+
):
232+
payload = {**payload, **passed_json_fields}
233+
return payload
227234
# format message string based on superclass
228235
message = formatter_handler.format(record)
229236
try:
@@ -235,6 +242,11 @@ def _format_and_parse_message(record, formatter_handler):
235242
except (json.decoder.JSONDecodeError, IndexError):
236243
# log string is not valid json
237244
pass
245+
# if json_fields was set, create a dictionary using that
246+
if passed_json_fields and isinstance(passed_json_fields, collections.abc.Mapping):
247+
if message != "None":
248+
passed_json_fields["message"] = message
249+
return passed_json_fields
238250
# if formatted message contains no content, return None
239251
return message if message != "None" else None
240252

tests/system/test_system.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,31 @@ def test_handlers_w_extras(self):
551551
)
552552
self.assertEqual(entries[0].resource.type, extra["resource"].type)
553553

554+
def test_handlers_w_json_fields(self):
555+
LOG_MESSAGE = "Testing with json_field extras."
556+
LOGGER_NAME = "json_field_extras"
557+
handler_name = self._logger_name(LOGGER_NAME)
558+
559+
handler = CloudLoggingHandler(
560+
Config.CLIENT, name=handler_name, transport=SyncTransport
561+
)
562+
563+
# only create the logger to delete, hidden otherwise
564+
logger = Config.CLIENT.logger(handler.name)
565+
self.to_delete.append(logger)
566+
567+
cloud_logger = logging.getLogger(LOGGER_NAME)
568+
cloud_logger.addHandler(handler)
569+
extra = {"json_fields": {"hello": "world", "two": 2}}
570+
cloud_logger.warn(LOG_MESSAGE, extra=extra)
571+
572+
entries = _list_entries(logger)
573+
self.assertEqual(len(entries), 1)
574+
payload = entries[0].payload
575+
self.assertEqual(payload["message"], LOG_MESSAGE)
576+
self.assertEqual(payload["hello"], "world")
577+
self.assertEqual(payload["two"], 2)
578+
554579
def test_log_root_handler(self):
555580
LOG_MESSAGE = "It was the best of times."
556581

tests/unit/handlers/test_handlers.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,40 @@ def test_emit_dict(self):
447447
),
448448
)
449449

450+
def test_emit_w_json_extras(self):
451+
"""
452+
User can add json_fields to the record, which should populate the payload
453+
"""
454+
from google.cloud.logging_v2.logger import _GLOBAL_RESOURCE
455+
456+
client = _Client(self.PROJECT)
457+
handler = self._make_one(
458+
client, transport=_Transport, resource=_GLOBAL_RESOURCE,
459+
)
460+
message = "message"
461+
json_fields = {"hello": "world"}
462+
logname = "logname"
463+
expected_label = {"python_logger": logname}
464+
record = logging.LogRecord(
465+
logname, logging.INFO, None, None, message, None, None
466+
)
467+
setattr(record, "json_fields", json_fields)
468+
handler.handle(record)
469+
470+
self.assertEqual(
471+
handler.transport.send_called_with,
472+
(
473+
record,
474+
{"message": "message", "hello": "world"},
475+
_GLOBAL_RESOURCE,
476+
expected_label,
477+
None,
478+
None,
479+
None,
480+
None,
481+
),
482+
)
483+
450484
def test_emit_with_encoded_json(self):
451485
"""
452486
Handler should parse json encoded as a string
@@ -608,6 +642,62 @@ def test_broken_encoded_dict(self):
608642
result = _format_and_parse_message(record, handler)
609643
self.assertEqual(result, message)
610644

645+
def test_json_fields(self):
646+
"""
647+
record.json_fields should populate the json payload
648+
"""
649+
from google.cloud.logging_v2.handlers.handlers import _format_and_parse_message
650+
651+
message = "hello"
652+
json_fields = {"key": "val"}
653+
record = logging.LogRecord("logname", None, None, None, message, None, None)
654+
setattr(record, "json_fields", json_fields)
655+
handler = logging.StreamHandler()
656+
result = _format_and_parse_message(record, handler)
657+
self.assertEqual(result, {"message": message, "key": "val"})
658+
659+
def test_empty_json_fields(self):
660+
"""
661+
empty jsond_field dictionaries should result in a string output
662+
"""
663+
from google.cloud.logging_v2.handlers.handlers import _format_and_parse_message
664+
665+
message = "hello"
666+
record = logging.LogRecord("logname", None, None, None, message, None, None)
667+
setattr(record, "json_fields", {})
668+
handler = logging.StreamHandler()
669+
result = _format_and_parse_message(record, handler)
670+
self.assertEqual(result, message)
671+
672+
def test_json_fields_empty_message(self):
673+
"""
674+
empty message fields should not be added to json_fields dictionaries
675+
"""
676+
from google.cloud.logging_v2.handlers.handlers import _format_and_parse_message
677+
678+
message = None
679+
json_fields = {"key": "val"}
680+
record = logging.LogRecord("logname", None, None, None, message, None, None)
681+
setattr(record, "json_fields", json_fields)
682+
handler = logging.StreamHandler()
683+
result = _format_and_parse_message(record, handler)
684+
self.assertEqual(result, json_fields)
685+
686+
def test_json_fields_with_json_message(self):
687+
"""
688+
if json_fields and message are both dicts, they should be combined
689+
"""
690+
from google.cloud.logging_v2.handlers.handlers import _format_and_parse_message
691+
692+
message = {"key_m": "val_m"}
693+
json_fields = {"key_j": "val_j"}
694+
record = logging.LogRecord("logname", None, None, None, message, None, None)
695+
setattr(record, "json_fields", json_fields)
696+
handler = logging.StreamHandler()
697+
result = _format_and_parse_message(record, handler)
698+
self.assertEqual(result["key_m"], message["key_m"])
699+
self.assertEqual(result["key_j"], json_fields["key_j"])
700+
611701

612702
class TestSetupLogging(unittest.TestCase):
613703
def _call_fut(self, handler, excludes=None):

tests/unit/handlers/test_structured_log.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,3 +321,26 @@ def test_format_overrides(self):
321321
result = json.loads(handler.format(record))
322322
for (key, value) in expected_payload.items():
323323
self.assertEqual(value, result[key])
324+
325+
def test_format_with_json_fields(self):
326+
"""
327+
User can add json_fields to the record, which should populate the payload
328+
"""
329+
import logging
330+
import json
331+
332+
handler = self._make_one()
333+
message = "name: %s"
334+
name_arg = "Daniel"
335+
expected_result = "name: Daniel"
336+
json_fields = {"hello": "world", "number": 12}
337+
record = logging.LogRecord(
338+
None, logging.INFO, None, None, message, name_arg, None,
339+
)
340+
record.created = None
341+
setattr(record, "json_fields", json_fields)
342+
handler.filter(record)
343+
result = json.loads(handler.format(record))
344+
self.assertEqual(result["message"], expected_result)
345+
self.assertEqual(result["hello"], "world")
346+
self.assertEqual(result["number"], 12)

0 commit comments

Comments
 (0)