Skip to content

Commit b5902a0

Browse files
authored
Merge pull request googleapis#2636 from dhermes/logging-iterators-1
Converting Logging client->list_entries to iterator.
2 parents 2814567 + c89774b commit b5902a0

File tree

15 files changed

+671
-292
lines changed

15 files changed

+671
-292
lines changed

core/google/cloud/iterator.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ class HTTPIterator(Iterator):
298298
_PAGE_TOKEN = 'pageToken'
299299
_MAX_RESULTS = 'maxResults'
300300
_RESERVED_PARAMS = frozenset([_PAGE_TOKEN, _MAX_RESULTS])
301+
_HTTP_METHOD = 'GET'
301302

302303
def __init__(self, client, path, item_to_value,
303304
items_key=DEFAULT_ITEMS_KEY,
@@ -378,9 +379,19 @@ def _get_next_page_response(self):
378379
:rtype: dict
379380
:returns: The parsed JSON response of the next page's contents.
380381
"""
381-
return self.client.connection.api_request(
382-
method='GET', path=self.path,
383-
query_params=self._get_query_params())
382+
params = self._get_query_params()
383+
if self._HTTP_METHOD == 'GET':
384+
return self.client.connection.api_request(
385+
method=self._HTTP_METHOD,
386+
path=self.path,
387+
query_params=params)
388+
elif self._HTTP_METHOD == 'POST':
389+
return self.client.connection.api_request(
390+
method=self._HTTP_METHOD,
391+
path=self.path,
392+
data=params)
393+
else:
394+
raise ValueError('Unexpected HTTP method', self._HTTP_METHOD)
384395

385396

386397
class GAXIterator(Iterator):

core/unit_tests/test_iterator.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,32 @@ def test__get_next_page_response_new_no_token_in_response(self):
436436
self.assertEqual(kw['path'], path)
437437
self.assertEqual(kw['query_params'], {})
438438

439+
def test__get_next_page_response_with_post(self):
440+
path = '/foo'
441+
returned = {'green': 'eggs', 'ham': 55}
442+
connection = _Connection(returned)
443+
client = _Client(connection)
444+
iterator = self._makeOne(client, path, None)
445+
iterator._HTTP_METHOD = 'POST'
446+
response = iterator._get_next_page_response()
447+
self.assertEqual(response, returned)
448+
449+
self.assertEqual(len(connection._requested), 1)
450+
called_kwargs = connection._requested[0]
451+
self.assertEqual(called_kwargs, {
452+
'method': iterator._HTTP_METHOD,
453+
'path': path,
454+
'data': {},
455+
})
456+
457+
def test__get_next_page_bad_http_method(self):
458+
path = '/foo'
459+
client = _Client(None)
460+
iterator = self._makeOne(client, path, None)
461+
iterator._HTTP_METHOD = 'NOT-A-VERB'
462+
with self.assertRaises(ValueError):
463+
iterator._get_next_page_response()
464+
439465

440466
class TestGAXIterator(unittest.TestCase):
441467

docs/logging-usage.rst

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,7 @@ Fetch entries for the default project.
6868

6969
>>> from google.cloud import logging
7070
>>> client = logging.Client()
71-
>>> entries, token = client.list_entries() # API call
72-
>>> for entry in entries:
71+
>>> for entry in client.list_entries(): # API call(s)
7372
... timestamp = entry.timestamp.isoformat()
7473
... print('%sZ: %s' %
7574
... (timestamp, entry.payload))
@@ -82,8 +81,9 @@ Fetch entries across multiple projects.
8281

8382
>>> from google.cloud import logging
8483
>>> client = logging.Client()
85-
>>> entries, token = client.list_entries(
86-
... project_ids=['one-project', 'another-project']) # API call
84+
>>> iterator = client.list_entries(
85+
... project_ids=['one-project', 'another-project'])
86+
>>> entries = list(iterator) # API call(s)
8787

8888
Filter entries retrieved using the `Advanced Logs Filters`_ syntax
8989

@@ -94,15 +94,17 @@ Filter entries retrieved using the `Advanced Logs Filters`_ syntax
9494
>>> from google.cloud import logging
9595
>>> client = logging.Client()
9696
>>> FILTER = "log:log_name AND textPayload:simple"
97-
>>> entries, token = client.list_entries(filter=FILTER) # API call
97+
>>> iterator = client.list_entries(filter=FILTER)
98+
>>> entries = list(iterator) # API call(s)
9899

99100
Sort entries in descending timestamp order.
100101

101102
.. doctest::
102103

103104
>>> from google.cloud import logging
104105
>>> client = logging.Client()
105-
>>> entries, token = client.list_entries(order_by=logging.DESCENDING) # API call
106+
>>> iterator = client.list_entries(order_by=logging.DESCENDING)
107+
>>> entries = list(iterator) # API call(s)
106108

107109
Retrieve entries in batches of 10, iterating until done.
108110

@@ -111,12 +113,15 @@ Retrieve entries in batches of 10, iterating until done.
111113
>>> from google.cloud import logging
112114
>>> client = logging.Client()
113115
>>> retrieved = []
114-
>>> token = None
115-
>>> while True:
116-
... entries, token = client.list_entries(page_size=10, page_token=token) # API call
117-
... retrieved.extend(entries)
118-
... if token is None:
119-
... break
116+
>>> iterator = client.list_entries(page_size=10, page_token=token)
117+
>>> pages = iterator.pages
118+
>>> page1 = next(pages) # API call
119+
>>> for entry in page1:
120+
... do_something(entry)
121+
...
122+
>>> page2 = next(pages) # API call
123+
>>> for entry in page2:
124+
... do_something_else(entry)
120125

121126
Retrieve entries for a single logger, sorting in descending timestamp order:
122127

@@ -125,7 +130,8 @@ Retrieve entries for a single logger, sorting in descending timestamp order:
125130
>>> from google.cloud import logging
126131
>>> client = logging.Client()
127132
>>> logger = client.logger('log_name')
128-
>>> entries, token = logger.list_entries(order_by=logging.DESCENDING) # API call
133+
>>> iterator = logger.list_entries(order_by=logging.DESCENDING)
134+
>>> entries = list(iterator) # API call(s)
129135

130136
Delete all entries for a logger
131137
-------------------------------

logging/README.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ Example of fetching entries:
4747

4848
.. code:: python
4949
50-
entries, token = logger.list_entries()
51-
for entry in entries:
50+
for entry in logger.list_entries():
5251
print(entry.payload)
5352
5453
See the ``google-cloud-python`` API `logging documentation`_ to learn how to

logging/google/cloud/logging/_gax.py

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
"""GAX wrapper for Logging API requests."""
1616

17+
import functools
18+
1719
from google.gax import CallOptions
1820
from google.gax import INITIAL_PAGE
1921
from google.gax.errors import GaxError
@@ -28,6 +30,8 @@
2830
from google.cloud._helpers import _datetime_to_rfc3339
2931
from google.cloud.exceptions import Conflict
3032
from google.cloud.exceptions import NotFound
33+
from google.cloud.iterator import GAXIterator
34+
from google.cloud.logging._helpers import entry_from_resource
3135

3236

3337
class _LoggingAPI(object):
@@ -36,9 +40,13 @@ class _LoggingAPI(object):
3640
:type gax_api:
3741
:class:`google.logging.v2.logging_service_v2_api.LoggingServiceV2Api`
3842
:param gax_api: API object used to make GAX requests.
43+
44+
:type client: :class:`~google.cloud.logging.client.Client`
45+
:param client: The client that owns this API object.
3946
"""
40-
def __init__(self, gax_api):
47+
def __init__(self, gax_api, client):
4148
self._gax_api = gax_api
49+
self._client = client
4250

4351
def list_entries(self, projects, filter_='', order_by='',
4452
page_size=0, page_token=None):
@@ -49,8 +57,9 @@ def list_entries(self, projects, filter_='', order_by='',
4957
defaults to the project bound to the API's client.
5058
5159
:type filter_: str
52-
:param filter_: a filter expression. See:
53-
https://cloud.google.com/logging/docs/view/advanced_filters
60+
:param filter_:
61+
a filter expression. See:
62+
https://cloud.google.com/logging/docs/view/advanced_filters
5463
5564
:type order_by: str
5665
:param order_by: One of :data:`~google.cloud.logging.ASCENDING`
@@ -65,21 +74,24 @@ def list_entries(self, projects, filter_='', order_by='',
6574
passed, the API will return the first page of
6675
entries.
6776
68-
:rtype: tuple, (list, str)
69-
:returns: list of mappings, plus a "next page token" string:
70-
if not None, indicates that more entries can be retrieved
71-
with another call (pass that value as ``page_token``).
77+
:rtype: :class:`~google.cloud.iterator.Iterator`
78+
:returns: Iterator of :class:`~google.cloud.logging.entries._BaseEntry`
79+
accessible to the current API.
7280
"""
7381
if page_token is None:
7482
page_token = INITIAL_PAGE
7583
options = CallOptions(page_token=page_token)
7684
page_iter = self._gax_api.list_log_entries(
7785
projects, filter_=filter_, order_by=order_by,
7886
page_size=page_size, options=options)
79-
entries = [MessageToDict(entry_pb)
80-
for entry_pb in page_iter.next()]
81-
token = page_iter.page_token or None
82-
return entries, token
87+
88+
# We attach a mutable loggers dictionary so that as Logger
89+
# objects are created by entry_from_resource, they can be
90+
# re-used by other log entries from the same logger.
91+
loggers = {}
92+
item_to_value = functools.partial(
93+
_item_to_entry, loggers=loggers)
94+
return GAXIterator(self._client, page_iter, item_to_value)
8395

8496
def write_entries(self, entries, logger_name=None, resource=None,
8597
labels=None):
@@ -430,3 +442,34 @@ def _log_entry_mapping_to_pb(mapping):
430442
mapping['timestamp'] = _datetime_to_rfc3339(mapping['timestamp'])
431443
ParseDict(mapping, entry_pb)
432444
return entry_pb
445+
446+
447+
def _item_to_entry(iterator, entry_pb, loggers):
448+
"""Convert a log entry protobuf to the native object.
449+
450+
.. note::
451+
452+
This method does not have the correct signature to be used as
453+
the ``item_to_value`` argument to
454+
:class:`~google.cloud.iterator.Iterator`. It is intended to be
455+
patched with a mutable ``loggers`` argument that can be updated
456+
on subsequent calls. For an example, see how the method is
457+
used above in :meth:`_LoggingAPI.list_entries`.
458+
459+
:type iterator: :class:`~google.cloud.iterator.Iterator`
460+
:param iterator: The iterator that is currently in use.
461+
462+
:type entry_pb: :class:`~google.logging.v2.log_entry_pb2.LogEntry`
463+
:param entry_pb: Log entry protobuf returned from the API.
464+
465+
:type loggers: dict
466+
:param loggers:
467+
A mapping of logger fullnames -> loggers. If the logger
468+
that owns the entry is not in ``loggers``, the entry
469+
will have a newly-created logger.
470+
471+
:rtype: :class:`~google.cloud.logging.entries._BaseEntry`
472+
:returns: The next log entry in the page.
473+
"""
474+
resource = MessageToDict(entry_pb)
475+
return entry_from_resource(resource, iterator.client, loggers)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Copyright 2016 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Common logging helpers."""
16+
17+
18+
from google.cloud.logging.entries import ProtobufEntry
19+
from google.cloud.logging.entries import StructEntry
20+
from google.cloud.logging.entries import TextEntry
21+
22+
23+
def entry_from_resource(resource, client, loggers):
24+
"""Detect correct entry type from resource and instantiate.
25+
26+
:type resource: dict
27+
:param resource: one entry resource from API response
28+
29+
:type client: :class:`~google.cloud.logging.client.Client`
30+
:param client: Client that owns the log entry.
31+
32+
:type loggers: dict
33+
:param loggers:
34+
A mapping of logger fullnames -> loggers. If the logger
35+
that owns the entry is not in ``loggers``, the entry
36+
will have a newly-created logger.
37+
38+
:rtype: :class:`~google.cloud.logging.entries._BaseEntry`
39+
:returns: The entry instance, constructed via the resource
40+
"""
41+
if 'textPayload' in resource:
42+
return TextEntry.from_api_repr(resource, client, loggers)
43+
elif 'jsonPayload' in resource:
44+
return StructEntry.from_api_repr(resource, client, loggers)
45+
elif 'protoPayload' in resource:
46+
return ProtobufEntry.from_api_repr(resource, client, loggers)
47+
48+
raise ValueError('Cannot parse log entry resource')

0 commit comments

Comments
 (0)