Skip to content

Commit 97e32b6

Browse files
committed
fix: allow reading logs from non-project paths (#444)
1 parent a760e02 commit 97e32b6

File tree

3 files changed

+124
-7
lines changed

3 files changed

+124
-7
lines changed

google/cloud/logging_v2/entries.py

+12-5
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,12 @@
4646
)
4747

4848

49-
def logger_name_from_path(path):
49+
def logger_name_from_path(path, project=None):
5050
"""Validate a logger URI path and get the logger name.
5151
5252
Args:
5353
path (str): URI path for a logger API request
54+
project (str): The project the path is expected to belong to
5455
5556
Returns:
5657
str: Logger name parsed from ``path``.
@@ -59,7 +60,7 @@ def logger_name_from_path(path):
5960
ValueError: If the ``path`` is ill-formed of if the project
6061
from ``path`` does not agree with the ``project`` passed in.
6162
"""
62-
return _name_from_project_path(path, None, _LOGGER_TEMPLATE)
63+
return _name_from_project_path(path, project, _LOGGER_TEMPLATE)
6364

6465

6566
def _int_or_none(value):
@@ -155,7 +156,8 @@ def from_api_repr(cls, resource, client, *, loggers=None):
155156
Client which holds credentials and project configuration.
156157
loggers (Optional[dict]):
157158
A mapping of logger fullnames -> loggers. If not
158-
passed, the entry will have a newly-created logger.
159+
passed, the entry will have a newly-created logger if possible,
160+
or an empty logger field if not.
159161
160162
Returns:
161163
google.cloud.logging.entries.LogEntry: Log entry parsed from ``resource``.
@@ -165,8 +167,13 @@ def from_api_repr(cls, resource, client, *, loggers=None):
165167
logger_fullname = resource["logName"]
166168
logger = loggers.get(logger_fullname)
167169
if logger is None:
168-
logger_name = logger_name_from_path(logger_fullname)
169-
logger = loggers[logger_fullname] = client.logger(logger_name)
170+
# attempt to create a logger if possible
171+
try:
172+
logger_name = logger_name_from_path(logger_fullname, client.project)
173+
logger = loggers[logger_fullname] = client.logger(logger_name)
174+
except ValueError:
175+
# log name is not scoped to a project. Leave logger as None
176+
pass
170177
payload = cls._extract_payload(resource)
171178
insert_id = resource.get("insertId")
172179
timestamp = resource.get("timestamp")

tests/unit/test_entries.py

+82-2
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818

1919

2020
class Test_logger_name_from_path(unittest.TestCase):
21-
def _call_fut(self, path):
21+
def _call_fut(self, path, project=None):
2222
from google.cloud.logging_v2.entries import logger_name_from_path
2323

24-
return logger_name_from_path(path)
24+
return logger_name_from_path(path, project)
2525

2626
def test_w_simple_name(self):
2727
LOGGER_NAME = "LOGGER_NAME"
@@ -37,6 +37,30 @@ def test_w_name_w_all_extras(self):
3737
logger_name = self._call_fut(PATH)
3838
self.assertEqual(logger_name, LOGGER_NAME)
3939

40+
def test_w_wrong_project(self):
41+
LOGGER_NAME = "LOGGER_NAME"
42+
IN_PROJECT = "in-project"
43+
PATH_PROJECT = "path-project"
44+
PATH = "projects/%s/logs/%s" % (PATH_PROJECT, LOGGER_NAME)
45+
with self.assertRaises(ValueError):
46+
self._call_fut(PATH, IN_PROJECT)
47+
48+
def test_invalid_inputs(self):
49+
invalid_list = [
50+
"",
51+
"abc/123/logs/456",
52+
"projects//logs/",
53+
"projects/123/logs",
54+
"projects/123logs/",
55+
"projects123/logs",
56+
"project/123",
57+
"projects123logs456",
58+
"/logs/123",
59+
]
60+
for path in invalid_list:
61+
with self.assertRaises(ValueError):
62+
self._call_fut(path)
63+
4064

4165
class Test__int_or_none(unittest.TestCase):
4266
def _call_fut(self, value):
@@ -315,6 +339,62 @@ def test_from_api_repr_w_loggers_w_logger_match(self):
315339
self.assertEqual(entry.operation, OPERATION)
316340
self.assertIsNone(entry.payload)
317341

