Skip to content

Commit d3e28ce

Browse files
waprinrimey
authored andcommitted
Writing Custom Metrics (#2057)
This adds write_point() and write_time_series() as Client methods.
1 parent d555320 commit d3e28ce

10 files changed

Lines changed: 493 additions & 13 deletions

File tree

docs/monitoring-usage.rst

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ of the API:
3030
- Querying of time series.
3131
- Querying of metric descriptors and monitored resource descriptors.
3232
- Creation and deletion of metric descriptors for custom metrics.
33-
- (Writing of custom metric data will be coming soon.)
33+
- Writing of custom metric data.
3434

3535
.. _Stackdriver Monitoring API: https://cloud.google.com/monitoring/api/v3/
3636

@@ -278,3 +278,88 @@ follows::
278278

279279
.. _Time Series:
280280
https://cloud.google.com/monitoring/api/ref_v3/rest/v3/TimeSeries
281+
282+
283+
Writing Custom Metrics
284+
---------------------------
285+
286+
The Stackdriver Monitoring API can be used to write data points to custom metrics. Please refer to
287+
the documentation on `Custom Metrics`_ for more information.
288+
289+
To write a data point to a custom metric, you must provide an instance of
290+
:class:`~google.cloud.monitoring.metric.Metric` specifying the metric type as well as the values for
291+
the metric labels. You will need to have either created the metric descriptor earlier (see the
292+
`Metric Descriptors`_ section) or rely on metric type auto-creation (see `Auto-creation of
293+
custom metrics`_).
294+
295+
You will also need to provide a :class:`~google.cloud.monitoring.resource.Resource` instance
296+
specifying a monitored resource type as well as values for all of the monitored resource labels,
297+
except for ``project_id``, which is ignored when it's included in writes to the API. A good
298+
choice is to use the underlying physical resource where your application code runs – e.g., a
299+
monitored resource type of ``gce_instance`` or ``aws_ec2_instance``. In some limited
300+
circumstances, such as when only a single process writes to the custom metric, you may choose to
301+
use the ``global`` monitored resource type.
302+
303+
See `Monitored resource types`_ for more information about particular monitored resource types.
304+
305+
>>> from google.cloud import monitoring
306+
>>> # Create a Resource object for the desired monitored resource type.
307+
>>> resource = client.resource('gce_instance', labels={
308+
... 'instance_id': '1234567890123456789',
309+
... 'zone': 'us-central1-f'
310+
... })
311+
>>> # Create a Metric object, specifying the metric type as well as values for any metric labels.
312+
>>> metric = client.metric(type='custom.googleapis.com/my_metric', labels={
313+
... 'status': 'successful'
314+
... })
315+
316+
With a ``Metric`` and ``Resource`` in hand, the :class:`~google.cloud.monitoring.client.Client`
317+
can be used to write :class:`~google.cloud.monitoring.timeseries.Point` values.
318+
319+
When writing points, the Python type of the value must match the *value type* of the metric
320+
descriptor associated with the metric. For example, a Python float will map to ``ValueType.DOUBLE``.
321+
322+
Stackdriver Monitoring supports several *metric kinds*: ``GAUGE``, ``CUMULATIVE``, and ``DELTA``.
323+
However, ``DELTA`` is not supported for custom metrics.
324+
325+
``GAUGE`` metrics represent only a single point in time, so only the ``end_time`` should be
326+
specified::
327+
328+
>>> client.write_point(metric=metric, resource=resource,
329+
... value=3.14, end_time=end_time) # API call
330+
331+
By default, ``end_time`` defaults to :meth:`~datetime.datetime.utcnow()`, so metrics can be written
332+
to the current time as follows::
333+
334+
>>> client.write_point(metric, resource, 3.14) # API call
335+
336+
``CUMULATIVE`` metrics enable the monitoring system to compute rates of increase on metrics that
337+
sometimes reset, such as after a process restart. Without cumulative metrics, this
338+
reset would otherwise show up as a huge negative spike. For cumulative metrics, the same start
339+
time should be re-used repeatedly as more points are written to the time series.
340+
341+
In the examples below, the ``end_time`` again defaults to the current time::
342+
343+
>>> RESET = datetime.utcnow()
344+
>>> client.write_point(metric, resource, 3, start_time=RESET) # API call
345+
>>> client.write_point(metric, resource, 6, start_time=RESET) # API call
346+
347+
To write multiple ``TimeSeries`` in a single batch, you can use
348+
:meth:`~google.cloud.monitoring.client.write_time_series`::
349+
350+
>>> ts1 = client.time_series(metric1, resource, 3.14, end_time=end_time)
351+
>>> ts2 = client.time_series(metric2, resource, 42, end_time=end_time)
352+
>>> client.write_time_series([ts1, ts2]) # API call
353+
354+
While multiple time series can be written in a single batch, each ``TimeSeries`` object sent to
355+
the API must only include a single point.
356+
357+
All timezone-naive Python ``datetime`` objects are assumed to be UTC.
358+
359+
.. _TimeSeries: https://cloud.google.com/monitoring/api/ref_v3/rest/v3/TimeSeries
360+
.. _Custom Metrics: https://cloud.google.com/monitoring/custom-metrics/
361+
.. _Auto-creation of custom metrics:
362+
https://cloud.google.com/monitoring/custom-metrics/creating-metrics#auto-creation
363+
.. _Metrics: https://cloud.google.com/monitoring/api/v3/metrics
364+
.. _Monitored resource types:
365+
https://cloud.google.com/monitoring/api/resources

google/cloud/monitoring/client.py

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
import datetime
3232

33+
from google.cloud._helpers import _datetime_to_rfc3339
3334
from google.cloud.client import JSONClient
3435
from google.cloud.monitoring.connection import Connection
3536
from google.cloud.monitoring.group import Group
@@ -312,7 +313,7 @@ def time_series(metric, resource, value,
312313
:type start_time: :class:`~datetime.datetime`
313314
:param start_time:
314315
The start time for the point to be included in the time series.
315-
Assumed to be UTC if no time zone information is present
316+
Assumed to be UTC if no time zone information is present.
316317
Defaults to None. If the start time is unspecified,
317318
the API interprets the start time to be the same as the end time.
318319
@@ -321,6 +322,11 @@ def time_series(metric, resource, value,
321322
"""
322323
if end_time is None:
323324
end_time = _UTCNOW()
325+
326+
end_time = _datetime_to_rfc3339(end_time, ignore_zone=False)
327+
if start_time:
328+
start_time = _datetime_to_rfc3339(start_time, ignore_zone=False)
329+
324330
point = Point(value=value, start_time=start_time, end_time=end_time)
325331
return TimeSeries(metric=metric, resource=resource, metric_kind=None,
326332
value_type=None, points=[point])
@@ -495,3 +501,86 @@ def list_groups(self):
495501
:returns: A list of group instances.
496502
"""
497503
return Group._list(self)
504+
505+
def write_time_series(self, timeseries_list):
506+
"""Write a list of time series objects to the API.
507+
508+
The recommended approach to creating time series objects is using
509+
the :meth:`~google.cloud.monitoring.client.Client.time_series` factory
510+
method.
511+
512+
Example::
513+
514+
>>> client.write_time_series([ts1, ts2])
515+
516+
If you only need to write a single time series object, consider using
517+
the :meth:`~google.cloud.monitoring.client.Client.write_point` method
518+
instead.
519+
520+
:type timeseries_list:
521+
list of :class:`~google.cloud.monitoring.timeseries.TimeSeries`
522+
:param timeseries_list:
523+
A list of time series object to be written
524+
to the API. Each time series must contain exactly one point.
525+
"""
526+
path = '/projects/{project}/timeSeries/'.format(
527+
project=self.project)
528+
timeseries_dict = [timeseries._to_dict()
529+
for timeseries in timeseries_list]
530+
self.connection.api_request(method='POST', path=path,
531+
data={'timeSeries': timeseries_dict})
532+
533+
def write_point(self, metric, resource, value,
534+
end_time=None,
535+
start_time=None):
536+
"""Write a single point for a metric to the API.
537+
538+
This is a convenience method to write a single time series object to
539+
the API. To write multiple time series objects to the API as a batch
540+
operation, use the
541+
:meth:`~google.cloud.monitoring.client.Client.time_series`
542+
factory method to create time series objects and the
543+
:meth:`~google.cloud.monitoring.client.Client.write_time_series`
544+
method to write the objects.
545+
546+
Example::
547+
548+
>>> client.write_point(metric, resource, 3.14)
549+
550+
:type metric: :class:`~google.cloud.monitoring.metric.Metric`
551+
:param metric: A :class:`~google.cloud.monitoring.metric.Metric`
552+
object.
553+
554+
:type resource: :class:`~google.cloud.monitoring.resource.Resource`
555+
:param resource: A :class:`~google.cloud.monitoring.resource.Resource`
556+
object.
557+
558+
:type value: bool, int, string, or float
559+
:param value:
560+
The value of the data point to create for the
561+
:class:`~google.cloud.monitoring.timeseries.TimeSeries`.
562+
563+
.. note::
564+
565+
The Python type of the value will determine the
566+
:class:`~ValueType` sent to the API, which must match the value
567+
type specified in the metric descriptor. For example, a Python
568+
float will be sent to the API as a :data:`ValueType.DOUBLE`.
569+
570+
:type end_time: :class:`~datetime.datetime`
571+
:param end_time:
572+
The end time for the point to be included in the time series.
573+
Assumed to be UTC if no time zone information is present.
574+
Defaults to the current time, as obtained by calling
575+
:meth:`datetime.datetime.utcnow`.
576+
577+
:type start_time: :class:`~datetime.datetime`
578+
:param start_time:
579+
The start time for the point to be included in the time series.
580+
Assumed to be UTC if no time zone information is present.
581+
Defaults to None. If the start time is unspecified,
582+
the API interprets the start time to be the same as the end time.
583+
"""
584+
timeseries = self.time_series(
585+
metric, resource, value, end_time, start_time)
586+
self.write_time_series([timeseries])

google/cloud/monitoring/metric.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,3 +348,15 @@ def _from_dict(cls, info):
348348
type=info['type'],
349349
labels=info.get('labels', {}),
350350
)
351+
352+
def _to_dict(self):
353+
"""Build a dictionary ready to be serialized to the JSON format.
354+
355+
:rtype: dict
356+
:returns: A dict representation of the object that can be written to
357+
the API.
358+
"""
359+
return {
360+
'type': self.type,
361+
'labels': self.labels,
362+
}

google/cloud/monitoring/resource.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,15 @@ def _from_dict(cls, info):
187187
type=info['type'],
188188
labels=info.get('labels', {}),
189189
)
190+
191+
def _to_dict(self):
192+
"""Build a dictionary ready to be serialized to the JSON format.
193+
194+
:rtype: dict
195+
:returns: A dict representation of the object that can be written to
196+
the API.
197+
"""
198+
return {
199+
'type': self.type,
200+
'labels': self.labels,
201+
}

google/cloud/monitoring/timeseries.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,23 @@ def header(self, points=None):
9090
points = list(points) if points else []
9191
return self._replace(points=points)
9292

93+
def _to_dict(self):
94+
"""Build a dictionary ready to be serialized to the JSON wire format.
95+
96+
Since this method is used when writing to the API, it excludes
97+
output-only fields.
98+
99+
:rtype: dict
100+
:returns: The dictionary representation of the time series object.
101+
"""
102+
info = {
103+
'metric': self.metric._to_dict(),
104+
'resource': self.resource._to_dict(),
105+
'points': [point._to_dict() for point in self.points],
106+
}
107+
108+
return info
109+
93110
@classmethod
94111
def _from_dict(cls, info):
95112
"""Construct a time series from the parsed JSON representation.
@@ -124,6 +141,38 @@ def __repr__(self):
124141
)
125142

126143

144+
def _make_typed_value(value):
145+
"""Create a dict representing a TypedValue API object.
146+
147+
Typed values are objects with the value itself as the value, keyed by the
148+
type of the value. They are used when writing points to time series. This
149+
method returns the dict representation for the TypedValue.
150+
151+
This method uses the Python type of the object to infer the correct
152+
type to send to the API. For example, a Python float will be sent to the
153+
API with "doubleValue" as its key.
154+
155+
See: https://cloud.google.com/monitoring/api/ref_v3/rest/v3/TypedValue
156+
157+
:type value: bool, int, float, str, or dict
158+
:param value: value to infer the typed value of.
159+
160+
:rtype: dict
161+
:returns: A dict
162+
"""
163+
typed_value_map = {
164+
bool: "boolValue",
165+
int: "int64Value",
166+
float: "doubleValue",
167+
str: "stringValue",
168+
dict: "distributionValue",
169+
}
170+
type_ = typed_value_map[type(value)]
171+
if type_ == "int64Value":
172+
value = str(value)
173+
return {type_: value}
174+
175+
127176
class Point(collections.namedtuple('Point', 'end_time start_time value')):
128177
"""A single point in a time series.
129178
@@ -156,3 +205,24 @@ def _from_dict(cls, info):
156205
value = int(value) # Convert from string.
157206

158207
return cls(end_time, start_time, value)
208+
209+
def _to_dict(self):
210+
"""Build a dictionary ready to be serialized to the JSON wire format.
211+
212+
This method serializes a point in JSON format to be written
213+
to the API.
214+
215+
:rtype: dict
216+
:returns: The dictionary representation of the point object.
217+
"""
218+
info = {
219+
'interval': {
220+
'endTime': self.end_time
221+
},
222+
'value': _make_typed_value(self.value)
223+
}
224+
225+
if self.start_time is not None:
226+
info['interval']['startTime'] = self.start_time
227+
228+
return info

0 commit comments

Comments
 (0)