Skip to content

Commit 68baa1b

Browse files
committed
Merge pull request #1569 from tseaver/logging-client_list_entries
Add 'Client.list_entries'.
2 parents e898bca + d7a2e72 commit 68baa1b

File tree

11 files changed

+758
-5
lines changed

11 files changed

+758
-5
lines changed

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
logging-usage
114114
Client <logging-client>
115115
logging-logger
116+
logging-entries
116117

117118
.. toctree::
118119
:maxdepth: 0

docs/logging-entries.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Entries
2+
=======
3+
4+
.. automodule:: gcloud.logging.entries
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:
8+

gcloud/_helpers.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@
3838

3939
_NOW = datetime.datetime.utcnow # To be replaced by tests.
4040
_RFC3339_MICROS = '%Y-%m-%dT%H:%M:%S.%fZ'
41+
_RFC3339_NO_FRACTION = '%Y-%m-%dT%H:%M:%S'
42+
# datetime.strptime cannot handle nanosecond precision: parse w/ regex
43+
_RFC3339_NANOS = re.compile(r"""
44+
(?P<no_fraction>
45+
\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2} # YYYY-MM-DDTHH:MM:SS
46+
)
47+
\. # decimal point
48+
(?P<nanos>\d{9}) # nanoseconds
49+
Z # Zulu
50+
""", re.VERBOSE)
4151

4252

4353
class _LocalStack(Local):
@@ -301,7 +311,7 @@ def _total_seconds(offset):
301311

302312

303313
def _rfc3339_to_datetime(dt_str):
304-
"""Convert a string to a native timestamp.
314+
"""Convert a microsecond-precision timetamp to a native datetime.
305315
306316
:type dt_str: str
307317
:param dt_str: The string to convert.
@@ -313,6 +323,32 @@ def _rfc3339_to_datetime(dt_str):
313323
dt_str, _RFC3339_MICROS).replace(tzinfo=UTC)
314324

315325

