Skip to content

Commit 768dc2c

Browse files
AndrewAXuec24ttoumorokoshi
authored and
Alex Boten
committed
cloud-monitor: Add cloud monitoring exporter (open-telemetry#739)
Adding an exporter that supports sending metrics data to cloud monitoring (formerly known as stackdriver). Co-authored-by: Chris Kleinknecht <[email protected]> Co-authored-by: Yusuke Tsutsumi <[email protected]>
1 parent 5a29964 commit 768dc2c

File tree

11 files changed

+654
-0
lines changed

11 files changed

+654
-0
lines changed

docs-requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ wrapt>=1.0.0,<2.0.0
2222
psutil~=5.7.0
2323
boto~=2.0
2424
google-cloud-trace >=0.23.0
25+
google-cloud-monitoring>=0.36.0
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
Cloud Monitoring Exporter Example
2+
=================================
3+
4+
These examples show how to use OpenTelemetry to send metrics data to Cloud Monitoring.
5+
6+
7+
Basic Example
8+
-------------
9+
10+
To use this exporter you first need to:
11+
* `Create a Google Cloud project <https://console.cloud.google.com/projectcreate>`_.
12+
* Enable the Cloud Monitoring API (aka Stackdriver Monitoring API) in the project `here <https://console.cloud.google.com/apis/library?q=cloud_monitoring>`_.
13+
* Enable `Default Application Credentials <https://developers.google.com/identity/protocols/application-default-credentials>`_.
14+
15+
* Installation
16+
17+
.. code-block:: sh
18+
19+
pip install opentelemetry-api
20+
pip install opentelemetry-sdk
21+
pip install opentelemetry-exporter-cloud-monitoring
22+
23+
* Run example
24+
25+
.. code-block:: sh
26+
27+
python basic_metrics.py
28+
29+
Viewing Output
30+
--------------------------
31+
32+
After running the example:
33+
* Go to the `Cloud Monitoring Metrics Explorer page <https://console.cloud.google.com/monitoring/metrics-explorer>`_.
34+
* In "Find resource type and metric" enter "OpenTelemetry/request_counter".
35+
* You can filter by labels and change the graphical output here as well.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env python3
2+
# Copyright The OpenTelemetry Authors
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import time
17+
18+
from opentelemetry import metrics
19+
from opentelemetry.exporter.cloud_monitoring import (
20+
CloudMonitoringMetricsExporter,
21+
)
22+
from opentelemetry.sdk.metrics import Counter, MeterProvider
23+
from opentelemetry.sdk.metrics.export.controller import PushController
24+
25+
meter = metrics.get_meter(__name__, True)
26+
27+
# Gather and export metrics every 5 seconds
28+
controller = PushController(
29+
meter=meter, exporter=CloudMonitoringMetricsExporter(), interval=5
30+
)
31+
32+
requests_counter = meter.create_metric(
33+
name="request_counter",
34+
description="number of requests",
35+
unit="1",
36+
value_type=int,
37+
metric_type=Counter,
38+
label_keys=("environment"),
39+
)
40+
41+
staging_labels = {"environment": "staging"}
42+
43+
for i in range(20):
44+
requests_counter.add(25, staging_labels)
45+
time.sleep(10)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
OpenTelemetry Cloud Monitoring Exporter
2+
=======================================
3+
4+
.. automodule:: opentelemetry.exporter.cloud_monitoring
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
OpenTelemetry Cloud Monitoring Exporters
2+
========================================
3+
4+
This library provides classes for exporting metrics data to Google Cloud Monitoring.
5+
6+
Installation
7+
------------
8+
9+
::
10+
11+
pip install opentelemetry-exporter-cloud-monitoring
12+
13+
References
14+
----------
15+
16+
* `OpenTelemetry Cloud Monitoring Exporter <https://opentelemetry-python.readthedocs.io/en/latest/ext/cloud_monitoring/cloud_monitoring.html>`_
17+
* `Cloud Monitoring <https://cloud.google.com/monitoring/>`_
18+
* `OpenTelemetry Project <https://opentelemetry.io/>`_
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
2+
# Copyright OpenTelemetry Authors
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
[metadata]
17+
name = opentelemetry-exporter-cloud-monitoring
18+
description = Cloud Monitoring integration for OpenTelemetry
19+
long_description = file: README.rst
20+
long_description_content_type = text/x-rst
21+
author = OpenTelemetry Authors
22+
author_email = [email protected]
23+
url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-exporter-cloud-monitoring
24+
platforms = any
25+
license = Apache-2.0
26+
classifiers =
27+
Development Status :: 4 - Beta
28+
Intended Audience :: Developers
29+
License :: OSI Approved :: Apache Software License
30+
Programming Language :: Python
31+
Programming Language :: Python :: 3
32+
Programming Language :: Python :: 3.4
33+
Programming Language :: Python :: 3.5
34+
Programming Language :: Python :: 3.6
35+
Programming Language :: Python :: 3.7
36+
37+
[options]
38+
python_requires = >=3.4
39+
package_dir=
40+
=src
41+
packages=find_namespace:
42+
install_requires =
43+
opentelemetry-api
44+
opentelemetry-sdk
45+
google-cloud-monitoring
46+
47+
[options.packages.find]
48+
where = src
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import os
15+
16+
import setuptools
17+
18+
BASE_DIR = os.path.dirname(__file__)
19+
VERSION_FILENAME = os.path.join(
20+
BASE_DIR,
21+
"src",
22+
"opentelemetry",
23+
"exporter",
24+
"cloud_monitoring",
25+
"version.py",
26+
)
27+
PACKAGE_INFO = {}
28+
with open(VERSION_FILENAME) as f:
29+
exec(f.read(), PACKAGE_INFO)
30+
31+
setuptools.setup(version=PACKAGE_INFO["__version__"])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import logging
2+
from typing import Optional, Sequence
3+
4+
import google.auth
5+
from google.api.label_pb2 import LabelDescriptor
6+
from google.api.metric_pb2 import MetricDescriptor
7+
from google.cloud.monitoring_v3 import MetricServiceClient
8+
from google.cloud.monitoring_v3.proto.metric_pb2 import TimeSeries
9+
10+
from opentelemetry.sdk.metrics.export import (
11+
MetricRecord,
12+
MetricsExporter,
13+
MetricsExportResult,
14+
)
15+
from opentelemetry.sdk.metrics.export.aggregate import CounterAggregator
16+
17+
logger = logging.getLogger(__name__)
18+
MAX_BATCH_WRITE = 200
19+
WRITE_INTERVAL = 10
20+
21+
22+
# pylint is unable to resolve members of protobuf objects
23+
# pylint: disable=no-member
24+
class CloudMonitoringMetricsExporter(MetricsExporter):
25+
""" Implementation of Metrics Exporter to Google Cloud Monitoring"""
26+
27+
def __init__(self, project_id=None, client=None):
28+
self.client = client or MetricServiceClient()
29+
if not project_id:
30+
_, self.project_id = google.auth.default()
31+
else:
32+
self.project_id = project_id
33+
self.project_name = self.client.project_path(self.project_id)
34+
self._metric_descriptors = {}
35+
self._last_updated = {}
36+
37+
def _add_resource_info(self, series: TimeSeries) -> None:
38+
"""Add Google resource specific information (e.g. instance id, region).
39+
40+
Args:
41+
series: ProtoBuf TimeSeries
42+
"""
43+
# TODO: Leverage this better
44+
45+
def _batch_write(self, series: TimeSeries) -> None:
46+
""" Cloud Monitoring allows writing up to 200 time series at once
47+
48+
:param series: ProtoBuf TimeSeries
49+
:return:
50+
"""
51+
write_ind = 0
52+
while write_ind < len(series):
53+
self.client.create_time_series(
54+
self.project_name,
55+
series[write_ind : write_ind + MAX_BATCH_WRITE],
56+
)
57+
write_ind += MAX_BATCH_WRITE
58+
59+
def _get_metric_descriptor(
60+
self, record: MetricRecord
61+
) -> Optional[MetricDescriptor]:
62+
""" We can map Metric to MetricDescriptor using Metric.name or
63+
MetricDescriptor.type. We create the MetricDescriptor if it doesn't
64+
exist already and cache it. Note that recreating MetricDescriptors is
65+
a no-op if it already exists.
66+
67+
:param record:
68+
:return:
69+
"""
70+
descriptor_type = "custom.googleapis.com/OpenTelemetry/{}".format(
71+
record.metric.name
72+
)
73+
if descriptor_type in self._metric_descriptors:
74+
return self._metric_descriptors[descriptor_type]
75+
descriptor = {
76+
"name": None,
77+
"type": descriptor_type,
78+
"display_name": record.metric.name,
79+
"description": record.metric.description,
80+
"labels": [],
81+
}
82+
for key, value in record.labels:
83+
if isinstance(value, str):
84+
descriptor["labels"].append(
85+
LabelDescriptor(key=key, value_type="STRING")
86+
)
87+
elif isinstance(value, bool):
88+
descriptor["labels"].append(
89+
LabelDescriptor(key=key, value_type="BOOL")
90+
)
91+
elif isinstance(value, int):
92+
descriptor["labels"].append(
93+
LabelDescriptor(key=key, value_type="INT64")
94+
)
95+
else:
96+
logger.warning(
97+
"Label value %s is not a string, bool or integer", value
98+
)
99+
if isinstance(record.aggregator, CounterAggregator):
100+
descriptor["metric_kind"] = MetricDescriptor.MetricKind.GAUGE
101+
else:
102+
logger.warning(
103+
"Unsupported aggregation type %s, ignoring it",
104+
type(record.aggregator).__name__,
105+
)
106+
return None
107+
if record.metric.value_type == int:
108+
descriptor["value_type"] = MetricDescriptor.ValueType.INT64
109+
elif record.metric.value_type == float:
110+
descriptor["value_type"] = MetricDescriptor.ValueType.DOUBLE
111+
proto_descriptor = MetricDescriptor(**descriptor)
112+
try:
113+
descriptor = self.client.create_metric_descriptor(
114+
self.project_name, proto_descriptor
115+
)
116+
# pylint: disable=broad-except
117+
except Exception as ex:
118+
logger.error(
119+
"Failed to create metric descriptor %s",
120+
proto_descriptor,
121+
exc_info=ex,
122+
)
123+
return None
124+
self._metric_descriptors[descriptor_type] = descriptor
125+
return descriptor
126+
127+
def export(
128+
self, metric_records: Sequence[MetricRecord]
129+
) -> "MetricsExportResult":
130+
all_series = []
131+
for record in metric_records:
132+
metric_descriptor = self._get_metric_descriptor(record)
133+
if not metric_descriptor:
134+
continue
135+
136+
series = TimeSeries()
137+
self._add_resource_info(series)
138+
series.metric.type = metric_descriptor.type
139+
for key, value in record.labels:
140+
series.metric.labels[key] = str(value)
141+
142+
point = series.points.add()
143+
if record.metric.value_type == int:
144+
point.value.int64_value = record.aggregator.checkpoint
145+
elif record.metric.value_type == float:
146+
point.value.double_value = record.aggregator.checkpoint
147+
seconds, nanos = divmod(
148+
record.aggregator.last_update_timestamp, 1e9
149+
)
150+
151+
# Cloud Monitoring API allows, for any combination of labels and
152+
# metric name, one update per WRITE_INTERVAL seconds
153+
updated_key = (metric_descriptor.type, record.labels)
154+
last_updated_seconds = self._last_updated.get(updated_key, 0)
155+
if seconds <= last_updated_seconds + WRITE_INTERVAL:
156+
continue
157+
self._last_updated[updated_key] = seconds
158+
point.interval.end_time.seconds = int(seconds)
159+
point.interval.end_time.nanos = int(nanos)
160+
all_series.append(series)
161+
try:
162+
self._batch_write(all_series)
163+
# pylint: disable=broad-except
164+
except Exception as ex:
165+
logger.error(
166+
"Error while writing to Cloud Monitoring", exc_info=ex
167+
)
168+
return MetricsExportResult.FAILURE
169+
return MetricsExportResult.SUCCESS
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
__version__ = "0.9.dev0"

ext/opentelemetry-exporter-cloud-monitoring/tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)