Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'Client.list_metrics' API wrapper. #1590

Merged
merged 7 commits into from
Mar 14, 2016
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion 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.metric import Metric
from gcloud.logging.sink import Sink


Expand Down Expand Up @@ -79,7 +80,8 @@ def _entry_from_resource(self, resource, loggers):
raise ValueError('Cannot parse job resource')

def list_entries(self, projects=None, filter_=None, order_by=None,
page_size=None, page_token=None):
page_size=None,
page_token=None):

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

"""Return a page of log entries.

See:
Expand Down Expand Up @@ -112,6 +114,7 @@ def list_entries(self, projects=None, filter_=None, order_by=None,
more topics can be retrieved with another call (pass that
value as ``page_token``).
"""
# pylint: disable=too-many-branches

This comment was marked as spam.

This comment was marked as spam.

if projects is None:
projects = [self.project]

Expand Down Expand Up @@ -190,3 +193,56 @@ 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.logger.Logger`
:returns: Logger created with the current client.

This comment was marked as spam.

This comment was marked as spam.

"""
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

This comment was marked as spam.

This comment was marked as spam.

"""
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')
53 changes: 53 additions & 0 deletions gcloud/logging/metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<project>[^/]+) # initial letter, wordchars + hyphen
/metrics/ # static midfix
(?P<name>[^/]+) # 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.

Expand Down Expand Up @@ -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.

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

Expand Down
76 changes: 76 additions & 0 deletions gcloud/logging/test_metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 = {
Expand Down