Skip to content

Commit 6f3d1a7

Browse files
committed
Merge pull request #1596 from tseaver/logging-sink_create
Add 'Sink.create' API wrapper and 'Client.sink' factory.
2 parents b8151b1 + a9c8515 commit 6f3d1a7

File tree

6 files changed

+247
-0
lines changed

6 files changed

+247
-0
lines changed

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
Client <logging-client>
115115
logging-logger
116116
logging-entries
117+
logging-sink
117118

118119
.. toctree::
119120
:maxdepth: 0

docs/logging-sink.rst

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

gcloud/logging/client.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from gcloud.logging.entries import StructEntry
2121
from gcloud.logging.entries import TextEntry
2222
from gcloud.logging.logger import Logger
23+
from gcloud.logging.sink import Sink
2324

2425

2526
class Client(JSONClient):
@@ -134,3 +135,22 @@ def list_entries(self, projects=None, filter_=None, order_by=None,
134135
entries = [self._entry_from_resource(resource, loggers)
135136
for resource in resp.get('entries', ())]
136137
return entries, resp.get('nextPageToken')
138+
139+
def sink(self, name, filter_, destination):
140+
"""Creates a sink bound to the current client.
141+
142+
:type name: string
143+
:param name: the name of the sink to be constructed.
144+
145+
:type filter_: string
146+
:param filter_: the advanced logs filter expression defining the
147+
entries exported by the sink.
148+
149+
:type destination: string
150+
:param destination: destination URI for the entries exported by
151+
the sink.
152+
153+
:rtype: :class:`gcloud.pubsub.sink.Sink`
154+
:returns: Sink created with the current client.
155+
"""
156+
return Sink(name, filter_, destination, client=self)

gcloud/logging/sink.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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+
"""Define Logging API Sinks."""
16+
17+
18+
class Sink(object):
19+
"""Sinks represent filtered exports for log entries.
20+
21+
See:
22+
https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.sinks
23+
24+
:type name: string
25+
:param name: the name of the sink
26+
27+
:type filter_: string
28+
:param filter_: the advanced logs filter expression defining the entries
29+
exported by the sink.
30+
31+
:type destination: string
32+
:param destination: destination URI for the entries exported by the sink.
33+
34+
:type client: :class:`gcloud.logging.client.Client`
35+
:param client: A client which holds credentials and project configuration
36+
for the sink (which requires a project).
37+
"""
38+
def __init__(self, name, filter_, destination, client):
39+
self.name = name
40+
self.filter_ = filter_
41+
self.destination = destination
42+
self._client = client
43+
44+
@property
45+
def client(self):
46+
"""Clent bound to the sink."""
47+
return self._client
48+
49+
@property
50+
def project(self):
51+
"""Project bound to the sink."""
52+
return self._client.project
53+
54+
@property
55+
def full_name(self):
56+
"""Fully-qualified name used in sink APIs"""
57+
return 'projects/%s/sinks/%s' % (self.project, self.name)
58+
59+
@property
60+
def path(self):
61+
"""URL path for the sink's APIs"""
62+
return '/%s' % (self.full_name)
63+
64+
def _require_client(self, client):
65+
"""Check client or verify over-ride.
66+
:type client: :class:`gcloud.logging.client.Client` or ``NoneType``
67+
:param client: the client to use. If not passed, falls back to the
68+
``client`` stored on the current sink.
69+
:rtype: :class:`gcloud.logging.client.Client`
70+
:returns: The client passed in or the currently bound client.
71+
"""
72+
if client is None:
73+
client = self._client
74+
return client
75+
76+
def create(self, client=None):
77+
"""API call: create the sink via a PUT request
78+
79+
See:
80+
https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.sinks/create
81+
82+
:type client: :class:`gcloud.logging.client.Client` or ``NoneType``
83+
:param client: the client to use. If not passed, falls back to the
84+
``client`` stored on the current sink.
85+
"""
86+
client = self._require_client(client)
87+
data = {
88+
'name': self.name,
89+
'filter': self.filter_,
90+
'destination': self.destination,
91+
}
92+
client.connection.api_request(method='PUT', path=self.path, data=data)

gcloud/logging/test_client.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ class TestClient(unittest2.TestCase):
1919

2020
PROJECT = 'PROJECT'
2121
LOGGER_NAME = 'LOGGER_NAME'
22+
SINK_NAME = 'SINK_NAME'
23+
FILTER = 'logName:syslog AND severity>=ERROR'
24+
DESTINATION_URI = 'faux.googleapis.com/destination'
2225

2326
def _getTargetClass(self):
2427
from gcloud.logging.client import Client
@@ -33,9 +36,11 @@ def test_ctor(self):
3336
self.assertEqual(client.project, self.PROJECT)
3437

3538
def test_logger(self):
39+
from gcloud.logging.logger import Logger
3640
creds = _Credentials()
3741
client = self._makeOne(project=self.PROJECT, credentials=creds)
3842
logger = client.logger(self.LOGGER_NAME)
43+
self.assertTrue(isinstance(logger, Logger))
3944
self.assertEqual(logger.name, self.LOGGER_NAME)
4045
self.assertTrue(logger.client is client)
4146
self.assertEqual(logger.project, self.PROJECT)
@@ -100,6 +105,7 @@ def test_list_entries_explicit(self):
100105
from gcloud._helpers import UTC
101106
from gcloud.logging import DESCENDING
102107
from gcloud.logging.entries import StructEntry
108+
from gcloud.logging.logger import Logger
103109
from gcloud.logging.test_entries import _datetime_to_rfc3339_w_nanos
104110
PROJECT1 = 'PROJECT1'
105111
PROJECT2 = 'PROJECT2'
@@ -142,6 +148,7 @@ def test_list_entries_explicit(self):
142148
self.assertEqual(entry.payload, PAYLOAD)
143149
self.assertEqual(entry.timestamp, NOW)
144150
logger = entry.logger
151+
self.assertTrue(isinstance(logger, Logger))
145152
self.assertEqual(logger.name, self.LOGGER_NAME)
146153
self.assertTrue(logger.client is client)
147154
self.assertEqual(logger.project, self.PROJECT)
@@ -152,6 +159,18 @@ def test_list_entries_explicit(self):
152159
self.assertEqual(req['path'], '/entries:list')
153160
self.assertEqual(req['data'], SENT)
154161

162+
def test_sink(self):
163+
from gcloud.logging.sink import Sink
164+
creds = _Credentials()
165+
client = self._makeOne(project=self.PROJECT, credentials=creds)
166+
sink = client.sink(self.SINK_NAME, self.FILTER, self.DESTINATION_URI)
167+
self.assertTrue(isinstance(sink, Sink))
168+
self.assertEqual(sink.name, self.SINK_NAME)
169+
self.assertEqual(sink.filter_, self.FILTER)
170+
self.assertEqual(sink.destination, self.DESTINATION_URI)
171+
self.assertTrue(sink.client is client)
172+
self.assertEqual(sink.project, self.PROJECT)
173+
155174

156175
class _Credentials(object):
157176

gcloud/logging/test_sink.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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 TestSink(unittest2.TestCase):
19+
20+
PROJECT = 'test-project'
21+
SINK_NAME = 'sink-name'
22+
FILTER = 'logName:syslog AND severity>=INFO'
23+
DESTINATION_URI = 'faux.googleapis.com/destination'
24+
25+
def _getTargetClass(self):
26+
from gcloud.logging.sink import Sink
27+
return Sink
28+
29+
def _makeOne(self, *args, **kw):
30+
return self._getTargetClass()(*args, **kw)
31+
32+
def test_ctor(self):
33+
FULL = 'projects/%s/sinks/%s' % (self.PROJECT, self.SINK_NAME)
34+
conn = _Connection()
35+
client = _Client(self.PROJECT, conn)
36+
sink = self._makeOne(self.SINK_NAME, self.FILTER, self.DESTINATION_URI,
37+
client=client)
38+
self.assertEqual(sink.name, self.SINK_NAME)
39+
self.assertEqual(sink.filter_, self.FILTER)
40+
self.assertEqual(sink.destination, self.DESTINATION_URI)
41+
self.assertTrue(sink.client is client)
42+
self.assertEqual(sink.project, self.PROJECT)
43+
self.assertEqual(sink.full_name, FULL)
44+
self.assertEqual(sink.path, '/%s' % (FULL,))
45+
46+
def test_create_w_bound_client(self):
47+
FULL = 'projects/%s/sinks/%s' % (self.PROJECT, self.SINK_NAME)
48+
RESOURCE = {
49+
'name': self.SINK_NAME,
50+
'filter': self.FILTER,
51+
'destination': self.DESTINATION_URI,
52+
}
53+
conn = _Connection({'name': FULL})
54+
client = _Client(project=self.PROJECT, connection=conn)
55+
sink = self._makeOne(self.SINK_NAME, self.FILTER, self.DESTINATION_URI,
56+
client=client)
57+
sink.create()
58+
self.assertEqual(len(conn._requested), 1)
59+
req = conn._requested[0]
60+
self.assertEqual(req['method'], 'PUT')
61+
self.assertEqual(req['path'], '/%s' % FULL)
62+
self.assertEqual(req['data'], RESOURCE)
63+
64+
def test_create_w_alternate_client(self):
65+
FULL = 'projects/%s/sinks/%s' % (self.PROJECT, self.SINK_NAME)
66+
RESOURCE = {
67+
'name': self.SINK_NAME,
68+
'filter': self.FILTER,
69+
'destination': self.DESTINATION_URI,
70+
}
71+
conn1 = _Connection({'name': FULL})
72+
client1 = _Client(project=self.PROJECT, connection=conn1)
73+
conn2 = _Connection({'name': FULL})
74+
client2 = _Client(project=self.PROJECT, connection=conn2)
75+
sink = self._makeOne(self.SINK_NAME, self.FILTER, self.DESTINATION_URI,
76+
client=client1)
77+
sink.create(client=client2)
78+
self.assertEqual(len(conn1._requested), 0)
79+
self.assertEqual(len(conn2._requested), 1)
80+
req = conn2._requested[0]
81+
self.assertEqual(req['method'], 'PUT')
82+
self.assertEqual(req['path'], '/%s' % FULL)
83+
self.assertEqual(req['data'], RESOURCE)
84+
85+
86+
class _Connection(object):
87+
88+
def __init__(self, *responses):
89+
self._responses = responses
90+
self._requested = []
91+
92+
def api_request(self, **kw):
93+
from gcloud.exceptions import NotFound
94+
self._requested.append(kw)
95+
96+
try:
97+
response, self._responses = self._responses[0], self._responses[1:]
98+
except: # pragma: NO COVER
99+
raise NotFound('miss')
100+
else:
101+
return response
102+
103+
104+
class _Client(object):
105+
106+
def __init__(self, project, connection=None):
107+
self.project = project
108+
self.connection = connection

0 commit comments

Comments
 (0)