Skip to content

Commit 6be12f2

Browse files
feat!: make logging API more friendly to use (#422)
1 parent ab570ef commit 6be12f2

File tree

4 files changed

+146
-6
lines changed

4 files changed

+146
-6
lines changed

google/cloud/logging_v2/logger.py

+25-5
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
("source_location", None),
4646
)
4747

48+
_STRUCT_EXTRACTABLE_FIELDS = ["severity", "trace", "span_id"]
49+
4850

4951
class Logger(object):
5052
"""Loggers represent named targets for log entries.
@@ -133,6 +135,20 @@ def _do_log(self, client, _entry_class, payload=None, **kw):
133135
kw["labels"] = kw.pop("labels", self.labels)
134136
kw["resource"] = kw.pop("resource", self.default_resource)
135137

138+
severity = kw.get("severity", None)
139+
if isinstance(severity, str) and not severity.isupper():
140+
# convert severity to upper case, as expected by enum definition
141+
kw["severity"] = severity.upper()
142+
143+
if isinstance(kw["resource"], collections.abc.Mapping):
144+
# if resource was passed as a dict, attempt to parse it into a
145+
# Resource object
146+
try:
147+
kw["resource"] = Resource(**kw["resource"])
148+
except TypeError as e:
149+
# dict couldn't be parsed as a Resource
150+
raise TypeError("invalid resource dict") from e
151+
136152
if payload is not None:
137153
entry = _entry_class(payload=payload, **kw)
138154
else:
@@ -186,6 +202,10 @@ def log_struct(self, info, *, client=None, **kw):
186202
kw (Optional[dict]): additional keyword arguments for the entry.
187203
See :class:`~logging_v2.entries.LogEntry`.
188204
"""
205+
for field in _STRUCT_EXTRACTABLE_FIELDS:
206+
# attempt to copy relevant fields from the payload into the LogEntry body
207+
if field in info and field not in kw:
208+
kw[field] = info[field]
189209
self._do_log(client, StructEntry, info, **kw)
190210

191211
def log_proto(self, message, *, client=None, **kw):
@@ -220,14 +240,14 @@ def log(self, message=None, *, client=None, **kw):
220240
kw (Optional[dict]): additional keyword arguments for the entry.
221241
See :class:`~logging_v2.entries.LogEntry`.
222242
"""
223-
entry_type = LogEntry
224243
if isinstance(message, google.protobuf.message.Message):
225-
entry_type = ProtobufEntry
244+
self.log_proto(message, client=client, **kw)
226245
elif isinstance(message, collections.abc.Mapping):
227-
entry_type = StructEntry
246+
self.log_struct(message, client=client, **kw)
228247
elif isinstance(message, str):
229-
entry_type = TextEntry
230-
self._do_log(client, entry_type, message, **kw)
248+
self.log_text(message, client=client, **kw)
249+
else:
250+
self._do_log(client, LogEntry, message, **kw)
231251

232252
def delete(self, logger_name=None, *, client=None):
233253
"""Delete all entries in a logger via a DELETE request

tests/system/test_system.py

+19
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,25 @@ def test_log_empty(self):
455455
self.assertEqual(len(entries), 1)
456456
self.assertIsNone(entries[0].payload)
457457

458+
def test_log_struct_logentry_data(self):
459+
logger = Config.CLIENT.logger(self._logger_name("log_w_struct"))
460+
self.to_delete.append(logger)
461+
462+
JSON_PAYLOAD = {
463+
"message": "System test: test_log_struct_logentry_data",
464+
"severity": "warning",
465+
"trace": "123",
466+
"span_id": "456",
467+
}
468+
logger.log(JSON_PAYLOAD)
469+
entries = _list_entries(logger)
470+
471+
self.assertEqual(len(entries), 1)
472+
self.assertEqual(entries[0].payload, JSON_PAYLOAD)
473+
self.assertEqual(entries[0].severity, "WARNING")
474+
self.assertEqual(entries[0].trace, JSON_PAYLOAD["trace"])
475+
self.assertEqual(entries[0].span_id, JSON_PAYLOAD["span_id"])
476+
458477
def test_log_handler_async(self):
459478
LOG_MESSAGE = "It was the worst of times"
460479

