From 6a643c07f869175179d163ea590eae1a8ea2552a Mon Sep 17 00:00:00 2001 From: Bill Prin Date: Thu, 28 Jul 2016 09:43:14 -0700 Subject: [PATCH 01/11] Monitoring Custom Metric Usage --- docs/monitoring-usage.rst | 87 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/docs/monitoring-usage.rst b/docs/monitoring-usage.rst index 2b993de31448..13a7d10650af 100644 --- a/docs/monitoring-usage.rst +++ b/docs/monitoring-usage.rst @@ -278,3 +278,90 @@ follows:: .. _Time Series: https://cloud.google.com/monitoring/api/ref_v3/rest/v3/TimeSeries + + +Writing Custom Metrics +--------------------------- + +The Stackdriver Monitoring API can be used to write data points to custom metrics. Please refer to +the documentation on `Custom Metrics`_ for more information. + +To write a data point to a custom metric, you must provide an instance of +:class:`~gcloud.monitoring.metric.Metric` specifying the metric type as well as the values for +the metric labels. You will need to have either created the metric descriptor earlier (see the +`Metric Descriptors`_ section) or rely on metric type auto-creation (see `Auto-creation of +custom metrics`_). + +You will also need to provide a :class:`~gcloud.monitoring.resource.Resource` instance specifying a +monitored resource type as well as values for all of the monitored resource labels, except for +``project_id``, which is ignored when it's included in writes to the API. A good choice is to use +the underlying physical resource where your application code runs – e.g., a monitored resource +type of ``gce_instance`` or ``aws_ec2_instance``. In some limited circumstances, such as when +only a single process writes to the custom metric, you may choose to use the ``global`` monitored +resource type. + +See `Monitored resource types`_ for a list of all monitored resource types available in +Stackdriver Monitoring. + +>>> from gcloud import monitoring +>>> # Create a Resource object for the desired monitored resource type. +>>> resource = client.resource('gce_instance', labels={ +... 'instance_id': '1234567890123456789', +... 'zone': 'us-central1-f' +... }) +>>> # Create a Metric object, specifying the metric type as well as values for any metric labels. +>>> metric = client.metric(type='custom.googleapis.com/my_metric', labels={ +... 'status': 'successful' +... }) + +Please refer to the `Metrics`_ documentation for more information. + +With a ``Metric`` and ``Resource`` in hand, the :class:`~gcloud.monitoring.client.Client` +can be used to write :class:`~gcloud.monitoring.timeseries.Point` values. + +When writing points, the Python type of the value must match the *value type* of the metric +descriptor associated with the metric. For example, a Python float will map to ``ValueType.DOUBLE``. + +Stackdriver Monitoring supports several *metric kinds*: ``GAUGE``, ``CUMULATIVE``, and ``DELTA``. +However, ``DELTA`` custom metrics are not supported. + +``GAUGE`` metrics represent only a single point in time, so only the ``end_time`` should be +specified:: + + >>> client.write_point(metric=metric, resource=resource, 3.14, end_time=end) # API call + +By default, ``end_time`` defaults to :meth:`~datetime.datetime.utcnow()`, so metrics can be written +to the current time as follows:: + + >>> client.write_point(metric, resource, 3.14) # API call + +``CUMULATIVE`` metrics enable the monitoring system to compute rates of increase on metrics that +sometimes reset, such as after a process restart. Without cumulative metrics, this +reset would otherwise show up as a huge negative spike. For cumulative metrics, the same start +time should be re-used repeatedly as more points are written to the time series. + +In the examples below, the ``end_time`` again defaults to the current time:: + + >>> RESET = datetime.utcnow() + >>> client.write_point(metric, resource, 3, start_time=RESET) # API call + >>> client.write_point(metric, resource, 6, start_time=RESET) # API call + +To write multiple ``TimeSeries`` in a single batch, you can use +:meth:`~gcloud.monitoring.client.write_time_series`:: + + >>> ts1 = client.time_series(metric1, resource, 3.14, end_time=end_time) + >>> ts2 = client.time_series(metric2, resource, 42, end_time=end_time) + >>> client.write_time_series([ts1, ts2]) # API call + +While multiple time series can be written in a single batch, each ``TimeSeries`` object sent to +the API must only include a single point. + +All timezone-naive Python ``datetime`` objects are assumed to be UTC. + +.. _TimeSeries: https://cloud.google.com/monitoring/api/ref_v3/rest/v3/TimeSeries +.. _Custom Metrics: https://cloud.google.com/monitoring/custom-metrics/ +.. _Auto-creation of custom metrics: + https://cloud.google.com/monitoring/custom-metrics/creating-metrics#auto-creation +.. _Metrics: https://cloud.google.com/monitoring/api/v3/metrics +.. _Monitored resource types: + https://cloud.google.com/monitoring/api/resources From 83ce108464b7be2ba80362074d1a0700b967fe52 Mon Sep 17 00:00:00 2001 From: Bill Prin Date: Fri, 26 Aug 2016 06:44:38 -0700 Subject: [PATCH 02/11] Add write_point and write_time_series --- docs/monitoring-usage.rst | 9 +-- google/cloud/monitoring/client.py | 90 ++++++++++++++++++++++- google/cloud/monitoring/metric.py | 13 ++++ google/cloud/monitoring/resource.py | 13 ++++ google/cloud/monitoring/timeseries.py | 73 +++++++++++++++++++ system_tests/monitoring.py | 48 +++++++++++- unit_tests/monitoring/test_client.py | 93 ++++++++++++++++++++++-- unit_tests/monitoring/test_metric.py | 10 +++ unit_tests/monitoring/test_resource.py | 13 ++++ unit_tests/monitoring/test_timeseries.py | 71 +++++++++++++++++- 10 files changed, 415 insertions(+), 18 deletions(-) diff --git a/docs/monitoring-usage.rst b/docs/monitoring-usage.rst index 13a7d10650af..79218c37b3ec 100644 --- a/docs/monitoring-usage.rst +++ b/docs/monitoring-usage.rst @@ -30,7 +30,7 @@ of the API: - Querying of time series. - Querying of metric descriptors and monitored resource descriptors. - Creation and deletion of metric descriptors for custom metrics. -- (Writing of custom metric data will be coming soon.) +- Writing of custom metric data. .. _Stackdriver Monitoring API: https://cloud.google.com/monitoring/api/v3/ @@ -300,8 +300,7 @@ type of ``gce_instance`` or ``aws_ec2_instance``. In some limited circumstances, only a single process writes to the custom metric, you may choose to use the ``global`` monitored resource type. -See `Monitored resource types`_ for a list of all monitored resource types available in -Stackdriver Monitoring. +See `Monitored resource types`_ for more information about parcitular monitored resource types. >>> from gcloud import monitoring >>> # Create a Resource object for the desired monitored resource type. @@ -314,8 +313,6 @@ Stackdriver Monitoring. ... 'status': 'successful' ... }) -Please refer to the `Metrics`_ documentation for more information. - With a ``Metric`` and ``Resource`` in hand, the :class:`~gcloud.monitoring.client.Client` can be used to write :class:`~gcloud.monitoring.timeseries.Point` values. @@ -323,7 +320,7 @@ When writing points, the Python type of the value must match the *value type* of descriptor associated with the metric. For example, a Python float will map to ``ValueType.DOUBLE``. Stackdriver Monitoring supports several *metric kinds*: ``GAUGE``, ``CUMULATIVE``, and ``DELTA``. -However, ``DELTA`` custom metrics are not supported. +However, ``DELTA`` is not supported for custom metrics. ``GAUGE`` metrics represent only a single point in time, so only the ``end_time`` should be specified:: diff --git a/google/cloud/monitoring/client.py b/google/cloud/monitoring/client.py index 44a7715000a2..e8fdc857f565 100644 --- a/google/cloud/monitoring/client.py +++ b/google/cloud/monitoring/client.py @@ -30,6 +30,7 @@ import datetime +from google.cloud._helpers import _datetime_to_rfc3339 from google.cloud.client import JSONClient from google.cloud.monitoring.connection import Connection from google.cloud.monitoring.group import Group @@ -312,7 +313,7 @@ def time_series(metric, resource, value, :type start_time: :class:`~datetime.datetime` :param start_time: The start time for the point to be included in the time series. - Assumed to be UTC if no time zone information is present + Assumed to be UTC if no time zone information is present. Defaults to None. If the start time is unspecified, the API interprets the start time to be the same as the end time. @@ -321,6 +322,11 @@ def time_series(metric, resource, value, """ if end_time is None: end_time = _UTCNOW() + + end_time = _datetime_to_rfc3339(end_time, ignore_zone=False) + if start_time: + start_time = _datetime_to_rfc3339(start_time, ignore_zone=False) + point = Point(value=value, start_time=start_time, end_time=end_time) return TimeSeries(metric=metric, resource=resource, metric_kind=None, value_type=None, points=[point]) @@ -495,3 +501,85 @@ def list_groups(self): :returns: A list of group instances. """ return Group._list(self) + + def write_time_series(self, timeseries_list): + """Write a list of time series objects to the API. + + The recommended approach to creating time series objects is using + the :meth:`~google.cloud.monitoring.client.Client.time_series` factory + method. + + Example:: + + >>> client.write_time_series([ts1, ts2]) + + If you only need to write a single time series object, consider using + the :meth:`~google.cloud.monitoring.client.Client.write_point` method + instead. + + :type timeseries_list: + list of :class:`~google.cloud.monitoring.timeseries.TimeSeries` + :param timeseries_list: + A list of time series object to be written + to the API. Each time series must contain exactly one point. + """ + path = '/projects/{project}/timeSeries/'.format( + project=self.project) + timeseries_dict = [timeseries._to_dict() + for timeseries in timeseries_list] + self.connection.api_request(method='POST', path=path, + data={'timeSeries': timeseries_dict}) + + def write_point(self, metric, resource, value, + end_time=None, + start_time=None): + """Write a single point for a metric to the API. + + This is a convenience method to write a single time series object to + the API. To write multiple time series objects to the API as a batch + operation, use the + :meth:`~google.cloud.monitoring.client.Client.time_series` + factory method to create time series objects and the + :meth:`~google.cloud.monitoring.client.Client.write_time_series` + method to write the objects. + + Example:: + + >>> client.write_point(metric, resource, 3.14) + + :type metric: :class:`~gcloud.monitoring.metric.Metric` + :param metric: A :class:`~gcloud.monitoring.metric.Metric` object. + + :type resource: :class:`~gcloud.monitoring.resource.Resource` + :param resource: A :class:`~gcloud.monitoring.resource.Resource` + object. + + :type value: bool, int, string, or float + :param value: + The value of the data point to create for the + :class:`~google.cloud.monitoring.timeseries.TimeSeries`. + + .. note:: + + The Python type of the value will determine the + :class:`~ValueType` sent to the API, which must match the value + type specified in the metric descriptor. For example, a Python + float will be sent to the API as a :data:`ValueType.DOUBLE`. + + :type end_time: :class:`~datetime.datetime` + :param end_time: + The end time for the point to be included in the time series. + Assumed to be UTC if no time zone information is present. + Defaults to the current time, as obtained by calling + :meth:`datetime.datetime.utcnow`. + + :type start_time: :class:`~datetime.datetime` + :param start_time: + The start time for the point to be included in the time series. + Assumed to be UTC if no time zone information is present. + Defaults to None. If the start time is unspecified, + the API interprets the start time to be the same as the end time. + """ + timeseries = self.time_series( + metric, resource, value, end_time, start_time) + self.write_time_series([timeseries]) diff --git a/google/cloud/monitoring/metric.py b/google/cloud/monitoring/metric.py index bd31719f37bb..af3952336e57 100644 --- a/google/cloud/monitoring/metric.py +++ b/google/cloud/monitoring/metric.py @@ -348,3 +348,16 @@ def _from_dict(cls, info): type=info['type'], labels=info.get('labels', {}), ) + + def _to_dict(self): + """Build a dictionary ready to be serialized to the JSON format. + + :rtype: dict + :returns: A dict representation of the object that can be written to + the API. + """ + info = { + 'type': self.type, + 'labels': self.labels, + } + return info diff --git a/google/cloud/monitoring/resource.py b/google/cloud/monitoring/resource.py index 6d3c687cbf8b..e174fdc2a03b 100644 --- a/google/cloud/monitoring/resource.py +++ b/google/cloud/monitoring/resource.py @@ -187,3 +187,16 @@ def _from_dict(cls, info): type=info['type'], labels=info.get('labels', {}), ) + + def _to_dict(self): + """Build a dictionary ready to be serialized to the JSON format. + + :rtype: dict + :returns: A dict representation of the object that can be written to + the API. + """ + info = { + 'type': self.type, + 'labels': self.labels, + } + return info diff --git a/google/cloud/monitoring/timeseries.py b/google/cloud/monitoring/timeseries.py index 64e2443dcbf6..402c96c34b02 100644 --- a/google/cloud/monitoring/timeseries.py +++ b/google/cloud/monitoring/timeseries.py @@ -90,6 +90,23 @@ def header(self, points=None): points = list(points) if points else [] return self._replace(points=points) + def _to_dict(self): + """Build a dictionary ready to be serialized to the JSON wire format. + + Since this method is used when writing to the API, it excludes + output-only fields. + + :rtype: dict + :returns: The dictionary representation of the time series object. + """ + info = { + 'metric': self.metric._to_dict(), + 'resource': self.resource._to_dict(), + 'points': [point._to_dict() for point in self.points] + } + + return info + @classmethod def _from_dict(cls, info): """Construct a time series from the parsed JSON representation. @@ -124,6 +141,41 @@ def __repr__(self): ) +def _make_typed_value(value): + """Creates a dict representing TypeValue API object from a value. + + Typed values contain a string representing the type of the + value being written, and the value itself. They are used when writing + points to time series. This method returns the appropriate string to + use when writing typed values based on the Python type of the value. + + This method uses the Python type of the object to infer the correct + type to send to the API. For example, a Python float will be sent to the + API with "doubleValue" as its key. + + See: https://cloud.google.com/monitoring/api/ref_v3/rest/v3/TypedValue + + :type value: bool, int, float, str, or dict + :param value: value to infer the typed value of. + + :rtype: dict + :returns: A dict + """ + typed_value_map = { + bool: "boolValue", + int: "int64Value", + float: "doubleValue", + str: "stringValue", + dict: "distributionValue", + } + type_ = typed_value_map[type(value)] + if type_ == "int64Value": + value = str(value) + return { + type_: value + } + + class Point(collections.namedtuple('Point', 'end_time start_time value')): """A single point in a time series. @@ -156,3 +208,24 @@ def _from_dict(cls, info): value = int(value) # Convert from string. return cls(end_time, start_time, value) + + def _to_dict(self): + """Build a dictionary ready to be serialized to the JSON wire format. + + This method serializes a point in JSON format to be written + to the API. + + :rtype: dict + :returns: The dictionary representation of the point object. + """ + info = { + 'interval': { + 'endTime': self.end_time + }, + 'value': _make_typed_value(self.value) + } + + if self.start_time is not None: + info['interval']['startTime'] = self.start_time + + return info diff --git a/system_tests/monitoring.py b/system_tests/monitoring.py index af68a24a40f4..cbee13a113fe 100644 --- a/system_tests/monitoring.py +++ b/system_tests/monitoring.py @@ -14,15 +14,17 @@ import unittest +from google.cloud.exceptions import BadRequest from google.cloud.exceptions import InternalServerError from google.cloud.exceptions import NotFound from google.cloud.exceptions import ServiceUnavailable from google.cloud import monitoring from retry import RetryErrors +from retry import RetryResult from system_test_utils import unique_resource_id -retry_404 = RetryErrors(NotFound) +retry_404 = RetryErrors(NotFound, max_tries=5) retry_404_500 = RetryErrors((NotFound, InternalServerError)) retry_500 = RetryErrors(InternalServerError) retry_503 = RetryErrors(ServiceUnavailable) @@ -159,7 +161,7 @@ def test_query(self): pass # Not necessarily reached. def test_create_and_delete_metric_descriptor(self): - METRIC_TYPE = ('custom.googleapis.com/tmp/systest' + + METRIC_TYPE = ('custom.googleapis.com/tmp/system_test_example' + unique_resource_id()) METRIC_KIND = monitoring.MetricKind.GAUGE VALUE_TYPE = monitoring.ValueType.DOUBLE @@ -176,6 +178,48 @@ def test_create_and_delete_metric_descriptor(self): retry_500(descriptor.create)() retry_404_500(descriptor.delete)() + def test_write_point(self): + METRIC_TYPE = ('custom.googleapis.com/tmp/system_test_example' + + unique_resource_id()) + METRIC_KIND = monitoring.MetricKind.GAUGE + VALUE_TYPE = monitoring.ValueType.DOUBLE + DESCRIPTION = 'System test example -- DELETE ME!' + VALUE = 3.14 + + client = monitoring.Client() + descriptor = client.metric_descriptor( + METRIC_TYPE, + metric_kind=METRIC_KIND, + value_type=VALUE_TYPE, + description=DESCRIPTION, + ) + + descriptor.create() + retry_404(descriptor._fetch)(client, METRIC_TYPE) + + metric = client.metric(METRIC_TYPE, {}) + resource = client.resource('global', {}) + + client.write_point(metric, resource, VALUE) + + def _query_timeseries_with_retries(): + def _has_timeseries(result): + return len(list(result)) > 0 + retry_result = RetryResult(_has_timeseries, max_tries=7)( + client.query) + return RetryErrors(BadRequest)(retry_result) + + query = _query_timeseries_with_retries()(METRIC_TYPE, minutes=5) + timeseries_list = list(query) + self.assertEqual(len(timeseries_list), 1) + timeseries = timeseries_list[0] + self.assertEqual(timeseries.metric, metric) + # resource labels will not be equal. + self.assertEqual(timeseries.resource.type, resource.type) + self.assertEqual(timeseries.points[0].value, VALUE) + + retry_404(descriptor.delete)() + with self.assertRaises(NotFound): descriptor.delete() diff --git a/unit_tests/monitoring/test_client.py b/unit_tests/monitoring/test_client.py index 62487b137f19..202b76f2f24e 100644 --- a/unit_tests/monitoring/test_client.py +++ b/unit_tests/monitoring/test_client.py @@ -186,6 +186,7 @@ def test_timeseries_factory_gauge(self): import datetime from unit_tests._testing import _Monkey import google.cloud.monitoring.client + from google.cloud._helpers import _datetime_to_rfc3339 METRIC_TYPE = 'custom.googleapis.com/my_metric' METRIC_LABELS = { 'status': 'successful' @@ -199,6 +200,7 @@ def test_timeseries_factory_gauge(self): VALUE = 42 TIME1 = datetime.datetime.utcnow() + TIME1_STR = _datetime_to_rfc3339(TIME1, ignore_zone=False) client = self._makeOne(project=PROJECT, credentials=_Credentials()) client.connection = _Connection() # For safety's sake. @@ -213,19 +215,21 @@ def test_timeseries_factory_gauge(self): self.assertEqual(len(timeseries.points), 1) self.assertEqual(timeseries.points[0].value, VALUE) self.assertIsNone(timeseries.points[0].start_time) - self.assertEqual(timeseries.points[0].end_time, TIME1) + self.assertEqual(timeseries.points[0].end_time, TIME1_STR) TIME2 = datetime.datetime.utcnow() + TIME2_STR = _datetime_to_rfc3339(TIME2, ignore_zone=False) # Construct a time series assuming a gauge metric using the current # time with _Monkey(google.cloud.monitoring.client, _UTCNOW=lambda: TIME2): timeseries_no_end = client.time_series(metric, resource, VALUE) - self.assertEqual(timeseries_no_end.points[0].end_time, TIME2) + self.assertEqual(timeseries_no_end.points[0].end_time, TIME2_STR) self.assertIsNone(timeseries_no_end.points[0].start_time) def test_timeseries_factory_cumulative(self): import datetime + from google.cloud._helpers import _datetime_to_rfc3339 MY_CUMULATIVE_METRIC = 'custom.googleapis.com/my_cumulative_metric' METRIC_LABELS = { 'status': 'successful' @@ -261,14 +265,18 @@ def test_timeseries_factory_cumulative(self): start_time=RESET_TIME, end_time=TIME2) + RESET_TIME_STR = _datetime_to_rfc3339(RESET_TIME, ignore_zone=False) + TIME1_STR = _datetime_to_rfc3339(TIME1, ignore_zone=False) + TIME2_STR = _datetime_to_rfc3339(TIME2, ignore_zone=False) + self.assertEqual(cumulative_timeseries.points[0].start_time, - RESET_TIME) - self.assertEqual(cumulative_timeseries.points[0].end_time, TIME1) + RESET_TIME_STR) + self.assertEqual(cumulative_timeseries.points[0].end_time, TIME1_STR) self.assertEqual(cumulative_timeseries.points[0].value, VALUE) self.assertEqual(cumulative_timeseries2.points[0].start_time, - RESET_TIME) + RESET_TIME_STR) self.assertEqual(cumulative_timeseries2.points[0].end_time, - TIME2) + TIME2_STR) self.assertEqual(cumulative_timeseries2.points[0].value, VALUE2) def test_fetch_metric_descriptor(self): @@ -541,9 +549,80 @@ def test_list_groups(self): 'query_params': {}} self.assertEqual(request, expected_request) + def test_write_time_series(self): + PATH = '/projects/{project}/timeSeries/'.format(project=PROJECT) + client = self._makeOne(project=PROJECT, credentials=_Credentials()) -class _Credentials(object): + RESOURCE_TYPE = 'gce_instance' + RESOURCE_LABELS = { + 'instance_id': '1234567890123456789', + 'zone': 'us-central1-f' + } + + METRIC_TYPE = 'custom.googleapis.com/my_metric' + METRIC_LABELS = { + 'status': 'successful' + } + METRIC_TYPE2 = 'custom.googleapis.com/count_404s' + METRIC_LABELS2 = { + 'request_ip': '127.0.0.1' + } + + connection = client.connection = _Connection({}) + + METRIC = client.metric(METRIC_TYPE, METRIC_LABELS) + METRIC2 = client.metric(METRIC_TYPE2, METRIC_LABELS2) + RESOURCE = client.resource(RESOURCE_TYPE, RESOURCE_LABELS) + + TIMESERIES1 = client.time_series(METRIC, RESOURCE, 3) + TIMESERIES2 = client.time_series(METRIC2, RESOURCE, 3.14) + + expected_data = { + 'timeSeries': [ + TIMESERIES1._to_dict(), + TIMESERIES2._to_dict() + ] + } + expected_request = {'method': 'POST', 'path': PATH, + 'data': expected_data} + + client.write_time_series([TIMESERIES1, TIMESERIES2]) + request, = connection._requested + self.assertEqual(request, expected_request) + + def test_write_point(self): + import datetime + PATH = '/projects/{project}/timeSeries/'.format(project=PROJECT) + client = self._makeOne(project=PROJECT, credentials=_Credentials()) + + RESOURCE_TYPE = 'gce_instance' + RESOURCE_LABELS = { + 'instance_id': '1234567890123456789', + 'zone': 'us-central1-f' + } + METRIC_TYPE = 'custom.googleapis.com/my_metric' + METRIC_LABELS = { + 'status': 'successful' + } + + connection = client.connection = _Connection({}) + + METRIC = client.metric(METRIC_TYPE, METRIC_LABELS) + RESOURCE = client.resource(RESOURCE_TYPE, RESOURCE_LABELS) + VALUE = 3.14 + TIMESTAMP = datetime.datetime.now() + TIMESERIES = client.time_series(METRIC, RESOURCE, VALUE, TIMESTAMP) + + expected_request = {'method': 'POST', 'path': PATH, + 'data': {'timeSeries': [TIMESERIES._to_dict()]}} + + client.write_point(METRIC, RESOURCE, VALUE, TIMESTAMP) + request, = connection._requested + self.assertEqual(request, expected_request) + + +class _Credentials(object): _scopes = None @staticmethod diff --git a/unit_tests/monitoring/test_metric.py b/unit_tests/monitoring/test_metric.py index 07e73b52c980..08b987127227 100644 --- a/unit_tests/monitoring/test_metric.py +++ b/unit_tests/monitoring/test_metric.py @@ -530,6 +530,16 @@ def test_from_dict_defaults(self): self.assertEqual(metric.type, TYPE) self.assertEqual(metric.labels, {}) + def test_to_dict(self): + TYPE = 'custom.googleapis.com/my_metric' + LABELS = {} + metric = self._makeOne(TYPE, LABELS) + expected_dict = { + 'type': TYPE, + 'labels': LABELS + } + self.assertEquals(metric._to_dict(), expected_dict) + class _Connection(object): diff --git a/unit_tests/monitoring/test_resource.py b/unit_tests/monitoring/test_resource.py index d4f8c6b461b5..4a2e723c129a 100644 --- a/unit_tests/monitoring/test_resource.py +++ b/unit_tests/monitoring/test_resource.py @@ -316,6 +316,19 @@ def test_from_dict_defaults(self): self.assertEqual(resource.type, TYPE) self.assertEqual(resource.labels, {}) + def test_to_dict(self): + TYPE = 'gce_instance' + LABELS = { + 'instance_id': '1234567890123456789', + 'zone': 'us-central1-a', + } + resource = self._makeOne(TYPE, LABELS) + expected_dict = { + 'type': TYPE, + 'labels': LABELS + } + self.assertEquals(resource._to_dict(), expected_dict) + class _Connection(object): diff --git a/unit_tests/monitoring/test_timeseries.py b/unit_tests/monitoring/test_timeseries.py index eff705255c8d..ca724b6b197e 100644 --- a/unit_tests/monitoring/test_timeseries.py +++ b/unit_tests/monitoring/test_timeseries.py @@ -33,7 +33,6 @@ class TestTimeSeries(unittest.TestCase): - def _getTargetClass(self): from google.cloud.monitoring.timeseries import TimeSeries return TimeSeries @@ -147,9 +146,40 @@ def test_labels(self): self.assertIsNotNone(series._labels) self.assertEqual(series.labels, labels) + def test_to_dict(self): + import datetime + from google.cloud._helpers import _datetime_to_rfc3339 -class TestPoint(unittest.TestCase): + from google.cloud.monitoring.metric import Metric + from google.cloud.monitoring.resource import Resource + from google.cloud.monitoring.timeseries import Point + + VALUE = 42 + end_time = datetime.datetime.now() + end_time_str = _datetime_to_rfc3339(end_time, ignore_zone=False) + + METRIC = Metric(type=METRIC_TYPE, labels=METRIC_LABELS) + RESOURCE = Resource(type=RESOURCE_TYPE, labels=RESOURCE_LABELS) + POINT = Point(start_time=None, end_time=end_time_str, value=VALUE) + + info = { + 'metric': {'type': METRIC_TYPE, 'labels': METRIC_LABELS}, + 'resource': {'type': RESOURCE_TYPE, 'labels': RESOURCE_LABELS}, + 'points': [{ + 'interval': { + 'endTime': end_time_str}, + 'value': {'int64Value': str(VALUE)}, + }] + } + series = self._makeOne(metric=METRIC, resource=RESOURCE, + metric_kind=None, value_type=None, + points=[POINT]) + series_dict = series._to_dict() + self.assertEqual(info, series_dict) + + +class TestPoint(unittest.TestCase): def _getTargetClass(self): from google.cloud.monitoring.timeseries import Point return Point @@ -196,3 +226,40 @@ def test_from_dict_int64(self): self.assertIsNone(point.start_time) self.assertEqual(point.end_time, TS1) self.assertEqual(point.value, VALUE) + + def test_to_dict_int64(self): + import datetime + from google.cloud._helpers import _datetime_to_rfc3339 + VALUE = 42 + end_time = datetime.datetime.now() + end_time_str = _datetime_to_rfc3339(end_time, ignore_zone=False) + point = self._makeOne(end_time=end_time_str, start_time=None, + value=VALUE) + info = { + 'interval': {'endTime': end_time_str}, + 'value': {'int64Value': str(VALUE)}, + } + + point_dict = point._to_dict() + self.assertEqual(info, point_dict) + + def test_to_dict_float_with_start_time(self): + import datetime + from google.cloud._helpers import _datetime_to_rfc3339 + VALUE = 1.6180339 + start_time = datetime.datetime.now() + start_time_str = _datetime_to_rfc3339(start_time, ignore_zone=False) + end_time = datetime.datetime.now() + end_time_str = _datetime_to_rfc3339(end_time, ignore_zone=False) + + point = self._makeOne(end_time=end_time_str, start_time=start_time_str, + value=VALUE) + info = { + 'interval': { + 'startTime': start_time_str, + 'endTime': end_time_str}, + 'value': {'doubleValue': VALUE}, + } + + point_dict = point._to_dict() + self.assertEqual(info, point_dict) From 2ddc536f91c92054529dac1b6eb2c45b51ae6685 Mon Sep 17 00:00:00 2001 From: Bill Prin Date: Wed, 14 Sep 2016 17:22:01 -0700 Subject: [PATCH 03/11] More review comments --- docs/monitoring-usage.rst | 3 ++- system_tests/monitoring.py | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/monitoring-usage.rst b/docs/monitoring-usage.rst index 79218c37b3ec..52c947ce9577 100644 --- a/docs/monitoring-usage.rst +++ b/docs/monitoring-usage.rst @@ -325,7 +325,8 @@ However, ``DELTA`` is not supported for custom metrics. ``GAUGE`` metrics represent only a single point in time, so only the ``end_time`` should be specified:: - >>> client.write_point(metric=metric, resource=resource, 3.14, end_time=end) # API call + >>> client.write_point(metric=metric, resource=resource, + ... value=3.14, end_time=end_time) # API call By default, ``end_time`` defaults to :meth:`~datetime.datetime.utcnow()`, so metrics can be written to the current time as follows:: diff --git a/system_tests/monitoring.py b/system_tests/monitoring.py index cbee13a113fe..040dbebe8599 100644 --- a/system_tests/monitoring.py +++ b/system_tests/monitoring.py @@ -207,6 +207,7 @@ def _has_timeseries(result): return len(list(result)) > 0 retry_result = RetryResult(_has_timeseries, max_tries=7)( client.query) + return retry_result return RetryErrors(BadRequest)(retry_result) query = _query_timeseries_with_retries()(METRIC_TYPE, minutes=5) @@ -214,11 +215,11 @@ def _has_timeseries(result): self.assertEqual(len(timeseries_list), 1) timeseries = timeseries_list[0] self.assertEqual(timeseries.metric, metric) - # resource labels will not be equal. - self.assertEqual(timeseries.resource.type, resource.type) - self.assertEqual(timeseries.points[0].value, VALUE) + # project_id label only exists on output. + del timeseries.resource.labels['project_id'] + self.assertEqual(timeseries.resource, resource) - retry_404(descriptor.delete)() + descriptor.delete() with self.assertRaises(NotFound): descriptor.delete() From a8337a01fb4fe51c9bc21e7285c9901e7222a91e Mon Sep 17 00:00:00 2001 From: Bill Prin Date: Wed, 14 Sep 2016 17:27:36 -0700 Subject: [PATCH 04/11] gcloud rename mixup --- docs/monitoring-usage.rst | 26 +++++++++++++------------- google/cloud/monitoring/client.py | 8 ++++---- google/cloud/monitoring/timeseries.py | 2 +- system_tests/monitoring.py | 1 - 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/docs/monitoring-usage.rst b/docs/monitoring-usage.rst index 52c947ce9577..843a0004f94a 100644 --- a/docs/monitoring-usage.rst +++ b/docs/monitoring-usage.rst @@ -287,22 +287,22 @@ The Stackdriver Monitoring API can be used to write data points to custom metric the documentation on `Custom Metrics`_ for more information. To write a data point to a custom metric, you must provide an instance of -:class:`~gcloud.monitoring.metric.Metric` specifying the metric type as well as the values for +:class:`~google.cloud.monitoring.metric.Metric` specifying the metric type as well as the values for the metric labels. You will need to have either created the metric descriptor earlier (see the `Metric Descriptors`_ section) or rely on metric type auto-creation (see `Auto-creation of custom metrics`_). -You will also need to provide a :class:`~gcloud.monitoring.resource.Resource` instance specifying a -monitored resource type as well as values for all of the monitored resource labels, except for -``project_id``, which is ignored when it's included in writes to the API. A good choice is to use -the underlying physical resource where your application code runs – e.g., a monitored resource -type of ``gce_instance`` or ``aws_ec2_instance``. In some limited circumstances, such as when -only a single process writes to the custom metric, you may choose to use the ``global`` monitored -resource type. +You will also need to provide a :class:`~google.cloud.monitoring.resource.Resource` instance +specifying a monitored resource type as well as values for all of the monitored resource labels, +except for ``project_id``, which is ignored when it's included in writes to the API. A good +choice is to use the underlying physical resource where your application code runs – e.g., a +monitored resource type of ``gce_instance`` or ``aws_ec2_instance``. In some limited +circumstances, such as when only a single process writes to the custom metric, you may choose to +use the ``global`` monitored resource type. -See `Monitored resource types`_ for more information about parcitular monitored resource types. +See `Monitored resource types`_ for more information about particular monitored resource types. ->>> from gcloud import monitoring +>>> from google.cloud import monitoring >>> # Create a Resource object for the desired monitored resource type. >>> resource = client.resource('gce_instance', labels={ ... 'instance_id': '1234567890123456789', @@ -313,8 +313,8 @@ See `Monitored resource types`_ for more information about parcitular monitored ... 'status': 'successful' ... }) -With a ``Metric`` and ``Resource`` in hand, the :class:`~gcloud.monitoring.client.Client` -can be used to write :class:`~gcloud.monitoring.timeseries.Point` values. +With a ``Metric`` and ``Resource`` in hand, the :class:`~google.cloud.monitoring.client.Client` +can be used to write :class:`~google.cloud.monitoring.timeseries.Point` values. When writing points, the Python type of the value must match the *value type* of the metric descriptor associated with the metric. For example, a Python float will map to ``ValueType.DOUBLE``. @@ -345,7 +345,7 @@ In the examples below, the ``end_time`` again defaults to the current time:: >>> client.write_point(metric, resource, 6, start_time=RESET) # API call To write multiple ``TimeSeries`` in a single batch, you can use -:meth:`~gcloud.monitoring.client.write_time_series`:: +:meth:`~google.cloud.monitoring.client.write_time_series`:: >>> ts1 = client.time_series(metric1, resource, 3.14, end_time=end_time) >>> ts2 = client.time_series(metric2, resource, 42, end_time=end_time) diff --git a/google/cloud/monitoring/client.py b/google/cloud/monitoring/client.py index e8fdc857f565..18e9d82988e4 100644 --- a/google/cloud/monitoring/client.py +++ b/google/cloud/monitoring/client.py @@ -547,11 +547,11 @@ def write_point(self, metric, resource, value, >>> client.write_point(metric, resource, 3.14) - :type metric: :class:`~gcloud.monitoring.metric.Metric` - :param metric: A :class:`~gcloud.monitoring.metric.Metric` object. + :type metric: :class:`~google.cloud.monitoring.metric.Metric` + :param metric: A :class:`~google.cloud.monitoring.metric.Metric` object. - :type resource: :class:`~gcloud.monitoring.resource.Resource` - :param resource: A :class:`~gcloud.monitoring.resource.Resource` + :type resource: :class:`~google.cloud.monitoring.resource.Resource` + :param resource: A :class:`~google.cloud.monitoring.resource.Resource` object. :type value: bool, int, string, or float diff --git a/google/cloud/monitoring/timeseries.py b/google/cloud/monitoring/timeseries.py index 402c96c34b02..ef1d6f5e321a 100644 --- a/google/cloud/monitoring/timeseries.py +++ b/google/cloud/monitoring/timeseries.py @@ -102,7 +102,7 @@ def _to_dict(self): info = { 'metric': self.metric._to_dict(), 'resource': self.resource._to_dict(), - 'points': [point._to_dict() for point in self.points] + 'points': [point._to_dict() for point in self.points], } return info diff --git a/system_tests/monitoring.py b/system_tests/monitoring.py index 040dbebe8599..0790513ef386 100644 --- a/system_tests/monitoring.py +++ b/system_tests/monitoring.py @@ -207,7 +207,6 @@ def _has_timeseries(result): return len(list(result)) > 0 retry_result = RetryResult(_has_timeseries, max_tries=7)( client.query) - return retry_result return RetryErrors(BadRequest)(retry_result) query = _query_timeseries_with_retries()(METRIC_TYPE, minutes=5) From 1a0fff2cce7d932e995b697e5a812c123153e769 Mon Sep 17 00:00:00 2001 From: Bill Prin Date: Wed, 14 Sep 2016 17:30:41 -0700 Subject: [PATCH 05/11] word cleanup --- google/cloud/monitoring/timeseries.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/google/cloud/monitoring/timeseries.py b/google/cloud/monitoring/timeseries.py index ef1d6f5e321a..fca7e1a042e5 100644 --- a/google/cloud/monitoring/timeseries.py +++ b/google/cloud/monitoring/timeseries.py @@ -142,12 +142,11 @@ def __repr__(self): def _make_typed_value(value): - """Creates a dict representing TypeValue API object from a value. + """Creates a dict representing a TypeValue API object from a value. - Typed values contain a string representing the type of the - value being written, and the value itself. They are used when writing - points to time series. This method returns the appropriate string to - use when writing typed values based on the Python type of the value. + Typed values are objects with the value itself as the value, keyed by the + type of the value. They are used when writing points to time series. This + method returns the dict representation for the TypedValue. This method uses the Python type of the object to infer the correct type to send to the API. For example, a Python float will be sent to the From 21b6bb9a0333e75fe4f7394e54190856d16d630f Mon Sep 17 00:00:00 2001 From: Bill Prin Date: Wed, 14 Sep 2016 17:33:27 -0700 Subject: [PATCH 06/11] Small typo --- google/cloud/monitoring/timeseries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/cloud/monitoring/timeseries.py b/google/cloud/monitoring/timeseries.py index fca7e1a042e5..ff5ae2bb830b 100644 --- a/google/cloud/monitoring/timeseries.py +++ b/google/cloud/monitoring/timeseries.py @@ -142,7 +142,7 @@ def __repr__(self): def _make_typed_value(value): - """Creates a dict representing a TypeValue API object from a value. + """Creates a dict representing a TypedValue API object from a value. Typed values are objects with the value itself as the value, keyed by the type of the value. They are used when writing points to time series. This From 58ba168d4594d1f1780b536c3d2ad3490b2e1b66 Mon Sep 17 00:00:00 2001 From: Bill Prin Date: Wed, 14 Sep 2016 17:42:18 -0700 Subject: [PATCH 07/11] More mixups --- google/cloud/monitoring/client.py | 3 ++- unit_tests/monitoring/test_metric.py | 2 +- unit_tests/monitoring/test_resource.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/google/cloud/monitoring/client.py b/google/cloud/monitoring/client.py index 18e9d82988e4..4c6d33ad02e0 100644 --- a/google/cloud/monitoring/client.py +++ b/google/cloud/monitoring/client.py @@ -548,7 +548,8 @@ def write_point(self, metric, resource, value, >>> client.write_point(metric, resource, 3.14) :type metric: :class:`~google.cloud.monitoring.metric.Metric` - :param metric: A :class:`~google.cloud.monitoring.metric.Metric` object. + :param metric: A :class:`~google.cloud.monitoring.metric.Metric` + object. :type resource: :class:`~google.cloud.monitoring.resource.Resource` :param resource: A :class:`~google.cloud.monitoring.resource.Resource` diff --git a/unit_tests/monitoring/test_metric.py b/unit_tests/monitoring/test_metric.py index 08b987127227..c6e6a5d8bfd6 100644 --- a/unit_tests/monitoring/test_metric.py +++ b/unit_tests/monitoring/test_metric.py @@ -536,7 +536,7 @@ def test_to_dict(self): metric = self._makeOne(TYPE, LABELS) expected_dict = { 'type': TYPE, - 'labels': LABELS + 'labels': LABELS, } self.assertEquals(metric._to_dict(), expected_dict) diff --git a/unit_tests/monitoring/test_resource.py b/unit_tests/monitoring/test_resource.py index 4a2e723c129a..5e3cac503b01 100644 --- a/unit_tests/monitoring/test_resource.py +++ b/unit_tests/monitoring/test_resource.py @@ -325,7 +325,7 @@ def test_to_dict(self): resource = self._makeOne(TYPE, LABELS) expected_dict = { 'type': TYPE, - 'labels': LABELS + 'labels': LABELS, } self.assertEquals(resource._to_dict(), expected_dict) From c075afcd02af72fb1c8f31cae986b5e4824a23f5 Mon Sep 17 00:00:00 2001 From: Bill Prin Date: Thu, 15 Sep 2016 11:15:01 -0700 Subject: [PATCH 08/11] supriya reivew --- docs/monitoring-usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/monitoring-usage.rst b/docs/monitoring-usage.rst index 843a0004f94a..99890be454d4 100644 --- a/docs/monitoring-usage.rst +++ b/docs/monitoring-usage.rst @@ -326,7 +326,7 @@ However, ``DELTA`` is not supported for custom metrics. specified:: >>> client.write_point(metric=metric, resource=resource, - ... value=3.14, end_time=end_time) # API call + ... value=3.14, end_time=end_time) # API call By default, ``end_time`` defaults to :meth:`~datetime.datetime.utcnow()`, so metrics can be written to the current time as follows:: From b6d3e9c35e72c774bdb46f8aeabf569fba7efb1b Mon Sep 17 00:00:00 2001 From: Bill Prin Date: Mon, 19 Sep 2016 10:55:44 -0700 Subject: [PATCH 09/11] Riley review --- google/cloud/monitoring/metric.py | 3 +-- google/cloud/monitoring/resource.py | 3 +-- google/cloud/monitoring/timeseries.py | 6 ++---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/google/cloud/monitoring/metric.py b/google/cloud/monitoring/metric.py index af3952336e57..5bec97f7c4eb 100644 --- a/google/cloud/monitoring/metric.py +++ b/google/cloud/monitoring/metric.py @@ -356,8 +356,7 @@ def _to_dict(self): :returns: A dict representation of the object that can be written to the API. """ - info = { + return { 'type': self.type, 'labels': self.labels, } - return info diff --git a/google/cloud/monitoring/resource.py b/google/cloud/monitoring/resource.py index e174fdc2a03b..b49ecc864f97 100644 --- a/google/cloud/monitoring/resource.py +++ b/google/cloud/monitoring/resource.py @@ -195,8 +195,7 @@ def _to_dict(self): :returns: A dict representation of the object that can be written to the API. """ - info = { + return { 'type': self.type, 'labels': self.labels, } - return info diff --git a/google/cloud/monitoring/timeseries.py b/google/cloud/monitoring/timeseries.py index ff5ae2bb830b..c80083baeb3a 100644 --- a/google/cloud/monitoring/timeseries.py +++ b/google/cloud/monitoring/timeseries.py @@ -142,7 +142,7 @@ def __repr__(self): def _make_typed_value(value): - """Creates a dict representing a TypedValue API object from a value. + """Create a dict representing a TypedValue API object. Typed values are objects with the value itself as the value, keyed by the type of the value. They are used when writing points to time series. This @@ -170,9 +170,7 @@ def _make_typed_value(value): type_ = typed_value_map[type(value)] if type_ == "int64Value": value = str(value) - return { - type_: value - } + return { type_: value } class Point(collections.namedtuple('Point', 'end_time start_time value')): From 2e79651a7c6c320c4ac49c1381a903882b81b914 Mon Sep 17 00:00:00 2001 From: Bill Prin Date: Mon, 19 Sep 2016 14:05:14 -0700 Subject: [PATCH 10/11] Fix lint --- google/cloud/monitoring/timeseries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/cloud/monitoring/timeseries.py b/google/cloud/monitoring/timeseries.py index c80083baeb3a..b3926bd63cb4 100644 --- a/google/cloud/monitoring/timeseries.py +++ b/google/cloud/monitoring/timeseries.py @@ -170,7 +170,7 @@ def _make_typed_value(value): type_ = typed_value_map[type(value)] if type_ == "int64Value": value = str(value) - return { type_: value } + return {type_: value} class Point(collections.namedtuple('Point', 'end_time start_time value')): From ff46b70752f3145e9dafa684ade98a6f4c35412e Mon Sep 17 00:00:00 2001 From: Bill Prin Date: Mon, 19 Sep 2016 14:18:51 -0700 Subject: [PATCH 11/11] Remove fetch descriptor, wrap write_point 500 --- system_tests/monitoring.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/system_tests/monitoring.py b/system_tests/monitoring.py index 0790513ef386..6c6931693001 100644 --- a/system_tests/monitoring.py +++ b/system_tests/monitoring.py @@ -195,12 +195,11 @@ def test_write_point(self): ) descriptor.create() - retry_404(descriptor._fetch)(client, METRIC_TYPE) metric = client.metric(METRIC_TYPE, {}) resource = client.resource('global', {}) - client.write_point(metric, resource, VALUE) + retry_500(client.write_point)(metric, resource, VALUE) def _query_timeseries_with_retries(): def _has_timeseries(result):