326+
def _rfc3339_nanos_to_datetime(dt_str):
327+
"""Convert a nanosecond-precision timestamp to a native datetime.
328+
329+
.. note::
330+
331+
Python datetimes do not support nanosecond precision; this function
332+
therefore truncates such values to microseconds.
333+
334+
:type dt_str: str
335+
:param dt_str: The string to convert.
336+
337+
:rtype: :class:`datetime.datetime`
338+
:returns: The datetime object created from the string.
339+
"""
340+
with_nanos = _RFC3339_NANOS.match(dt_str)
341+
if with_nanos is None:
342+
raise ValueError(
343+
'Timestamp: %r, does not match pattern: %r' % (
344+
dt_str, _RFC3339_NANOS.pattern))
345+
bare_seconds = datetime.datetime.strptime(
346+
with_nanos.group('no_fraction'), _RFC3339_NO_FRACTION)
347+
nanos = int(with_nanos.group('nanos'))
348+
micros = nanos // 1000
349+
return bare_seconds.replace(microsecond=micros, tzinfo=UTC)
350+
351+
316352
def _datetime_to_rfc3339(value):
317353
"""Convert a native timestamp to a string.
318354

gcloud/logging/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,5 @@
1919

2020

2121
SCOPE = Connection.SCOPE
22+
ASCENDING = 'timestamp asc'
23+
DESCENDING = 'timestamp desc'

gcloud/logging/_helpers.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Copyright 2016 Google Inc. All rights reserved.
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+
"""Helper functions for shared behavior."""
16+
17+
import re
18+
19+
from gcloud._helpers import _name_from_project_path
20+
21+
22+
_LOGGER_TEMPLATE = re.compile(r"""
23+
projects/ # static prefix
24+
(?P<project>[^/]+) # initial letter, wordchars + hyphen
25+
/logs/ # static midfix
26+
(?P<name>[^/]+) # initial letter, wordchars + allowed punc
27+
""", re.VERBOSE)
28+
29+
30+
def logger_name_from_path(path, project):
31+
"""Validate a logger URI path and get the logger name.
32+
33+
:type path: string
34+
:param path: URI path for a logger API request.
35+
36+
:type project: string
37+
:param project: The project associated with the request. It is
38+
included for validation purposes.
39+
40+
:rtype: string
41+
:returns: Topic name parsed from ``path``.
42+
:raises: :class:`ValueError` if the ``path`` is ill-formed or if
43+
the project from the ``path`` does not agree with the
44+
``project`` passed in.
45+
"""
46+
return _name_from_project_path(path, project, _LOGGER_TEMPLATE)

gcloud/logging/client.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
from gcloud.client import JSONClient
1919
from gcloud.logging.connection import Connection
20+
from gcloud.logging.entries import StructEntry
21+
from gcloud.logging.entries import TextEntry
2022
from gcloud.logging.logger import Logger
2123

2224

@@ -53,3 +55,82 @@ def logger(self, name):
5355
:returns: Logger created with the current client.
5456
"""
5557
return Logger(name, client=self)
58+
59+
def _entry_from_resource(self, resource, loggers):
60+
"""Detect correct entry type from resource and instantiate.
61+
62+
:type resource: dict
63+
:param resource: one entry resource from API response
64+
65+
:type loggers: dict or None
66+
:param loggers: A mapping of logger fullnames -> loggers. If not
67+
passed, the entry will have a newly-created logger.
68+
69+
:rtype; One of:
70+
:class:`gcloud.logging.entries.TextEntry`,
71+
:class:`gcloud.logging.entries.StructEntry`,
72+
:returns: the entry instance, constructed via the resource
73+
"""
74+
if 'textPayload' in resource:
75+
return TextEntry.from_api_repr(resource, self, loggers)
76+
elif 'jsonPayload' in resource:
77+
return StructEntry.from_api_repr(resource, self, loggers)
78+
raise ValueError('Cannot parse job resource')
79+
80+
def list_entries(self, projects=None, filter_=None, order_by=None,
81+
page_size=None, page_token=None):
82+
"""Return a page of log entries.
83+
84+
See:
85+
https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/entries/list
86+
87+
:type projects: list of strings
88+
:param projects: project IDs to include. If not passed,
89+
defaults to the project bound to the client.
90+
91+
:type filter_: string
92+
:param filter_: a filter expression. See:
93+
https://cloud.google.com/logging/docs/view/advanced_filters
94+
95+
:type order_by: string
96+
:param order_by: One of :data:`gcloud.logging.ASCENDING` or
97+
:data:`gcloud.logging.DESCENDING`.
98+
99+
:type page_size: int
100+
:param page_size: maximum number of topics to return, If not passed,
101+
defaults to a value set by the API.
102+
103+
:type page_token: string
104+
:param page_token: opaque marker for the next "page" of topics. If not
105+
passed, the API will return the first page of
106+
topics.
107+
108+
:rtype: tuple, (list, str)
109+
:returns: list of :class:`gcloud.logging.entry.TextEntry`, plus a
110+
"next page token" string: if not None, indicates that
111+
more topics can be retrieved with another call (pass that
112+
value as ``page_token``).
113+
"""
114+
if projects is None:
115+
projects = [self.project]
116+
117+
params = {'projectIds': projects}
118+
119+
if filter_ is not None:
120+
params['filter'] = filter_
121+
122+
if order_by is not None:
123+
params['orderBy'] = order_by
124+
125+
if page_size is not None:
126+
params['pageSize'] = page_size
127+
128+
if page_token is not None:
129+
params['pageToken'] = page_token
130+
131+
resp = self.connection.api_request(method='POST', path='/entries:list',
132+
data=params)
133+
loggers = {}
134+
entries = [self._entry_from_resource(resource, loggers)
135+
for resource in resp.get('entries', ())]
136+
return entries, resp.get('nextPageToken')

