diff --git a/docs/.DS_Store b/docs/.DS_Store new file mode 100644 index 000000000000..f41eff01ff9c Binary files /dev/null and b/docs/.DS_Store differ diff --git a/docs/conf.py b/docs/conf.py index 37842e9bc08c..6b2dcd872739 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -292,5 +292,6 @@ intersphinx_mapping = { 'httplib2': ('http://bitworking.org/projects/httplib2/doc/html/', None), 'oauth2client': ('http://oauth2client.readthedocs.org/en/latest/', None), + 'pandas': ('http://pandas.pydata.org/pandas-docs/stable/', None), 'python': ('https://docs.python.org/', None), } diff --git a/docs/index.rst b/docs/index.rst index a9c1094a6789..dee362f4bc91 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -119,6 +119,19 @@ logging-metric logging-sink +.. toctree:: + :maxdepth: 0 + :hidden: + :caption: Cloud Monitoring + + monitoring-usage + Client + monitoring-metric + monitoring-resource + monitoring-query + monitoring-timeseries + monitoring-label + .. toctree:: :maxdepth: 0 :hidden: diff --git a/docs/monitoring-client.rst b/docs/monitoring-client.rst new file mode 100644 index 000000000000..8083d39d5f4f --- /dev/null +++ b/docs/monitoring-client.rst @@ -0,0 +1,16 @@ +Monitoring Client +================= + +.. automodule:: gcloud.monitoring.client + :members: + :undoc-members: + :show-inheritance: + +Connection +~~~~~~~~~~ + +.. automodule:: gcloud.monitoring.connection + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/monitoring-label.rst b/docs/monitoring-label.rst new file mode 100644 index 000000000000..827058c92662 --- /dev/null +++ b/docs/monitoring-label.rst @@ -0,0 +1,7 @@ +Label Descriptors +================= + +.. automodule:: gcloud.monitoring.label + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/monitoring-metric.rst b/docs/monitoring-metric.rst new file mode 100644 index 000000000000..79e608b63932 --- /dev/null +++ b/docs/monitoring-metric.rst @@ -0,0 +1,7 @@ +Metric Descriptors +================== + +.. automodule:: gcloud.monitoring.metric + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/monitoring-query.rst b/docs/monitoring-query.rst new file mode 100644 index 000000000000..0a1e64982e48 --- /dev/null +++ b/docs/monitoring-query.rst @@ -0,0 +1,7 @@ +Time Series Query +================= + +.. automodule:: gcloud.monitoring.query + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/monitoring-resource.rst b/docs/monitoring-resource.rst new file mode 100644 index 000000000000..8f6c23d470a7 --- /dev/null +++ b/docs/monitoring-resource.rst @@ -0,0 +1,7 @@ +Resource Descriptors +==================== + +.. automodule:: gcloud.monitoring.resource + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/monitoring-timeseries.rst b/docs/monitoring-timeseries.rst new file mode 100644 index 000000000000..120f7280c885 --- /dev/null +++ b/docs/monitoring-timeseries.rst @@ -0,0 +1,7 @@ +Time Series +=========== + +.. automodule:: gcloud.monitoring.timeseries + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/monitoring-usage.rst b/docs/monitoring-usage.rst new file mode 100644 index 000000000000..521b91411e92 --- /dev/null +++ b/docs/monitoring-usage.rst @@ -0,0 +1,155 @@ +Using the API +============= + + +Introduction +------------ + +With the Monitoring API, you can work with Stackdriver metric data +pertaining to monitored resources in Google Cloud Platform (GCP) +or elsewhere. + +Essential concepts: + +- Metric data is associated with a **monitored resource**. A monitored + resource has a *resource type* and a set of *resource labels* — + key-value pairs — that identify the particular resource. +- A **metric** further identifies the particular kind of data that + is being collected. It has a *metric type* and a set of *metric + labels* that, when combined with the resource labels, identify + a particular time series. +- A **time series** is a collection of data points associated with + points or intervals in time. + +Please refer to the documentation for the `Monitoring API`_ for +more information. + +At present, this client library supports querying of time series, +metric descriptors, and resource descriptors. + +.. _Monitoring API: https://cloud.google.com/monitoring/api/ + + +The Monitoring Client Object +---------------------------- + +The monitoring client library generally makes its +functionality available as methods of the monitoring +:class:`~gcloud.monitoring.client.Client` class. +A :class:`~gcloud.monitoring.client.Client` instance holds +authentication credentials and the ID of the target project with +which the metric data of interest is associated. This project ID +will often refer to a `Stackdriver account`_ binding multiple +GCP projects and AWS accounts. It can also simply be the ID of +a monitored project. + +Most often the authentication credentials will be determined +implicitly from your environment. See :doc:`gcloud-auth` for +more information. + +It is thus typical to create a client object as follows:: + + >>> from gcloud import monitoring + >>> client = monitoring.Client(project='target-project') + +If you are running in Google Compute Engine or Google App Engine, +the current project is the default target project. This default +can be further overridden with the :envvar:`GCLOUD_PROJECT` +environment variable. Using the default target project is +even easier:: + + >>> client = monitoring.Client() + +If necessary, you can pass in ``credentials`` and ``project`` explicitly:: + + >>> client = monitoring.Client(project='target-project', credentials=...) + +.. _Stackdriver account: https://cloud.google.com/monitoring/accounts/ + + +Monitored Resource Descriptors +------------------------------ + +The available monitored resource types are defined by *monitored resource +descriptors*. You can fetch a list of these with the +:meth:`~gcloud.monitoring.client.Client.list_resource_descriptors` method:: + + >>> for descriptor in client.list_resource_descriptors(): + ... print(descriptor.type) + +Each :class:`~gcloud.monitoring.resource.ResourceDescriptor` +has a type, a display name, a description, and a list of +:class:`~gcloud.monitoring.label.LabelDescriptor` instances. +See the documentation about `Monitored Resources`_ +for more information. + +.. _Monitored Resources: + https://cloud.google.com/monitoring/api/v3/monitored-resources + + +Metric Descriptors +------------------ + +The available metric types are defined by *metric descriptors*. +They include `platform metrics`_, `agent metrics`_, and `custom metrics`_. +You can list all of these with the +:meth:`~gcloud.monitoring.client.Client.list_metric_descriptors` method:: + + >>> for descriptor in client.list_metric_descriptors(): + ... print(descriptor.type) + +See :class:`~gcloud.monitoring.metric.MetricDescriptor` and the +`Metric Descriptors`_ API documentation for more information. + +.. _platform metrics: https://cloud.google.com/monitoring/api/metrics +.. _agent metrics: https://cloud.google.com/monitoring/agent/ +.. _custom metrics: https://cloud.google.com/monitoring/custom-metrics/ +.. _Metric Descriptors: + https://cloud.google.com/monitoring/api/ref_v3/rest/v3/\ + projects.metricDescriptors + + +Time Series Queries +------------------- + +A time series includes a collection of data points and a set of +resource and metric label values. +See :class:`~gcloud.monitoring.timeseries.TimeSeries` and the +`Time Series`_ API documentation for more information. + +While you can obtain time series objects by iterating over a +:class:`~gcloud.monitoring.query.Query` object, usually it is +more useful to retrieve time series data in the form of a +:class:`pandas.DataFrame`, where each column corresponds to a +single time series. For this, you must have :mod:`pandas` installed; +it is not a required dependency of ``gcloud-python``. + +You can display CPU utilization across your GCE instances during +the last five minutes as follows:: + + >>> METRIC = 'compute.googleapis.com/instance/cpu/utilization' + >>> query = client.query(METRIC, minutes=5) + >>> print(query.as_dataframe()) + +:class:`~gcloud.monitoring.query.Query` objects provide a variety of +methods for refining the query. You can request temporal alignment +and cross-series reduction, and you can filter by label values. +See the client :meth:`~gcloud.monitoring.client.Client.query` method +and the :class:`~gcloud.monitoring.query.Query` class for more +information. + +For example, you can display CPU utilization during the last hour +across GCE instances with names beginning with ``"mycluster-"``, +averaged over five-minute intervals and aggregated per zone, as +follows:: + + >>> from gcloud.monitoring import Aligner, Reducer + >>> METRIC = 'compute.googleapis.com/instance/cpu/utilization' + >>> query = (client.query(METRIC, hours=1) + ... .select_metrics(instance_name_prefix='mycluster-') + ... .align(Aligner.ALIGN_MEAN, minutes=5) + ... .reduce(Reducer.REDUCE_MEAN, 'resource.zone')) + >>> print(query.as_dataframe()) + +.. _Time Series: + https://cloud.google.com/monitoring/api/ref_v3/rest/v3/TimeSeries diff --git a/gcloud/monitoring/__init__.py b/gcloud/monitoring/__init__.py new file mode 100644 index 000000000000..2ba783148131 --- /dev/null +++ b/gcloud/monitoring/__init__.py @@ -0,0 +1,34 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Google Monitoring API wrapper.""" + +from gcloud.monitoring.client import Client +from gcloud.monitoring.connection import Connection +from gcloud.monitoring.label import LabelDescriptor +from gcloud.monitoring.label import LabelValueType +from gcloud.monitoring.metric import Metric +from gcloud.monitoring.metric import MetricDescriptor +from gcloud.monitoring.metric import MetricKind +from gcloud.monitoring.metric import ValueType +from gcloud.monitoring.query import Aligner +from gcloud.monitoring.query import Query +from gcloud.monitoring.query import Reducer +from gcloud.monitoring.resource import Resource +from gcloud.monitoring.resource import ResourceDescriptor +from gcloud.monitoring.timeseries import Point +from gcloud.monitoring.timeseries import TimeSeries + + +SCOPE = Connection.SCOPE diff --git a/gcloud/monitoring/_dataframe.py b/gcloud/monitoring/_dataframe.py new file mode 100644 index 000000000000..1b5b1944d918 --- /dev/null +++ b/gcloud/monitoring/_dataframe.py @@ -0,0 +1,116 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Time series as :mod:`pandas` dataframes.""" + +import itertools + +TOP_RESOURCE_LABELS = ( + 'project_id', + 'aws_account', + 'location', + 'region', + 'zone', +) + + +def _build_dataframe(time_series_iterable, + label=None, labels=None): # pragma: NO COVER + """Build a :mod:`pandas` dataframe out of time series. + + :type time_series_iterable: + iterable over :class:`~gcloud.monitoring.timeseries.TimeSeries` + :param time_series_iterable: + An iterable (e.g., a query object) yielding time series. + + :type label: string or None + :param label: + The label name to use for the dataframe header. This can be the name + of a resource label or metric label (e.g., ``"instance_name"``), or + the string ``"resource_type"``. + + :type labels: list of strings, or None + :param labels: + A list or tuple of label names to use for the dataframe header. + If more than one label name is provided, the resulting dataframe + will have a multi-level column header. + + Specifying neither ``label`` or ``labels`` results in a dataframe + with a multi-level column header including the resource type and + all available resource and metric labels. + + Specifying both ``label`` and ``labels`` is an error. + + :rtype: :class:`pandas.DataFrame` + :returns: A dataframe where each column represents one time series. + """ + import pandas # pylint: disable=import-error + + if labels is not None: + if label is not None: + raise ValueError('Cannot specify both "label" and "labels".') + elif not labels: + raise ValueError('"labels" must be non-empty or None.') + + columns = [] + headers = [] + for time_series in time_series_iterable: + pandas_series = pandas.Series( + data=[point.value for point in time_series.points], + index=[point.end_time for point in time_series.points], + ) + columns.append(pandas_series) + headers.append(time_series.header()) + + # Implement a smart default of using all available labels. + if label is None and labels is None: + resource_labels = set(itertools.chain.from_iterable( + header.resource.labels for header in headers)) + metric_labels = set(itertools.chain.from_iterable( + header.metric.labels for header in headers)) + labels = (['resource_type'] + + _sorted_resource_labels(resource_labels) + + sorted(metric_labels)) + + # Assemble the columns into a DataFrame. + dataframe = pandas.DataFrame.from_records(columns).T + + # Convert the timestamp strings into a DatetimeIndex. + dataframe.index = pandas.to_datetime(dataframe.index) + + # Build a multi-level stack of column headers. Some labels may + # be undefined for some time series. + levels = [] + for key in labels or [label]: + level = [header.labels.get(key, '') for header in headers] + levels.append(level) + + # Build a column Index or MultiIndex. Do not include level names + # in the column header if the user requested a single-level header + # by specifying "label". + dataframe.columns = pandas.MultiIndex.from_arrays( + levels, + names=labels or None) + + # Sort the rows just in case (since the API doesn't guarantee the + # ordering), and sort the columns lexicographically. + return dataframe.sort_index(axis=0).sort_index(axis=1) + + +def _sorted_resource_labels(labels): + """Sort label names, putting well-known resource labels first.""" + head = [label for label in TOP_RESOURCE_LABELS if label in labels] + tail = sorted(label for label in labels + if label not in TOP_RESOURCE_LABELS) + return head + tail diff --git a/gcloud/monitoring/client.py b/gcloud/monitoring/client.py new file mode 100644 index 000000000000..633740a36cb1 --- /dev/null +++ b/gcloud/monitoring/client.py @@ -0,0 +1,192 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Client for interacting with the `Google Monitoring API (V3)`_. + +Example:: + + >>> from gcloud import monitoring + >>> client = monitoring.Client() + >>> query = client.query(minutes=5) + >>> print(query.as_dataframe()) # Requires pandas. + +At present, the client supports querying of time series, metric descriptors, +and monitored resource descriptors. + +.. _Google Monitoring API (V3): https://cloud.google.com/monitoring/api/ +""" + +from gcloud.client import JSONClient +from gcloud.monitoring.connection import Connection +from gcloud.monitoring.metric import MetricDescriptor +from gcloud.monitoring.query import Query +from gcloud.monitoring.resource import ResourceDescriptor + + +class Client(JSONClient): + """Client to bundle configuration needed for API requests. + + :type project: string + :param project: The target project. If not passed, falls back to the + default inferred from the environment. + + :type credentials: :class:`oauth2client.client.OAuth2Credentials` or + :class:`NoneType` + :param credentials: The OAuth2 Credentials to use for the connection + owned by this client. If not passed (and if no ``http`` + object is passed), falls back to the default inferred + from the environment. + + :type http: :class:`httplib2.Http` or class that defines ``request()`` + :param http: An optional HTTP object to make requests. If not passed, an + ``http`` object is created that is bound to the + ``credentials`` for the current object. + """ + + _connection_class = Connection + + def query(self, + metric_type=Query.DEFAULT_METRIC_TYPE, + end_time=None, + days=0, hours=0, minutes=0): + """Construct a query object for listing time series. + + Example:: + + >>> query = client.query(minutes=5) + >>> print(query.as_dataframe()) # Requires pandas. + + :type metric_type: string + :param metric_type: The metric type name. The default value is + :data:`Query.DEFAULT_METRIC_TYPE + `, + but please note that this default value is provided only for + demonstration purposes and is subject to change. See the + `supported metrics`_. + + :type end_time: :class:`datetime.datetime` or None + :param end_time: The end time (inclusive) of the time interval + for which results should be returned, as a datetime object. + The default is the start of the current minute. + + The start time (exclusive) is determined by combining the + values of ``days``, ``hours``, and ``minutes``, and + subtracting the resulting duration from the end time. + + It is also allowed to omit the end time and duration here, + in which case + :meth:`~gcloud.monitoring.query.Query.select_interval` + must be called before the query is executed. + + :type days: integer + :param days: The number of days in the time interval. + + :type hours: integer + :param hours: The number of hours in the time interval. + + :type minutes: integer + :param minutes: The number of minutes in the time interval. + + :rtype: :class:`~gcloud.monitoring.query.Query` + :returns: The query object. + + :raises: :exc:`ValueError` if ``end_time`` is specified but + ``days``, ``hours``, and ``minutes`` are all zero. + If you really want to specify a point in time, use + :meth:`~gcloud.monitoring.query.Query.select_interval`. + + .. _supported metrics: https://cloud.google.com/monitoring/api/metrics + """ + return Query(self, metric_type, + end_time=end_time, + days=days, hours=hours, minutes=minutes) + + def fetch_metric_descriptor(self, metric_type): + """Look up a metric descriptor by type. + + Example:: + + >>> METRIC = 'compute.googleapis.com/instance/cpu/utilization' + >>> print(client.fetch_metric_descriptor(METRIC)) + + :type metric_type: string + :param metric_type: The metric type name. + + :rtype: :class:`~gcloud.monitoring.metric.MetricDescriptor` + :returns: The metric descriptor instance. + + :raises: :class:`gcloud.exceptions.NotFound` if the metric descriptor + is not found. + """ + return MetricDescriptor._fetch(self, metric_type) + + def list_metric_descriptors(self, filter_string=None): + """List all metric descriptors for the project. + + Example:: + + >>> for descriptor in client.list_metric_descriptors(): + ... print(descriptor.type) + + :type filter_string: string or None + :param filter_string: + An optional filter expression describing the metric descriptors + to be returned. See the `filter documentation`_. + + :rtype: list of :class:`~gcloud.monitoring.metric.MetricDescriptor` + :returns: A list of metric descriptor instances. + + .. _filter documentation: + https://cloud.google.com/monitoring/api/v3/filters + """ + return MetricDescriptor._list(self, filter_string) + + def fetch_resource_descriptor(self, resource_type): + """Look up a resource descriptor by type. + + Example:: + + >>> print(client.fetch_resource_descriptor('gce_instance')) + + :type resource_type: string + :param resource_type: The resource type name. + + :rtype: :class:`~gcloud.monitoring.resource.ResourceDescriptor` + :returns: The resource descriptor instance. + + :raises: :class:`gcloud.exceptions.NotFound` if the resource descriptor + is not found. + """ + return ResourceDescriptor._fetch(self, resource_type) + + def list_resource_descriptors(self, filter_string=None): + """List all resource descriptors for the project. + + Example:: + + >>> for descriptor in client.list_resource_descriptors(): + ... print(descriptor.type) + + :type filter_string: string or None + :param filter_string: + An optional filter expression describing the resource descriptors + to be returned. See the `filter documentation`_. + + :rtype: list of :class:`~gcloud.monitoring.resource.ResourceDescriptor` + :returns: A list of resource descriptor instances. + + .. _filter documentation: + https://cloud.google.com/monitoring/api/v3/filters + """ + return ResourceDescriptor._list(self, filter_string) diff --git a/gcloud/monitoring/connection.py b/gcloud/monitoring/connection.py new file mode 100644 index 000000000000..5887da62e65d --- /dev/null +++ b/gcloud/monitoring/connection.py @@ -0,0 +1,47 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Create / interact with gcloud monitoring connections.""" + +from gcloud import connection as base_connection + + +class Connection(base_connection.JSONConnection): + """A connection to Google Monitoring via the JSON REST API. + + :type credentials: :class:`oauth2client.client.OAuth2Credentials` + :param credentials: (Optional) The OAuth2 Credentials to use for this + connection. + + :type http: :class:`httplib2.Http` or class that defines ``request()`` + :param http: (Optional) HTTP object to make requests. + + :type api_base_url: string + :param api_base_url: The base of the API call URL. Defaults to the value + :attr:`Connection.API_BASE_URL`. + """ + + API_BASE_URL = 'https://monitoring.googleapis.com' + """The base of the API call URL.""" + + API_VERSION = 'v3' + """The version of the API, used in building the API call's URL.""" + + API_URL_TEMPLATE = '{api_base_url}/{api_version}{path}' + """A template for the URL of a particular API call.""" + + SCOPE = ('https://www.googleapis.com/auth/monitoring.read', + 'https://www.googleapis.com/auth/monitoring', + 'https://www.googleapis.com/auth/cloud-platform') + """The scopes required for authenticating as a Monitoring consumer.""" diff --git a/gcloud/monitoring/label.py b/gcloud/monitoring/label.py new file mode 100644 index 000000000000..d2f7c92201af --- /dev/null +++ b/gcloud/monitoring/label.py @@ -0,0 +1,77 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Label Descriptors for the `Google Monitoring API (V3)`_. + +.. _Google Monitoring API (V3): + https://cloud.google.com/monitoring/api/ref_v3/rest/v3/LabelDescriptor +""" + + +class LabelValueType(object): + """Allowed values for the `type of a label`_. + + .. _type of a label: + https://cloud.google.com/monitoring/api/ref_v3/rest/v3/\ + LabelDescriptor#ValueType + """ + + STRING = 'STRING' + BOOL = 'BOOL' + INT64 = 'INT64' + + +class LabelDescriptor(object): + """Schema specification and documentation for a single label. + + :type key: string + :param key: The name of the label. + + :type value_type: string + :param value_type: + The type of the label. It must be one of :data:`LabelValueType.STRING`, + :data:`LabelValueType.BOOL`, or :data:`LabelValueType.INT64`. + See :class:`LabelValueType`. + + :type description: string + :param description: A human-readable description for the label. + """ + + def __init__(self, key, value_type, description): + self.key = key + self.value_type = value_type + self.description = description + + @classmethod + def _from_dict(cls, info): + """Construct a label descriptor from the parsed JSON representation. + + :type info: dict + :param info: + A ``dict`` parsed from the JSON wire-format representation. + + :rtype: :class:`LabelDescriptor` + :returns: A label descriptor. + """ + return cls( + info['key'], + info.get('valueType', LabelValueType.STRING), + info.get('description', ''), + ) + + def __repr__(self): + return ( + 'LabelDescriptor(key={key!r}, value_type={value_type!r},' + ' description={description!r})' + ).format(**self.__dict__) diff --git a/gcloud/monitoring/metric.py b/gcloud/monitoring/metric.py new file mode 100644 index 000000000000..a6c3f3eae82d --- /dev/null +++ b/gcloud/monitoring/metric.py @@ -0,0 +1,239 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Metric Descriptors for the `Google Monitoring API (V3)`_. + +Features intentionally omitted from this first version of the client library: + * Creating and deleting metric descriptors. + +.. _Google Monitoring API (V3): + https://cloud.google.com/monitoring/api/ref_v3/rest/v3/\ + projects.metricDescriptors +""" + +import collections + +from gcloud.monitoring.label import LabelDescriptor + + +class MetricKind(object): + """Allowed values for the `kind of measurement`_. + + .. _kind of measurement: + https://cloud.google.com/monitoring/api/ref_v3/rest/v3/\ + projects.metricDescriptors#MetricKind + """ + + GAUGE = 'GAUGE' + DELTA = 'DELTA' + CUMULATIVE = 'CUMULATIVE' + + +class ValueType(object): + """Allowed values for the `metric value type`_. + + .. _metric value type: + https://cloud.google.com/monitoring/api/ref_v3/rest/v3/\ + projects.metricDescriptors#ValueType + """ + + BOOL = 'BOOL' + INT64 = 'INT64' + DOUBLE = 'DOUBLE' + STRING = 'STRING' + DISTRIBUTION = 'DISTRIBUTION' + MONEY = 'MONEY' + + +class MetricDescriptor(object): + """Specification of a metric type and its schema. + + :type name: string + :param name: + The "resource name" of the metric descriptor. For example: + ``"projects//metricDescriptors/"`` + + :type type_: string + :param type_: + The metric type including a DNS name prefix. For example: + ``"compute.googleapis.com/instance/cpu/utilization"`` + + :type labels: list of :class:`~gcloud.monitoring.label.LabelDescriptor` + :param labels: + A sequence of label descriptors specifying the labels used to + identify a specific instance of this metric. + + :type metric_kind: string + :param metric_kind: + The kind of measurement. It must be one of :data:`MetricKind.GAUGE`, + :data:`MetricKind.DELTA`, or :data:`MetricKind.CUMULATIVE`. + See :class:`MetricKind`. + + :type value_type: string + :param value_type: + The value type of the metric. It must be one of :data:`ValueType.BOOL`, + :data:`ValueType.INT64`, :data:`ValueType.DOUBLE`, + :data:`ValueType.STRING`, :data:`ValueType.DISTRIBUTION`, + or :data:`ValueType.MONEY`. + See :class:`ValueType`. + + :type unit: string + :param unit: The unit in which the metric value is reported. + + :type description: string + :param description: A detailed description of the metric. + + :type display_name: string + :param display_name: A concise name for the metric. + """ + + def __init__(self, name, type_, labels, metric_kind, value_type, + unit, description, display_name): + self.name = name + self.type = type_ + self.labels = labels + self.metric_kind = metric_kind + self.value_type = value_type + self.unit = unit + self.description = description + self.display_name = display_name + + @classmethod + def _fetch(cls, client, metric_type): + """Look up a metric descriptor by type. + + :type client: :class:`gcloud.monitoring.client.Client` + :param client: The client to use. + + :type metric_type: string + :param metric_type: The metric type name. + + :rtype: :class:`MetricDescriptor` + :returns: The metric descriptor instance. + + :raises: :class:`gcloud.exceptions.NotFound` if the metric descriptor + is not found. + """ + path = '/projects/{project}/metricDescriptors/{type}'.format( + project=client.project, + type=metric_type) + info = client.connection.api_request(method='GET', path=path) + return cls._from_dict(info) + + @classmethod + def _list(cls, client, filter_string=None): + """List all metric descriptors for the project. + + :type client: :class:`gcloud.monitoring.client.Client` + :param client: The client to use. + + :type filter_string: string or None + :param filter_string: + An optional filter expression describing the metric descriptors + to be returned. See the `filter documentation`_. + + :rtype: list of :class:`MetricDescriptor` + :returns: A list of metric descriptor instances. + + .. _filter documentation: + https://cloud.google.com/monitoring/api/v3/filters + """ + path = '/projects/{project}/metricDescriptors/'.format( + project=client.project) + + descriptors = [] + + page_token = None + while True: + params = {} + + if filter_string is not None: + params['filter'] = filter_string + + if page_token is not None: + params['pageToken'] = page_token + + response = client.connection.api_request( + method='GET', path=path, query_params=params) + for info in response.get('metricDescriptors', ()): + descriptors.append(cls._from_dict(info)) + + page_token = response.get('nextPageToken') + if not page_token: + break + + return descriptors + + @classmethod + def _from_dict(cls, info): + """Construct a metric descriptor from the parsed JSON representation. + + :type info: dict + :param info: + A ``dict`` parsed from the JSON wire-format representation. + + :rtype: :class:`MetricDescriptor` + :returns: A metric descriptor. + """ + return cls( + name=info['name'], + type_=info['type'], + labels=tuple(LabelDescriptor._from_dict(label) + for label in info.get('labels', ())), + metric_kind=info['metricKind'], + value_type=info['valueType'], + unit=info.get('unit', ''), + description=info.get('description', ''), + display_name=info.get('displayName', ''), + ) + + def __repr__(self): + return ( + 'MetricDescriptor(\n' + ' name={name!r},\n' + ' type={type!r},\n' + ' metric_kind={metric_kind!r}, value_type={value_type!r},\n' + ' labels={labels!r},\n' + ' display_name={display_name!r}, unit={unit!r},\n' + ' description={description!r})' + ).format(**self.__dict__) + + +class Metric(collections.namedtuple('Metric', 'type labels')): + """A specific metric identified by specifying values for all labels. + + :type type: string + :param type: The metric type name. + + :type labels: dict + :param labels: A mapping from label names to values for all labels + enumerated in the associated :class:`MetricDescriptor`. + """ + __slots__ = () + + @classmethod + def _from_dict(cls, info): + """Construct a metric object from the parsed JSON representation. + + :type info: dict + :param info: + A ``dict`` parsed from the JSON wire-format representation. + + :rtype: :class:`Metric` + :returns: A metric object. + """ + return cls( + type=info['type'], + labels=info.get('labels', {}), + ) diff --git a/gcloud/monitoring/query.py b/gcloud/monitoring/query.py new file mode 100644 index 000000000000..b8c529f50ae1 --- /dev/null +++ b/gcloud/monitoring/query.py @@ -0,0 +1,668 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Time series query for the `Google Monitoring API (V3)`_. + +.. _Google Monitoring API (V3): + https://cloud.google.com/monitoring/api/ref_v3/rest/v3/\ + projects.timeSeries/list +""" + +import copy +import datetime +import itertools + +import six + +from gcloud.monitoring._dataframe import _build_dataframe +from gcloud.monitoring.timeseries import TimeSeries + +_UTCNOW = datetime.datetime.utcnow # To be replaced by tests. + + +class Aligner(object): + """Allowed values for the `supported aligners`_.""" + + ALIGN_NONE = 'ALIGN_NONE' + ALIGN_DELTA = 'ALIGN_DELTA' + ALIGN_RATE = 'ALIGN_RATE' + ALIGN_INTERPOLATE = 'ALIGN_INTERPOLATE' + ALIGN_NEXT_OLDER = 'ALIGN_NEXT_OLDER' + ALIGN_MIN = 'ALIGN_MIN' + ALIGN_MAX = 'ALIGN_MAX' + ALIGN_MEAN = 'ALIGN_MEAN' + ALIGN_COUNT = 'ALIGN_COUNT' + ALIGN_SUM = 'ALIGN_SUM' + ALIGN_STDDEV = 'ALIGN_STDDEV' + ALIGN_COUNT_TRUE = 'ALIGN_COUNT_TRUE' + ALIGN_FRACTION_TRUE = 'ALIGN_FRACTION_TRUE' + + +class Reducer(object): + """Allowed values for the `supported reducers`_.""" + + REDUCE_NONE = 'REDUCE_NONE' + REDUCE_MEAN = 'REDUCE_MEAN' + REDUCE_MIN = 'REDUCE_MIN' + REDUCE_MAX = 'REDUCE_MAX' + REDUCE_SUM = 'REDUCE_SUM' + REDUCE_STDDEV = 'REDUCE_STDDEV' + REDUCE_COUNT = 'REDUCE_COUNT' + REDUCE_COUNT_TRUE = 'REDUCE_COUNT_TRUE' + REDUCE_FRACTION_TRUE = 'REDUCE_FRACTION_TRUE' + REDUCE_PERCENTILE_99 = 'REDUCE_PERCENTILE_99' + REDUCE_PERCENTILE_95 = 'REDUCE_PERCENTILE_95' + REDUCE_PERCENTILE_50 = 'REDUCE_PERCENTILE_50' + REDUCE_PERCENTILE_05 = 'REDUCE_PERCENTILE_05' + + +class Query(object): + """Query object for listing time series. + + The preferred way to construct a query object is using the + :meth:`~gcloud.monitoring.client.Client.query` method + of the :class:`~gcloud.monitoring.client.Client` class. + + :type client: :class:`gcloud.monitoring.client.Client` + :param client: The client to use. + + :type metric_type: string + :param metric_type: The metric type name. The default value is + :data:`Query.DEFAULT_METRIC_TYPE + `, + but please note that this default value is provided only for + demonstration purposes and is subject to change. See the + `supported metrics`_. + + :type end_time: :class:`datetime.datetime` or None + :param end_time: The end time (inclusive) of the time interval + for which results should be returned, as a datetime object. + The default is the start of the current minute. + + The start time (exclusive) is determined by combining the + values of ``days``, ``hours``, and ``minutes``, and + subtracting the resulting duration from the end time. + + It is also allowed to omit the end time and duration here, + in which case + :meth:`~gcloud.monitoring.query.Query.select_interval` + must be called before the query is executed. + + :type days: integer + :param days: The number of days in the time interval. + + :type hours: integer + :param hours: The number of hours in the time interval. + + :type minutes: integer + :param minutes: The number of minutes in the time interval. + + :raises: :exc:`ValueError` if ``end_time`` is specified but + ``days``, ``hours``, and ``minutes`` are all zero. + If you really want to specify a point in time, use + :meth:`~gcloud.monitoring.query.Query.select_interval`. + + .. _supported metrics: https://cloud.google.com/monitoring/api/metrics + """ + + DEFAULT_METRIC_TYPE = 'compute.googleapis.com/instance/cpu/utilization' + + def __init__(self, client, + metric_type=DEFAULT_METRIC_TYPE, + end_time=None, days=0, hours=0, minutes=0): + start_time = None + if days or hours or minutes: + if end_time is None: + end_time = _UTCNOW().replace(second=0, microsecond=0) + start_time = end_time - datetime.timedelta(days=days, + hours=hours, + minutes=minutes) + elif end_time is not None: + raise ValueError('Non-zero duration required for time interval.') + + self._client = client + self._end_time = end_time + self._start_time = start_time + self._filter = _Filter(metric_type) + + self._per_series_aligner = None + self._alignment_period_seconds = None + self._cross_series_reducer = None + self._group_by_fields = () + + def __iter__(self): + return self.iter() + + @property + def metric_type(self): + """The metric type name.""" + return self._filter.metric_type + + @property + def filter(self): + """The filter string. + + This is constructed from the metric type, the resource type, and + selectors for the group ID, monitored projects, resource labels, + and metric labels. + """ + return str(self._filter) + + def select_interval(self, end_time, start_time=None): + """Copy the query and set the query time interval. + + Example:: + + import datetime + + now = datetime.datetime.utcnow() + query = query.select_interval( + end_time=now, + start_time=now - datetime.timedelta(minutes=5)) + + As a convenience, you can alternatively specify the end time and + an interval duration when you create the query initially. + + :type end_time: :class:`datetime.datetime` + :param end_time: The end time (inclusive) of the time interval + for which results should be returned, as a datetime object. + + :type start_time: :class:`datetime.datetime` or None + :param start_time: The start time (exclusive) of the time interval + for which results should be returned, as a datetime object. + If not specified, the interval is a point in time. + + :rtype: :class:`Query` + :returns: The new query object. + """ + new_query = self.copy() + new_query._end_time = end_time + new_query._start_time = start_time + return new_query + + def select_group(self, group_id): + """Copy the query and add filtering by group. + + Example:: + + query = query.select_group('1234567') + + :type group_id: string + :param group_id: The ID of a group to filter by. + + :rtype: :class:`Query` + :returns: The new query object. + """ + new_query = self.copy() + new_query._filter.group_id = group_id + return new_query + + def select_projects(self, *args): + """Copy the query and add filtering by monitored projects. + + This is only useful if the target project represents a Stackdriver + account containing the specified monitored projects. + + Examples:: + + query = query.select_projects('project-1') + query = query.select_projects('project-1', 'project-2') + + :param args: Project IDs limiting the resources to be included + in the query. + + :rtype: :class:`Query` + :returns: The new query object. + """ + new_query = self.copy() + new_query._filter.projects = args + return new_query + + def select_resources(self, *args, **kwargs): + """Copy the query and add filtering by resource labels. + + Examples:: + + query = query.select_resources(zone='us-central1-a') + query = query.select_resources(zone_prefix='europe-') + query = query.select_resources(resource_type='gce_instance') + + A keyword argument ``