diff --git a/gcloud/logging/client.py b/gcloud/logging/client.py index 1713d8d47538..0344dde22555 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.metric import Metric from gcloud.logging.sink import Sink @@ -190,3 +191,57 @@ def list_sinks(self, page_size=None, page_token=None): sinks = [Sink.from_api_repr(resource, self) for resource in resp.get('sinks', ())] return sinks, resp.get('nextPageToken') + + def metric(self, name, filter_, description=''): + """Creates a metric bound to the current client. + + :type name: string + :param name: the name of the metric to be constructed. + + :type filter_: string + :param filter_: the advanced logs filter expression defining the + entries tracked by the metric. + + :type description: string + :param description: the description of the metric to be constructed. + + :rtype: :class:`gcloud.pubsub.metric.Metric` + :returns: Metric created with the current client. + """ + return Metric(name, filter_, client=self, description=description) + + def list_metrics(self, page_size=None, page_token=None): + """List metrics for the project associated with this client. + + See: + https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.metrics/list + + :type page_size: int + :param page_size: maximum number of metrics 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 metrics. If not + passed, the API will return the first page of + metrics. + + :rtype: tuple, (list, str) + :returns: list of :class:`gcloud.logging.metric.Metric`, plus a + "next page token" string: if not None, indicates that + more metrics can be retrieved with another call (pass that + value as ``page_token``). + """ + params = {} + + if page_size is not None: + params['pageSize'] = page_size + + if page_token is not None: + params['pageToken'] = page_token + + path = '/projects/%s/metrics' % (self.project,) + resp = self.connection.api_request(method='GET', path=path, + query_params=params) + metrics = [Metric.from_api_repr(resource, self) + for resource in resp.get('metrics', ())] + return metrics, resp.get('nextPageToken') diff --git a/gcloud/logging/metric.py b/gcloud/logging/metric.py index 3f5c95c3809d..983756f05700 100644 --- a/gcloud/logging/metric.py +++ b/gcloud/logging/metric.py @@ -14,9 +14,39 @@ """Define Logging API Metrics.""" +import re + +from gcloud._helpers import _name_from_project_path from gcloud.exceptions import NotFound +_METRIC_TEMPLATE = re.compile(r""" + projects/ # static prefix + (?P[^/]+) # initial letter, wordchars + hyphen + /metrics/ # static midfix + (?P[^/]+) # initial letter, wordchars + allowed punc +""", re.VERBOSE) + + +def _metric_name_from_path(path, project): + """Validate a metric URI path and get the metric name. + + :type path: string + :param path: URI path for a metric API request. + + :type project: string + :param project: The project associated with the request. It is + included for validation purposes. + + :rtype: string + :returns: Metric 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, _METRIC_TEMPLATE) + + class Metric(object): """Metrics represent named filters for log entries. @@ -63,6 +93,29 @@ def path(self): """URL path for the metric's APIs""" return '/%s' % (self.full_name,) + @classmethod + def from_api_repr(cls, resource, client): + """Factory: construct a metric given its API representation + + :type resource: dict + :param resource: metric resource representation returned from the API + + :type client: :class:`gcloud.pubsub.client.Client` + :param client: Client which holds credentials and project + configuration for the metric. + + :rtype: :class:`gcloud.logging.metric.Metric` + :returns: Metric parsed from ``resource``. + :raises: :class:`ValueError` if ``client`` is not ``None`` and the + project from the resource does not agree with the project + from the client. + """ + metric_name = _metric_name_from_path(resource['name'], client.project) + filter_ = resource['filter'] + description = resource.get('description', '') + return cls(metric_name, filter_, client=client, + description=description) + def _require_client(self, client): """Check client or verify over-ride. diff --git a/gcloud/logging/test_client.py b/gcloud/logging/test_client.py index 94a9997e978e..59bc10a177f9 100644 --- a/gcloud/logging/test_client.py +++ b/gcloud/logging/test_client.py @@ -22,6 +22,9 @@ class TestClient(unittest2.TestCase): SINK_NAME = 'SINK_NAME' FILTER = 'logName:syslog AND severity>=ERROR' DESTINATION_URI = 'faux.googleapis.com/destination' + METRIC_NAME = 'metric_name' + FILTER = 'logName:syslog AND severity>=ERROR' + DESCRIPTION = 'DESCRIPTION' def _getTargetClass(self): from gcloud.logging.client import Client @@ -270,6 +273,113 @@ def test_list_sinks_missing_key(self): self.assertEqual(req['path'], '/projects/%s/sinks' % PROJECT) self.assertEqual(req['query_params'], {}) + def test_metric(self): + from gcloud.logging.metric import Metric + creds = _Credentials() + + client_obj = self._makeOne(project=self.PROJECT, credentials=creds) + metric = client_obj.metric(self.METRIC_NAME, self.FILTER, + description=self.DESCRIPTION) + self.assertTrue(isinstance(metric, Metric)) + self.assertEqual(metric.name, self.METRIC_NAME) + self.assertEqual(metric.filter_, self.FILTER) + self.assertEqual(metric.description, self.DESCRIPTION) + self.assertTrue(metric.client is client_obj) + self.assertEqual(metric.project, self.PROJECT) + + def test_list_metrics_no_paging(self): + from gcloud.logging.metric import Metric + PROJECT = 'PROJECT' + CREDS = _Credentials() + + CLIENT_OBJ = self._makeOne(project=PROJECT, credentials=CREDS) + + METRIC_PATH = 'projects/%s/metrics/%s' % (PROJECT, self.METRIC_NAME) + + RETURNED = { + 'metrics': [{ + 'name': METRIC_PATH, + 'filter': self.FILTER, + 'description': self.DESCRIPTION, + }], + } + # Replace the connection on the client with one of our own. + CLIENT_OBJ.connection = _Connection(RETURNED) + + # Execute request. + metrics, next_page_token = CLIENT_OBJ.list_metrics() + # Test values are correct. + self.assertEqual(len(metrics), 1) + metric = metrics[0] + self.assertTrue(isinstance(metric, Metric)) + self.assertEqual(metric.name, self.METRIC_NAME) + self.assertEqual(metric.filter_, self.FILTER) + self.assertEqual(metric.description, self.DESCRIPTION) + self.assertEqual(next_page_token, None) + self.assertEqual(len(CLIENT_OBJ.connection._requested), 1) + req = CLIENT_OBJ.connection._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/projects/%s/metrics' % PROJECT) + self.assertEqual(req['query_params'], {}) + + def test_list_metrics_with_paging(self): + from gcloud.logging.metric import Metric + PROJECT = 'PROJECT' + CREDS = _Credentials() + + CLIENT_OBJ = self._makeOne(project=PROJECT, credentials=CREDS) + + METRIC_PATH = 'projects/%s/metrics/%s' % (PROJECT, self.METRIC_NAME) + TOKEN1 = 'TOKEN1' + TOKEN2 = 'TOKEN2' + SIZE = 1 + RETURNED = { + 'metrics': [{ + 'name': METRIC_PATH, + 'filter': self.FILTER, + 'description': self.DESCRIPTION, + }], + 'nextPageToken': TOKEN2, + } + # Replace the connection on the client with one of our own. + CLIENT_OBJ.connection = _Connection(RETURNED) + + # Execute request. + metrics, next_page_token = CLIENT_OBJ.list_metrics(SIZE, TOKEN1) + # Test values are correct. + self.assertEqual(len(metrics), 1) + metric = metrics[0] + self.assertTrue(isinstance(metric, Metric)) + self.assertEqual(metric.name, self.METRIC_NAME) + self.assertEqual(metric.filter_, self.FILTER) + self.assertEqual(metric.description, self.DESCRIPTION) + self.assertEqual(next_page_token, TOKEN2) + req = CLIENT_OBJ.connection._requested[0] + self.assertEqual(req['path'], '/projects/%s/metrics' % PROJECT) + self.assertEqual(req['query_params'], + {'pageSize': SIZE, 'pageToken': TOKEN1}) + + def test_list_metrics_missing_key(self): + PROJECT = 'PROJECT' + CREDS = _Credentials() + + CLIENT_OBJ = self._makeOne(project=PROJECT, credentials=CREDS) + + RETURNED = {} + # Replace the connection on the client with one of our own. + CLIENT_OBJ.connection = _Connection(RETURNED) + + # Execute request. + metrics, next_page_token = CLIENT_OBJ.list_metrics() + # Test values are correct. + self.assertEqual(len(metrics), 0) + self.assertEqual(next_page_token, None) + self.assertEqual(len(CLIENT_OBJ.connection._requested), 1) + req = CLIENT_OBJ.connection._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/projects/%s/metrics' % PROJECT) + self.assertEqual(req['query_params'], {}) + class _Credentials(object): diff --git a/gcloud/logging/test_metric.py b/gcloud/logging/test_metric.py index 43f5b1efb2ec..6c28fad3443c 100644 --- a/gcloud/logging/test_metric.py +++ b/gcloud/logging/test_metric.py @@ -15,6 +15,38 @@ import unittest2 +class Test__metric_name_from_path(unittest2.TestCase): + + def _callFUT(self, path, project): + from gcloud.logging.metric import _metric_name_from_path + return _metric_name_from_path(path, project) + + def test_invalid_path_length(self): + PATH = 'projects/foo' + PROJECT = None + self.assertRaises(ValueError, self._callFUT, PATH, PROJECT) + + def test_invalid_path_format(self): + METRIC_NAME = 'METRIC_NAME' + PROJECT = 'PROJECT' + PATH = 'foo/%s/bar/%s' % (PROJECT, METRIC_NAME) + self.assertRaises(ValueError, self._callFUT, PATH, PROJECT) + + def test_invalid_project(self): + METRIC_NAME = 'METRIC_NAME' + PROJECT1 = 'PROJECT1' + PROJECT2 = 'PROJECT2' + PATH = 'projects/%s/metrics/%s' % (PROJECT1, METRIC_NAME) + self.assertRaises(ValueError, self._callFUT, PATH, PROJECT2) + + def test_valid_data(self): + METRIC_NAME = 'METRIC_NAME' + PROJECT = 'PROJECT' + PATH = 'projects/%s/metrics/%s' % (PROJECT, METRIC_NAME) + metric_name = self._callFUT(PATH, PROJECT) + self.assertEqual(metric_name, METRIC_NAME) + + class TestMetric(unittest2.TestCase): PROJECT = 'test-project' @@ -56,6 +88,50 @@ def test_ctor_explicit(self): self.assertEqual(metric.full_name, FULL) self.assertEqual(metric.path, '/%s' % (FULL,)) + def test_from_api_repr_minimal(self): + CLIENT = _Client(project=self.PROJECT) + FULL = 'projects/%s/metrics/%s' % (self.PROJECT, self.METRIC_NAME) + RESOURCE = { + 'name': FULL, + 'filter': self.FILTER, + } + klass = self._getTargetClass() + metric = klass.from_api_repr(RESOURCE, client=CLIENT) + self.assertEqual(metric.name, self.METRIC_NAME) + self.assertEqual(metric.filter_, self.FILTER) + self.assertEqual(metric.description, '') + self.assertTrue(metric._client is CLIENT) + self.assertEqual(metric.project, self.PROJECT) + self.assertEqual(metric.full_name, FULL) + + def test_from_api_repr_w_description(self): + CLIENT = _Client(project=self.PROJECT) + FULL = 'projects/%s/metrics/%s' % (self.PROJECT, self.METRIC_NAME) + DESCRIPTION = 'DESCRIPTION' + RESOURCE = { + 'name': FULL, + 'filter': self.FILTER, + 'description': DESCRIPTION, + } + klass = self._getTargetClass() + metric = klass.from_api_repr(RESOURCE, client=CLIENT) + self.assertEqual(metric.name, self.METRIC_NAME) + self.assertEqual(metric.filter_, self.FILTER) + self.assertEqual(metric.description, DESCRIPTION) + self.assertTrue(metric._client is CLIENT) + self.assertEqual(metric.project, self.PROJECT) + self.assertEqual(metric.full_name, FULL) + + def test_from_api_repr_with_mismatched_project(self): + PROJECT1 = 'PROJECT1' + PROJECT2 = 'PROJECT2' + CLIENT = _Client(project=PROJECT1) + FULL = 'projects/%s/metrics/%s' % (PROJECT2, self.METRIC_NAME) + RESOURCE = {'name': FULL, 'filter': self.FILTER} + klass = self._getTargetClass() + self.assertRaises(ValueError, klass.from_api_repr, + RESOURCE, client=CLIENT) + def test_create_w_bound_client(self): FULL = 'projects/%s/metrics/%s' % (self.PROJECT, self.METRIC_NAME) RESOURCE = {