Skip to content

Commit

Permalink
Merge pull request #1596 from tseaver/logging-sink_create
Browse files Browse the repository at this point in the history
Add 'Sink.create' API wrapper and 'Client.sink' factory.
  • Loading branch information
tseaver committed Mar 11, 2016
2 parents b8151b1 + a9c8515 commit 6f3d1a7
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
Client <logging-client>
logging-logger
logging-entries
logging-sink

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

.. automodule:: gcloud.logging.sink
:members:
:undoc-members:
:show-inheritance:
20 changes: 20 additions & 0 deletions gcloud/logging/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
92 changes: 92 additions & 0 deletions gcloud/logging/sink.py
Original file line number Diff line number Diff line change
@@ -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)
19 changes: 19 additions & 0 deletions gcloud/logging/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand All @@ -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):

Expand Down
108 changes: 108 additions & 0 deletions gcloud/logging/test_sink.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 6f3d1a7

Please sign in to comment.