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 6be12f2

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

File tree

Expand file treeCollapse file tree

4 files changed

+146
-6
lines changed
Filter options
Expand file treeCollapse file tree

4 files changed

+146
-6
lines changed

‎google/cloud/logging_v2/logger.py

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

Copy file name to clipboardExpand all lines: tests/system/test_system.py
+19Lines changed: 19 additions & 0 deletions
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

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