diff --git a/docs/index.rst b/docs/index.rst index e2edfb05c7e8..4770b33da657 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -114,6 +114,7 @@ Client logging-logger logging-entries + logging-sink .. toctree:: :maxdepth: 0 diff --git a/docs/logging-sink.rst b/docs/logging-sink.rst new file mode 100644 index 000000000000..bbfb62130f27 --- /dev/null +++ b/docs/logging-sink.rst @@ -0,0 +1,7 @@ +Sinks +===== + +.. automodule:: gcloud.logging.sink + :members: + :undoc-members: + :show-inheritance: diff --git a/gcloud/logging/client.py b/gcloud/logging/client.py index 7f053eeeb775..ba011e831f6d 100644 --- a/gcloud/logging/client.py +++ b/gcloud/logging/client.py @@ -20,6 +20,7 @@ from gcloud.logging.entries import StructEntry from gcloud.logging.entries import TextEntry from gcloud.logging.logger import Logger +from gcloud.logging.sink import Sink class Client(JSONClient): @@ -134,3 +135,22 @@ def list_entries(self, projects=None, filter_=None, order_by=None, entries = [self._entry_from_resource(resource, loggers) for resource in resp.get('entries', ())] return entries, resp.get('nextPageToken') + + def sink(self, name, filter_, destination): + """Creates a sink bound to the current client. + + :type name: string + :param name: the name of the sink to be constructed. + + :type filter_: string + :param filter_: the advanced logs filter expression defining the + entries exported by the sink. + + :type destination: string + :param destination: destination URI for the entries exported by + the sink. + + :rtype: :class:`gcloud.pubsub.sink.Sink` + :returns: Sink created with the current client. + """ + return Sink(name, filter_, destination, client=self) diff --git a/gcloud/logging/sink.py b/gcloud/logging/sink.py new file mode 100644 index 000000000000..a5c0e614f135 --- /dev/null +++ b/gcloud/logging/sink.py @@ -0,0 +1,92 @@ +# 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. + +"""Define Logging API Sinks.""" + + +class Sink(object): + """Sinks represent filtered exports for log entries. + + See: + https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.sinks + + :type name: string + :param name: the name of the sink + + :type filter_: string + :param filter_: the advanced logs filter expression defining the entries + exported by the sink. + + :type destination: string + :param destination: destination URI for the entries exported by the sink. + + :type client: :class:`gcloud.logging.client.Client` + :param client: A client which holds credentials and project configuration + for the sink (which requires a project). + """ + def __init__(self, name, filter_, destination, client): + self.name = name + self.filter_ = filter_ + self.destination = destination + self._client = client + + @property + def client(self): + """Clent bound to the sink.""" + return self._client + + @property + def project(self): + """Project bound to the sink.""" + return self._client.project + + @property + def full_name(self): + """Fully-qualified name used in sink APIs""" + return 'projects/%s/sinks/%s' % (self.project, self.name) + + @property + def path(self): + """URL path for the sink's APIs""" + return '/%s' % (self.full_name) + + def _require_client(self, client): + """Check client or verify over-ride. + :type client: :class:`gcloud.logging.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current sink. + :rtype: :class:`gcloud.logging.client.Client` + :returns: The client passed in or the currently bound client. + """ + if client is None: + client = self._client + return client + + def create(self, client=None): + """API call: create the sink via a PUT request + + See: + https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.sinks/create + + :type client: :class:`gcloud.logging.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current sink. + """ + client = self._require_client(client) + data = { + 'name': self.name, + 'filter': self.filter_, + 'destination': self.destination, + } + client.connection.api_request(method='PUT', path=self.path, data=data) diff --git a/gcloud/logging/test_client.py b/gcloud/logging/test_client.py index 11a6935fd1b2..a56640cc9e1c 100644 --- a/gcloud/logging/test_client.py +++ b/gcloud/logging/test_client.py @@ -19,6 +19,9 @@ class TestClient(unittest2.TestCase): PROJECT = 'PROJECT' LOGGER_NAME = 'LOGGER_NAME' + SINK_NAME = 'SINK_NAME' + FILTER = 'logName:syslog AND severity>=ERROR' + DESTINATION_URI = 'faux.googleapis.com/destination' def _getTargetClass(self): from gcloud.logging.client import Client @@ -33,9 +36,11 @@ def test_ctor(self): self.assertEqual(client.project, self.PROJECT) def test_logger(self): + from gcloud.logging.logger import Logger creds = _Credentials() client = self._makeOne(project=self.PROJECT, credentials=creds) logger = client.logger(self.LOGGER_NAME) + self.assertTrue(isinstance(logger, Logger)) self.assertEqual(logger.name, self.LOGGER_NAME) self.assertTrue(logger.client is client) self.assertEqual(logger.project, self.PROJECT) @@ -100,6 +105,7 @@ def test_list_entries_explicit(self): from gcloud._helpers import UTC from gcloud.logging import DESCENDING from gcloud.logging.entries import StructEntry + from gcloud.logging.logger import Logger from gcloud.logging.test_entries import _datetime_to_rfc3339_w_nanos PROJECT1 = 'PROJECT1' PROJECT2 = 'PROJECT2' @@ -142,6 +148,7 @@ def test_list_entries_explicit(self): self.assertEqual(entry.payload, PAYLOAD) self.assertEqual(entry.timestamp, NOW) logger = entry.logger + self.assertTrue(isinstance(logger, Logger)) self.assertEqual(logger.name, self.LOGGER_NAME) self.assertTrue(logger.client is client) self.assertEqual(logger.project, self.PROJECT) @@ -152,6 +159,18 @@ def test_list_entries_explicit(self): self.assertEqual(req['path'], '/entries:list') self.assertEqual(req['data'], SENT) + def test_sink(self): + from gcloud.logging.sink import Sink + creds = _Credentials() + client = self._makeOne(project=self.PROJECT, credentials=creds) + sink = client.sink(self.SINK_NAME, self.FILTER, self.DESTINATION_URI) + self.assertTrue(isinstance(sink, Sink)) + self.assertEqual(sink.name, self.SINK_NAME) + self.assertEqual(sink.filter_, self.FILTER) + self.assertEqual(sink.destination, self.DESTINATION_URI) + self.assertTrue(sink.client is client) + self.assertEqual(sink.project, self.PROJECT) + class _Credentials(object): diff --git a/gcloud/logging/test_sink.py b/gcloud/logging/test_sink.py new file mode 100644 index 000000000000..67d99bcf8aa2 --- /dev/null +++ b/gcloud/logging/test_sink.py @@ -0,0 +1,108 @@ +# 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 TestSink(unittest2.TestCase): + + PROJECT = 'test-project' + SINK_NAME = 'sink-name' + FILTER = 'logName:syslog AND severity>=INFO' + DESTINATION_URI = 'faux.googleapis.com/destination' + + def _getTargetClass(self): + from gcloud.logging.sink import Sink + return Sink + + def _makeOne(self, *args, **kw): + return self._getTargetClass()(*args, **kw) + + def test_ctor(self): + FULL = 'projects/%s/sinks/%s' % (self.PROJECT, self.SINK_NAME) + conn = _Connection() + client = _Client(self.PROJECT, conn) + sink = self._makeOne(self.SINK_NAME, self.FILTER, self.DESTINATION_URI, + client=client) + self.assertEqual(sink.name, self.SINK_NAME) + self.assertEqual(sink.filter_, self.FILTER) + self.assertEqual(sink.destination, self.DESTINATION_URI) + self.assertTrue(sink.client is client) + self.assertEqual(sink.project, self.PROJECT) + self.assertEqual(sink.full_name, FULL) + self.assertEqual(sink.path, '/%s' % (FULL,)) + + def test_create_w_bound_client(self): + FULL = 'projects/%s/sinks/%s' % (self.PROJECT, self.SINK_NAME) + RESOURCE = { + 'name': self.SINK_NAME, + 'filter': self.FILTER, + 'destination': self.DESTINATION_URI, + } + conn = _Connection({'name': FULL}) + client = _Client(project=self.PROJECT, connection=conn) + sink = self._makeOne(self.SINK_NAME, self.FILTER, self.DESTINATION_URI, + client=client) + sink.create() + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'PUT') + self.assertEqual(req['path'], '/%s' % FULL) + self.assertEqual(req['data'], RESOURCE) + + def test_create_w_alternate_client(self): + FULL = 'projects/%s/sinks/%s' % (self.PROJECT, self.SINK_NAME) + RESOURCE = { + 'name': self.SINK_NAME, + 'filter': self.FILTER, + 'destination': self.DESTINATION_URI, + } + conn1 = _Connection({'name': FULL}) + client1 = _Client(project=self.PROJECT, connection=conn1) + conn2 = _Connection({'name': FULL}) + client2 = _Client(project=self.PROJECT, connection=conn2) + sink = self._makeOne(self.SINK_NAME, self.FILTER, self.DESTINATION_URI, + client=client1) + sink.create(client=client2) + self.assertEqual(len(conn1._requested), 0) + self.assertEqual(len(conn2._requested), 1) + req = conn2._requested[0] + self.assertEqual(req['method'], 'PUT') + self.assertEqual(req['path'], '/%s' % FULL) + self.assertEqual(req['data'], RESOURCE) + + +class _Connection(object): + + def __init__(self, *responses): + self._responses = responses + self._requested = [] + + def api_request(self, **kw): + from gcloud.exceptions import NotFound + self._requested.append(kw) + + try: + response, self._responses = self._responses[0], self._responses[1:] + except: # pragma: NO COVER + raise NotFound('miss') + else: + return response + + +class _Client(object): + + def __init__(self, project, connection=None): + self.project = project + self.connection = connection