diff --git a/gcloud/logging/_gax.py b/gcloud/logging/_gax.py index 15b2f9a8b3d9..88780d92326c 100644 --- a/gcloud/logging/_gax.py +++ b/gcloud/logging/_gax.py @@ -23,6 +23,7 @@ from google.gax.grpc import exc_to_code from google.logging.type.log_severity_pb2 import LogSeverity from google.logging.v2.logging_config_pb2 import LogSink +from google.logging.v2.logging_metrics_pb2 import LogMetric from google.logging.v2.log_entry_pb2 import LogEntry from google.protobuf.json_format import Parse from grpc.beta.interfaces import StatusCode @@ -254,6 +255,140 @@ def sink_delete(self, project, sink_name): raise +class _MetricsAPI(object): + """Helper mapping sink-related APIs. + + :type gax_api: + :class:`google.logging.v2.metrics_service_v2_api.MetricsServiceV2Api` + :param gax_api: API object used to make GAX requests. + """ + def __init__(self, gax_api): + self._gax_api = gax_api + + def list_metrics(self, project, page_size=0, page_token=None): + """List metrics for the project associated with this client. + + :type project: string + :param project: ID of the project whose metrics are to be listed. + + :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: str + :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 mappings, 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``). + """ + options = _build_paging_options(page_token) + page_iter = self._gax_api.list_log_metrics(project, page_size, options) + metrics = [_log_metric_pb_to_mapping(log_metric_pb) + for log_metric_pb in page_iter.next()] + token = page_iter.page_token or None + return metrics, token + + def metric_create(self, project, metric_name, filter_, description): + """API call: create a metric resource. + + See: + https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.metrics/create + + :type project: string + :param project: ID of the project in which to create the metric. + + :type metric_name: string + :param metric_name: the name of the metric + + :type filter_: string + :param filter_: the advanced logs filter expression defining the + entries exported by the metric. + + :type description: string + :param description: description of the metric. + """ + options = None + parent = 'projects/%s' % (project,) + path = 'projects/%s/metrics/%s' % (project, metric_name) + metric_pb = LogMetric(name=path, filter=filter_, + description=description) + try: + self._gax_api.create_log_metric(parent, metric_pb, options) + except GaxError as exc: + if exc_to_code(exc.cause) == StatusCode.FAILED_PRECONDITION: + raise Conflict(path) + raise + + def metric_get(self, project, metric_name): + """API call: retrieve a metric resource. + + :type project: string + :param project: ID of the project containing the metric. + + :type metric_name: string + :param metric_name: the name of the metric + """ + options = None + path = 'projects/%s/metrics/%s' % (project, metric_name) + try: + metric_pb = self._gax_api.get_log_metric(path, options) + except GaxError as exc: + if exc_to_code(exc.cause) == StatusCode.NOT_FOUND: + raise NotFound(path) + raise + return _log_metric_pb_to_mapping(metric_pb) + + def metric_update(self, project, metric_name, filter_, description): + """API call: update a metric resource. + + :type project: string + :param project: ID of the project containing the metric. + + :type metric_name: string + :param metric_name: the name of the metric + + :type filter_: string + :param filter_: the advanced logs filter expression defining the + entries exported by the metric. + + :type description: string + :param description: description of the metric. + """ + options = None + path = 'projects/%s/metrics/%s' % (project, metric_name) + metric_pb = LogMetric(name=path, filter=filter_, + description=description) + try: + self._gax_api.update_log_metric(path, metric_pb, options) + except GaxError as exc: + if exc_to_code(exc.cause) == StatusCode.NOT_FOUND: + raise NotFound(path) + raise + return _log_metric_pb_to_mapping(metric_pb) + + def metric_delete(self, project, metric_name): + """API call: delete a metric resource. + + :type project: string + :param project: ID of the project containing the metric. + + :type metric_name: string + :param metric_name: the name of the metric + """ + options = None + path = 'projects/%s/metrics/%s' % (project, metric_name) + try: + self._gax_api.delete_log_metric(path, options) + except GaxError as exc: + if exc_to_code(exc.cause) == StatusCode.NOT_FOUND: + raise NotFound(path) + raise + + def _build_paging_options(page_token=None): """Helper for :meth:'_PublisherAPI.list_topics' et aliae.""" if page_token is None: @@ -405,3 +540,17 @@ def _log_sink_pb_to_mapping(sink_pb): 'destination': sink_pb.destination, 'filter': sink_pb.filter, } + + +def _log_metric_pb_to_mapping(metric_pb): + """Helper for :meth:`list_metrics`, et aliae + + Ideally, would use a function from :mod:`protobuf.json_format`, but + the right one isn't public. See: + https://github.com/google/protobuf/issues/1351 + """ + return { + 'name': metric_pb.name, + 'description': metric_pb.description, + 'filter': metric_pb.filter, + } diff --git a/gcloud/logging/test__gax.py b/gcloud/logging/test__gax.py index 825d62299a3e..773384fc431f 100644 --- a/gcloud/logging/test__gax.py +++ b/gcloud/logging/test__gax.py @@ -394,7 +394,6 @@ def test_logger_delete(self): @unittest2.skipUnless(_HAVE_GAX, 'No gax-python') class Test_SinksAPI(_Base, unittest2.TestCase): - LIST_SINKS_PATH = '%s/sinks' % (_Base.PROJECT_PATH,) SINK_NAME = 'sink_name' SINK_PATH = 'projects/%s/sinks/%s' % (_Base.PROJECT, SINK_NAME) DESTINATION_URI = 'faux.googleapis.com/destination' @@ -597,6 +596,210 @@ def test_sink_delete_hit(self): self.assertEqual(options, None) +@unittest2.skipUnless(_HAVE_GAX, 'No gax-python') +class Test_MetricsAPI(_Base, unittest2.TestCase): + METRIC_NAME = 'metric_name' + METRIC_PATH = 'projects/%s/metrics/%s' % (_Base.PROJECT, METRIC_NAME) + DESCRIPTION = 'Description' + + def _getTargetClass(self): + from gcloud.logging._gax import _MetricsAPI + return _MetricsAPI + + def test_ctor(self): + gax_api = _GAXMetricsAPI() + api = self._makeOne(gax_api) + self.assertTrue(api._gax_api is gax_api) + + def test_list_metrics_no_paging(self): + from google.gax import INITIAL_PAGE + from gcloud._testing import _GAXPageIterator + TOKEN = 'TOKEN' + METRICS = [{ + 'name': self.METRIC_PATH, + 'filter': self.FILTER, + 'description': self.DESCRIPTION, + }] + response = _GAXPageIterator( + [_LogMetricPB(self.METRIC_PATH, self.DESCRIPTION, self.FILTER)], + TOKEN) + gax_api = _GAXMetricsAPI(_list_log_metrics_response=response) + api = self._makeOne(gax_api) + + metrics, token = api.list_metrics(self.PROJECT) + + self.assertEqual(metrics, METRICS) + self.assertEqual(token, TOKEN) + + project, page_size, options = gax_api._list_log_metrics_called_with + self.assertEqual(project, self.PROJECT) + self.assertEqual(page_size, 0) + self.assertEqual(options.page_token, INITIAL_PAGE) + + def test_list_metrics_w_paging(self): + from gcloud._testing import _GAXPageIterator + TOKEN = 'TOKEN' + PAGE_SIZE = 42 + METRICS = [{ + 'name': self.METRIC_PATH, + 'filter': self.FILTER, + 'description': self.DESCRIPTION, + }] + response = _GAXPageIterator( + [_LogMetricPB(self.METRIC_PATH, self.DESCRIPTION, self.FILTER)], + None) + gax_api = _GAXMetricsAPI(_list_log_metrics_response=response) + api = self._makeOne(gax_api) + + metrics, token = api.list_metrics( + self.PROJECT, page_size=PAGE_SIZE, page_token=TOKEN) + + self.assertEqual(metrics, METRICS) + self.assertEqual(token, None) + + project, page_size, options = gax_api._list_log_metrics_called_with + self.assertEqual(project, self.PROJECT) + self.assertEqual(page_size, PAGE_SIZE) + self.assertEqual(options.page_token, TOKEN) + + def test_metric_create_error(self): + from google.gax.errors import GaxError + gax_api = _GAXMetricsAPI(_random_gax_error=True) + api = self._makeOne(gax_api) + + with self.assertRaises(GaxError): + api.metric_create( + self.PROJECT, self.METRIC_NAME, self.FILTER, + self.DESCRIPTION) + + def test_metric_create_conflict(self): + from gcloud.exceptions import Conflict + gax_api = _GAXMetricsAPI(_create_log_metric_conflict=True) + api = self._makeOne(gax_api) + + with self.assertRaises(Conflict): + api.metric_create( + self.PROJECT, self.METRIC_NAME, self.FILTER, + self.DESCRIPTION) + + def test_metric_create_ok(self): + from google.logging.v2.logging_metrics_pb2 import LogMetric + gax_api = _GAXMetricsAPI() + api = self._makeOne(gax_api) + + api.metric_create( + self.PROJECT, self.METRIC_NAME, self.FILTER, self.DESCRIPTION) + + parent, metric, options = ( + gax_api._create_log_metric_called_with) + self.assertEqual(parent, self.PROJECT_PATH) + self.assertTrue(isinstance(metric, LogMetric)) + self.assertEqual(metric.name, self.METRIC_PATH) + self.assertEqual(metric.filter, self.FILTER) + self.assertEqual(metric.description, self.DESCRIPTION) + self.assertEqual(options, None) + + def test_metric_get_error(self): + from gcloud.exceptions import NotFound + gax_api = _GAXMetricsAPI() + api = self._makeOne(gax_api) + + with self.assertRaises(NotFound): + api.metric_get(self.PROJECT, self.METRIC_NAME) + + def test_metric_get_miss(self): + from google.gax.errors import GaxError + gax_api = _GAXMetricsAPI(_random_gax_error=True) + api = self._makeOne(gax_api) + + with self.assertRaises(GaxError): + api.metric_get(self.PROJECT, self.METRIC_NAME) + + def test_metric_get_hit(self): + RESPONSE = { + 'name': self.METRIC_PATH, + 'filter': self.FILTER, + 'description': self.DESCRIPTION, + } + metric_pb = _LogMetricPB( + self.METRIC_PATH, self.DESCRIPTION, self.FILTER) + gax_api = _GAXMetricsAPI(_get_log_metric_response=metric_pb) + api = self._makeOne(gax_api) + + response = api.metric_get(self.PROJECT, self.METRIC_NAME) + + self.assertEqual(response, RESPONSE) + + metric_name, options = gax_api._get_log_metric_called_with + self.assertEqual(metric_name, self.METRIC_PATH) + self.assertEqual(options, None) + + def test_metric_update_error(self): + from google.gax.errors import GaxError + gax_api = _GAXMetricsAPI(_random_gax_error=True) + api = self._makeOne(gax_api) + + with self.assertRaises(GaxError): + api.metric_update( + self.PROJECT, self.METRIC_NAME, self.FILTER, + self.DESCRIPTION) + + def test_metric_update_miss(self): + from gcloud.exceptions import NotFound + gax_api = _GAXMetricsAPI() + api = self._makeOne(gax_api) + + with self.assertRaises(NotFound): + api.metric_update( + self.PROJECT, self.METRIC_NAME, self.FILTER, + self.DESCRIPTION) + + def test_metric_update_hit(self): + from google.logging.v2.logging_metrics_pb2 import LogMetric + response = _LogMetricPB( + self.METRIC_NAME, self.FILTER, self.DESCRIPTION) + gax_api = _GAXMetricsAPI(_update_log_metric_response=response) + api = self._makeOne(gax_api) + + api.metric_update( + self.PROJECT, self.METRIC_NAME, self.FILTER, self.DESCRIPTION) + + metric_name, metric, options = ( + gax_api._update_log_metric_called_with) + self.assertEqual(metric_name, self.METRIC_PATH) + self.assertTrue(isinstance(metric, LogMetric)) + self.assertEqual(metric.name, self.METRIC_PATH) + self.assertEqual(metric.filter, self.FILTER) + self.assertEqual(metric.description, self.DESCRIPTION) + self.assertEqual(options, None) + + def test_metric_delete_error(self): + from google.gax.errors import GaxError + gax_api = _GAXMetricsAPI(_random_gax_error=True) + api = self._makeOne(gax_api) + + with self.assertRaises(GaxError): + api.metric_delete(self.PROJECT, self.METRIC_NAME) + + def test_metric_delete_miss(self): + from gcloud.exceptions import NotFound + gax_api = _GAXMetricsAPI(_log_metric_not_found=True) + api = self._makeOne(gax_api) + + with self.assertRaises(NotFound): + api.metric_delete(self.PROJECT, self.METRIC_NAME) + + def test_metric_delete_hit(self): + gax_api = _GAXMetricsAPI() + api = self._makeOne(gax_api) + + api.metric_delete(self.PROJECT, self.METRIC_NAME) + + metric_name, options = gax_api._delete_log_metric_called_with + self.assertEqual(metric_name, self.METRIC_PATH) + self.assertEqual(options, None) + + class _GAXBaseAPI(object): _random_gax_error = False @@ -687,6 +890,52 @@ def delete_sink(self, sink_name, options=None): raise GaxError('notfound', self._make_grpc_not_found()) +class _GAXMetricsAPI(_GAXBaseAPI): + + _create_log_metric_conflict = False + _log_metric_not_found = False + + def list_log_metrics(self, parent, page_size, options): + self._list_log_metrics_called_with = parent, page_size, options + return self._list_log_metrics_response + + def create_log_metric(self, parent, metric, options): + from google.gax.errors import GaxError + self._create_log_metric_called_with = parent, metric, options + if self._random_gax_error: + raise GaxError('error') + if self._create_log_metric_conflict: + raise GaxError('conflict', self._make_grpc_failed_precondition()) + + def get_log_metric(self, metric_name, options): + from google.gax.errors import GaxError + self._get_log_metric_called_with = metric_name, options + if self._random_gax_error: + raise GaxError('error') + try: + return self._get_log_metric_response + except AttributeError: + raise GaxError('notfound', self._make_grpc_not_found()) + + def update_log_metric(self, metric_name, metric, options=None): + from google.gax.errors import GaxError + self._update_log_metric_called_with = metric_name, metric, options + if self._random_gax_error: + raise GaxError('error') + try: + return self._update_log_metric_response + except AttributeError: + raise GaxError('notfound', self._make_grpc_not_found()) + + def delete_log_metric(self, metric_name, options=None): + from google.gax.errors import GaxError + self._delete_log_metric_called_with = metric_name, options + if self._random_gax_error: + raise GaxError('error') + if self._log_metric_not_found: + raise GaxError('notfound', self._make_grpc_not_found()) + + class _HTTPRequestPB(object): request_url = 'http://example.com/requested' @@ -735,3 +984,11 @@ def __init__(self, name, destination, filter_): self.name = name self.destination = destination self.filter = filter_ + + +class _LogMetricPB(object): + + def __init__(self, name, description, filter_): + self.name = name + self.description = description + self.filter = filter_