gcloud/logging/entries.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Copyright 2016 Google Inc. All rights reserved.
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+
"""Log entries within the Google Cloud Logging API."""
16+
17+
from gcloud._helpers import _rfc3339_nanos_to_datetime
18+
from gcloud.logging._helpers import logger_name_from_path
19+
20+
21+
class _BaseEntry(object):
22+
"""Base class for TextEntry, StructEntry.
23+
24+
:type payload: text or dict
25+
:param payload: The payload passed as ``textPayload``, ``jsonPayload``,
26+
or ``protoPayload``.
27+
28+
:type logger: :class:`gcloud.logging.logger.Logger`
29+
:param logger: the logger used to write the entry.
30+
31+
:type insert_id: text, or :class:`NoneType`
32+
:param insert_id: (optional) the ID used to identify an entry uniquely.
33+
34+
:type timestamp: :class:`datetime.datetime`, or :class:`NoneType`
35+
:param timestamp: (optional) timestamp for the entry
36+
"""
37+
def __init__(self, payload, logger, insert_id=None, timestamp=None):
38+
self.payload = payload
39+
self.logger = logger
40+
self.insert_id = insert_id
41+
self.timestamp = timestamp
42+
43+
@classmethod
44+
def from_api_repr(cls, resource, client, loggers=None):
45+
"""Factory: construct an entry given its API representation
46+
47+
:type resource: dict
48+
:param resource: text entry resource representation returned from
49+
the API
50+
51+
:type client: :class:`gcloud.logging.client.Client`
52+
:param client: Client which holds credentials and project
53+
configuration.
54+
55+
:type loggers: dict or None
56+
:param loggers: A mapping of logger fullnames -> loggers. If not
57+
passed, the entry will have a newly-created logger.
58+
59+
:rtype: :class:`gcloud.logging.entries.TextEntry`
60+
:returns: Text entry parsed from ``resource``.
61+
"""
62+
if loggers is None:
63+
loggers = {}
64+
logger_fullname = resource['logName']
65+
logger = loggers.get(logger_fullname)
66+
if logger is None:
67+
logger_name = logger_name_from_path(
68+
logger_fullname, client.project)
69+
logger = loggers[logger_fullname] = client.logger(logger_name)
70+
payload = resource[cls._PAYLOAD_KEY]
71+
insert_id = resource.get('insertId')
72+
timestamp = resource.get('timestamp')
73+
if timestamp is not None:
74+
timestamp = _rfc3339_nanos_to_datetime(timestamp)
75+
return cls(payload, logger, insert_id, timestamp)
76+
77+
78+
class TextEntry(_BaseEntry):
79+
"""Entry created via a write request with ``textPayload``.
80+
81+
See:
82+
https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/LogEntry
83+
"""
84+
_PAYLOAD_KEY = 'textPayload'
85+
86+
87+
class StructEntry(_BaseEntry):
88+
"""Entry created via a write request with ``jsonPayload``.
89+
90+
See:
91+
https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/LogEntry
92+
"""
93+
_PAYLOAD_KEY = 'jsonPayload'

gcloud/logging/test__helpers.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Copyright 2016 Google Inc. All rights reserved.
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+
import unittest2
16+
17+
18+
class Test_logger_name_from_path(unittest2.TestCase):
19+
20+
def _callFUT(self, path, project):
21+
from gcloud.logging._helpers import logger_name_from_path
22+
return logger_name_from_path(path, project)
23+
24+
def test_w_simple_name(self):
25+
LOGGER_NAME = 'LOGGER_NAME'
26+
PROJECT = 'my-project-1234'
27+
PATH = 'projects/%s/logs/%s' % (PROJECT, LOGGER_NAME)
28+
logger_name = self._callFUT(PATH, PROJECT)
29+
self.assertEqual(logger_name, LOGGER_NAME)
30+
31+
def test_w_name_w_all_extras(self):
32+
LOGGER_NAME = 'LOGGER_NAME-part.one~part.two%part-three'
33+
PROJECT = 'my-project-1234'
34+
PATH = 'projects/%s/logs/%s' % (PROJECT, LOGGER_NAME)
35+
logger_name = self._callFUT(PATH, PROJECT)
36+
self.assertEqual(logger_name, LOGGER_NAME)

0 commit comments

Comments
 (0)