Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
logging-usage
Client <logging-client>
logging-logger
logging-entries

.. toctree::
:maxdepth: 0
Expand Down
8 changes: 8 additions & 0 deletions docs/logging-entries.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Entries
=======

.. automodule:: gcloud.logging.entries
:members:
:undoc-members:
:show-inheritance:

38 changes: 37 additions & 1 deletion gcloud/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@

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


class _LocalStack(Local):
Expand Down Expand Up @@ -301,7 +311,7 @@ def _total_seconds(offset):


def _rfc3339_to_datetime(dt_str):
"""Convert a string to a native timestamp.
"""Convert a microsecond-precision timetamp to a native datetime.

:type dt_str: str
:param dt_str: The string to convert.
Expand All @@ -313,6 +323,32 @@ def _rfc3339_to_datetime(dt_str):
dt_str, _RFC3339_MICROS).replace(tzinfo=UTC)


def _rfc3339_nanos_to_datetime(dt_str):
"""Convert a nanosecond-precision timestamp to a native datetime.

.. note::

Python datetimes do not support nanosecond precision; this function
therefore truncates such values to microseconds.

:type dt_str: str
:param dt_str: The string to convert.

:rtype: :class:`datetime.datetime`
:returns: The datetime object created from the string.
"""
with_nanos = _RFC3339_NANOS.match(dt_str)
if with_nanos is None:
raise ValueError(
'Timestamp: %r, does not match pattern: %r' % (
dt_str, _RFC3339_NANOS.pattern))
bare_seconds = datetime.datetime.strptime(
with_nanos.group('no_fraction'), _RFC3339_NO_FRACTION)
nanos = int(with_nanos.group('nanos'))
micros = nanos // 1000
return bare_seconds.replace(microsecond=micros, tzinfo=UTC)


def _datetime_to_rfc3339(value):
"""Convert a native timestamp to a string.

Expand Down
2 changes: 2 additions & 0 deletions gcloud/logging/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@


SCOPE = Connection.SCOPE
ASCENDING = 'timestamp asc'
DESCENDING = 'timestamp desc'
46 changes: 46 additions & 0 deletions gcloud/logging/_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Helper functions for shared behavior."""

import re

from gcloud._helpers import _name_from_project_path


_LOGGER_TEMPLATE = re.compile(r"""
projects/ # static prefix
(?P<project>[^/]+) # initial letter, wordchars + hyphen
/logs/ # static midfix
(?P<name>[^/]+) # initial letter, wordchars + allowed punc
""", re.VERBOSE)


def logger_name_from_path(path, project):
"""Validate a logger URI path and get the logger name.

:type path: string
:param path: URI path for a logger API request.

:type project: string
:param project: The project associated with the request. It is
included for validation purposes.

:rtype: string
:returns: Topic name parsed from ``path``.
:raises: :class:`ValueError` if the ``path`` is ill-formed or if
the project from the ``path`` does not agree with the
``project`` passed in.
"""
return _name_from_project_path(path, project, _LOGGER_TEMPLATE)
81 changes: 81 additions & 0 deletions gcloud/logging/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

from gcloud.client import JSONClient
from gcloud.logging.connection import Connection
from gcloud.logging.entries import StructEntry
from gcloud.logging.entries import TextEntry
from gcloud.logging.logger import Logger


Expand Down Expand Up @@ -53,3 +55,82 @@ def logger(self, name):
:returns: Logger created with the current client.
"""
return Logger(name, client=self)

def _entry_from_resource(self, resource, loggers):
"""Detect correct entry type from resource and instantiate.

:type resource: dict
:param resource: one entry resource from API response

:type loggers: dict or None
:param loggers: A mapping of logger fullnames -> loggers. If not
passed, the entry will have a newly-created logger.

:rtype; One of:
:class:`gcloud.logging.entries.TextEntry`,
:class:`gcloud.logging.entries.StructEntry`,
:returns: the entry instance, constructed via the resource
"""
if 'textPayload' in resource:
return TextEntry.from_api_repr(resource, self, loggers)
elif 'jsonPayload' in resource:
return StructEntry.from_api_repr(resource, self, loggers)
raise ValueError('Cannot parse job resource')

def list_entries(self, projects=None, filter_=None, order_by=None,
page_size=None, page_token=None):
"""Return a page of log entries.

See:
https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/entries/list

:type projects: list of strings
:param projects: project IDs to include. If not passed,
defaults to the project bound to the client.

:type filter_: string
:param filter_: a filter expression. See:
https://cloud.google.com/logging/docs/view/advanced_filters

