Skip to content

Commit

Permalink
Merge pull request #1590 from tseaver/logging-client_list_metrics
Browse files Browse the repository at this point in the history
Add 'Client.list_metrics' API wrapper.
  • Loading branch information
tseaver committed Mar 14, 2016
2 parents 92a7bb4 + f1ca149 commit 5648192
Show file tree
Hide file tree
Showing 4 changed files with 294 additions and 0 deletions.
55 changes: 55 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.metric import Metric
from gcloud.logging.sink import Sink


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

0 comments on commit 5648192

Please sign in to comment.