tests/unit/test_logger.py

+101
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,107 @@ def test_log_struct_w_explicit(self):
379379

380380
self.assertEqual(api._write_entries_called_with, (ENTRIES, None, None, None))
381381

382+
def test_log_struct_inference(self):
383+
"""
384+
LogEntry fields in _STRUCT_EXTRACTABLE_FIELDS should be inferred from
385+
the payload data if not passed as a parameter
386+
"""
387+
from google.cloud.logging_v2.handlers._monitored_resources import (
388+
detect_resource,
389+
)
390+
391+
STRUCT = {
392+
"message": "System test: test_log_struct_logentry_data",
393+
"severity": "warning",
394+
"trace": "123",
395+
"span_id": "456",
396+
}
397+
RESOURCE = detect_resource(self.PROJECT)._to_dict()
398+
ENTRIES = [
399+
{
400+
"logName": "projects/%s/logs/%s" % (self.PROJECT, self.LOGGER_NAME),
401+
"jsonPayload": STRUCT,
402+
"severity": "WARNING",
403+
"trace": "123",
404+
"spanId": "456",
405+
"resource": RESOURCE,
406+
}
407+
]
408+
client = _Client(self.PROJECT)
409+
api = client.logging_api = _DummyLoggingAPI()
410+
logger = self._make_one(self.LOGGER_NAME, client=client)
411+
412+
logger.log_struct(STRUCT, resource=RESOURCE)
413+
414+
self.assertEqual(api._write_entries_called_with, (ENTRIES, None, None, None))
415+
416+
def test_log_w_dict_resource(self):
417+
"""
418+
Users should be able to input a dictionary with type and labels instead
419+
of a Resource object
420+
"""
421+
import pytest
422+
423+
MESSAGE = "hello world"
424+
client = _Client(self.PROJECT)
425+
api = client.logging_api = _DummyLoggingAPI()
426+
logger = self._make_one(self.LOGGER_NAME, client=client)
427+
broken_resource_dicts = [{}, {"type": ""}, {"labels": ""}]
428+
for resource in broken_resource_dicts:
429+
# ensure bad inputs result in a helpful error
430+
with pytest.raises(TypeError):
431+
logger.log(MESSAGE, resource=resource)
432+
# ensure well-formed dict is converted to a resource
433+
resource = {"type": "gae_app", "labels": []}
434+
ENTRIES = [
435+
{
436+
"logName": "projects/%s/logs/%s" % (self.PROJECT, self.LOGGER_NAME),
437+
"textPayload": MESSAGE,
438+
"resource": resource,
439+
}
440+
]
441+
logger.log(MESSAGE, resource=resource)
442+
self.assertEqual(api._write_entries_called_with, (ENTRIES, None, None, None))
443+
444+
def test_log_lowercase_severity(self):
445+
"""
446+
lower case severity strings should be accepted
447+
"""
448+
from google.cloud.logging_v2.handlers._monitored_resources import (
449+
detect_resource,
450+
)
451+
452+
for lower_severity in [
453+
"default",
454+
"debug",
455+
"info",
456+
"notice",
457+
"warning",
458+
"error",
459+
"critical",
460+
"alert",
461+
"emergency",
462+
]:
463+
MESSAGE = "hello world"
464+
RESOURCE = detect_resource(self.PROJECT)._to_dict()
465+
ENTRIES = [
466+
{
467+
"logName": "projects/%s/logs/%s" % (self.PROJECT, self.LOGGER_NAME),
468+
"textPayload": MESSAGE,
469+
"resource": RESOURCE,
470+
"severity": lower_severity.upper(),
471+
}
472+
]
473+
client = _Client(self.PROJECT)
474+
api = client.logging_api = _DummyLoggingAPI()
475+
logger = self._make_one(self.LOGGER_NAME, client=client)
476+
477+
logger.log(MESSAGE, severity=lower_severity)
478+
479+
self.assertEqual(
480+
api._write_entries_called_with, (ENTRIES, None, None, None)
481+
)
482+
382483
def test_log_proto_defaults(self):
383484
from google.cloud.logging_v2.handlers._monitored_resources import (
384485
detect_resource,

0 commit comments

Comments
 (0)