342+
def test_from_api_repr_w_folder_path(self):
343+
from datetime import datetime
344+
from datetime import timedelta
345+
from google.cloud._helpers import UTC
346+
347+
client = _Client(self.PROJECT)
348+
IID = "IID"
349+
NOW = datetime.utcnow().replace(tzinfo=UTC)
350+
LATER = NOW + timedelta(seconds=1)
351+
TIMESTAMP = _datetime_to_rfc3339_w_nanos(NOW)
352+
RECEIVED = _datetime_to_rfc3339_w_nanos(LATER)
353+
LOG_NAME = "folders/%s/logs/%s" % (self.PROJECT, self.LOGGER_NAME)
354+
LABELS = {"foo": "bar", "baz": "qux"}
355+
TRACE = "12345678-1234-5678-1234-567812345678"
356+
SPANID = "000000000000004a"
357+
FILE = "my_file.py"
358+
LINE_NO = 123
359+
FUNCTION = "my_function"
360+
SOURCE_LOCATION = {"file": FILE, "line": str(LINE_NO), "function": FUNCTION}
361+
OP_ID = "OP_ID"
362+
PRODUCER = "PRODUCER"
363+
OPERATION = {"id": OP_ID, "producer": PRODUCER, "first": True, "last": False}
364+
API_REPR = {
365+
"logName": LOG_NAME,
366+
"insertId": IID,
367+
"timestamp": TIMESTAMP,
368+
"receiveTimestamp": RECEIVED,
369+
"labels": LABELS,
370+
"trace": TRACE,
371+
"spanId": SPANID,
372+
"traceSampled": True,
373+
"sourceLocation": SOURCE_LOCATION,
374+
"operation": OPERATION,
375+
}
376+
klass = self._get_target_class()
377+
378+
entry = klass.from_api_repr(API_REPR, client)
379+
380+
self.assertEqual(entry.log_name, LOG_NAME)
381+
self.assertIsNone(entry.logger)
382+
self.assertEqual(entry.insert_id, IID)
383+
self.assertEqual(entry.timestamp, NOW)
384+
self.assertEqual(entry.received_timestamp, LATER)
385+
self.assertEqual(entry.labels, LABELS)
386+
self.assertEqual(entry.trace, TRACE)
387+
self.assertEqual(entry.span_id, SPANID)
388+
self.assertTrue(entry.trace_sampled)
389+
390+
source_location = entry.source_location
391+
self.assertEqual(source_location["file"], FILE)
392+
self.assertEqual(source_location["line"], LINE_NO)
393+
self.assertEqual(source_location["function"], FUNCTION)
394+
395+
self.assertEqual(entry.operation, OPERATION)
396+
self.assertIsNone(entry.payload)
397+
318398
def test_to_api_repr_w_source_location_no_line(self):
319399
from google.cloud.logging_v2.logger import _GLOBAL_RESOURCE
320400

tests/unit/test_logger.py

+30
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,36 @@ def test_list_entries_limit(self):
937937
},
938938
)
939939

940+
def test_list_entries_folder(self):
941+
from google.cloud.logging import TextEntry
942+
from google.cloud.logging import Client
943+
944+
client = Client(
945+
project=self.PROJECT, credentials=_make_credentials(), _use_grpc=False
946+
)
947+
FOLDER_ID = "123"
948+
LOG_NAME = f"folders/{FOLDER_ID}/logs/cloudaudit.googleapis.com%2Fdata_access"
949+
950+
ENTRIES = [
951+
{
952+
"textPayload": "hello world",
953+
"insertId": "1",
954+
"resource": {"type": "global"},
955+
"logName": LOG_NAME,
956+
},
957+
]
958+
returned = {"entries": ENTRIES}
959+
client._connection = _Connection(returned)
960+
961+
iterator = client.list_entries(resource_names=[f"folder/{FOLDER_ID}"],)
962+
entries = list(iterator)
963+
# Check the entries.
964+
self.assertEqual(len(entries), 1)
965+
entry = entries[0]
966+
self.assertIsInstance(entry, TextEntry)
967+
self.assertIsNone(entry.logger)
968+
self.assertEqual(entry.log_name, LOG_NAME)
969+
940970

941971
class TestBatch(unittest.TestCase):
942972

0 commit comments

Comments
 (0)