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 97e32b6

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

File tree

Expand file treeCollapse file tree

3 files changed

+124
-7
lines changed
Filter options
Expand file treeCollapse file tree

3 files changed

+124
-7
lines changed

‎google/cloud/logging_v2/entries.py

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

Copy file name to clipboardExpand all lines: tests/unit/test_entries.py
+82-2Lines changed: 82 additions & 2 deletions
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

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