:type order_by: string
:param order_by: One of :data:`gcloud.logging.ASCENDING` or
:data:`gcloud.logging.DESCENDING`.

:type page_size: int
:param page_size: maximum number of topics to return, If not passed,
defaults to a value set by the API.

:type page_token: string
:param page_token: opaque marker for the next "page" of topics. If not
passed, the API will return the first page of
topics.

:rtype: tuple, (list, str)
:returns: list of :class:`gcloud.logging.entry.TextEntry`, plus a
"next page token" string: if not None, indicates that
more topics can be retrieved with another call (pass that
value as ``page_token``).
"""
if projects is None:
projects = [self.project]

params = {'projectIds': projects}

if filter_ is not None:
params['filter'] = filter_

if order_by is not None:
params['orderBy'] = order_by

if page_size is not None:
params['pageSize'] = page_size

if page_token is not None:
params['pageToken'] = page_token

resp = self.connection.api_request(method='POST', path='/entries:list',
data=params)
loggers = {}

This comment was marked as spam.

This comment was marked as spam.

entries = [self._entry_from_resource(resource, loggers)
for resource in resp.get('entries', ())]
return entries, resp.get('nextPageToken')
93 changes: 93 additions & 0 deletions gcloud/logging/entries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Log entries within the Google Cloud Logging API."""

from gcloud._helpers import _rfc3339_nanos_to_datetime
from gcloud.logging._helpers import logger_name_from_path


class _BaseEntry(object):
"""Base class for TextEntry, StructEntry.

:type payload: text or dict
:param payload: The payload passed as ``textPayload``, ``jsonPayload``,
or ``protoPayload``.

:type logger: :class:`gcloud.logging.logger.Logger`
:param logger: the logger used to write the entry.

:type insert_id: text, or :class:`NoneType`
:param insert_id: (optional) the ID used to identify an entry uniquely.

:type timestamp: :class:`datetime.datetime`, or :class:`NoneType`
:param timestamp: (optional) timestamp for the entry
"""
def __init__(self, payload, logger, insert_id=None, timestamp=None):
self.payload = payload
self.logger = logger
self.insert_id = insert_id
self.timestamp = timestamp

@classmethod
def from_api_repr(cls, resource, client, loggers=None):
"""Factory: construct an entry given its API representation

:type resource: dict
:param resource: text entry resource representation returned from
the API

:type client: :class:`gcloud.logging.client.Client`
:param client: Client which holds credentials and project
configuration.

:type loggers: dict or None
:param loggers: A mapping of logger fullnames -> loggers. If not
passed, the entry will have a newly-created logger.

:rtype: :class:`gcloud.logging.entries.TextEntry`
:returns: Text entry parsed from ``resource``.
"""
if loggers is None:
loggers = {}
logger_fullname = resource['logName']
logger = loggers.get(logger_fullname)
if logger is None:
logger_name = logger_name_from_path(
logger_fullname, client.project)
logger = loggers[logger_fullname] = client.logger(logger_name)
payload = resource[cls._PAYLOAD_KEY]
insert_id = resource.get('insertId')
timestamp = resource.get('timestamp')
if timestamp is not None:
timestamp = _rfc3339_nanos_to_datetime(timestamp)
return cls(payload, logger, insert_id, timestamp)


class TextEntry(_BaseEntry):
"""Entry created via a write request with ``textPayload``.

See:
https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/LogEntry
"""
_PAYLOAD_KEY = 'textPayload'


class StructEntry(_BaseEntry):
"""Entry created via a write request with ``jsonPayload``.

See:
https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/LogEntry
"""
_PAYLOAD_KEY = 'jsonPayload'
36 changes: 36 additions & 0 deletions gcloud/logging/test__helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import unittest2


class Test_logger_name_from_path(unittest2.TestCase):

def _callFUT(self, path, project):
from gcloud.logging._helpers import logger_name_from_path
return logger_name_from_path(path, project)

def test_w_simple_name(self):
LOGGER_NAME = 'LOGGER_NAME'
PROJECT = 'my-project-1234'
PATH = 'projects/%s/logs/%s' % (PROJECT, LOGGER_NAME)
logger_name = self._callFUT(PATH, PROJECT)
self.assertEqual(logger_name, LOGGER_NAME)

def test_w_name_w_all_extras(self):
LOGGER_NAME = 'LOGGER_NAME-part.one~part.two%part-three'
PROJECT = 'my-project-1234'
PATH = 'projects/%s/logs/%s' % (PROJECT, LOGGER_NAME)
logger_name = self._callFUT(PATH, PROJECT)
self.assertEqual(logger_name, LOGGER_NAME)
Loading