Skip to content

Commit 2b0877c

Browse files
committed
0 parents  commit 2b0877c

18 files changed

Lines changed: 1927 additions & 0 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Changelog
2+
3+
## Unreleased
4+
5+
## Version 0.10b0
6+
7+
Released 2020-06-23
8+
9+
- Add ability for exporter to add unique identifier
10+
([#841](https://github.com/open-telemetry/opentelemetry-python/pull/841))
11+
- Added tests to tox coverage files
12+
([#804](https://github.com/open-telemetry/opentelemetry-python/pull/804))
13+
14+
## 0.9b0
15+
16+
Released 2020-06-10
17+
18+
- Initial release
Lines changed: 18 additions & 0 deletions
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/>`_
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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 = cncf-opentelemetry-contributors@lists.cncf.io
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
49+
50+
[options.extras_require]
51+
test =
Lines changed: 31 additions & 0 deletions
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__"])
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import logging
2+
import random
3+
from typing import Optional, Sequence
4+
5+
import google.auth
6+
from google.api.label_pb2 import LabelDescriptor
7+
from google.api.metric_pb2 import MetricDescriptor
8+
from google.cloud.monitoring_v3 import MetricServiceClient
9+
from google.cloud.monitoring_v3.proto.metric_pb2 import TimeSeries
10+
11+
from opentelemetry.sdk.metrics.export import (
12+
MetricRecord,
13+
MetricsExporter,
14+
MetricsExportResult,
15+
)
16+
from opentelemetry.sdk.metrics.export.aggregate import SumAggregator
17+
18+
logger = logging.getLogger(__name__)
19+
MAX_BATCH_WRITE = 200
20+
WRITE_INTERVAL = 10
21+
UNIQUE_IDENTIFIER_KEY = "opentelemetry_id"
22+
23+
24+
# pylint is unable to resolve members of protobuf objects
25+
# pylint: disable=no-member
26+
class CloudMonitoringMetricsExporter(MetricsExporter):
27+
""" Implementation of Metrics Exporter to Google Cloud Monitoring
28+
29+
You can manually pass in project_id and client, or else the
30+
Exporter will take that information from Application Default
31+
Credentials.
32+
33+
Args:
34+
project_id: project id of your Google Cloud project.
35+
client: Client to upload metrics to Google Cloud Monitoring.
36+
add_unique_identifier: Add an identifier to each exporter metric. This
37+
must be used when there exist two (or more) exporters that may
38+
export to the same metric name within WRITE_INTERVAL seconds of
39+
each other.
40+
"""
41+
42+
def __init__(
43+
self, project_id=None, client=None, add_unique_identifier=False
44+
):
45+
self.client = client or MetricServiceClient()
46+
if not project_id:
47+
_, self.project_id = google.auth.default()
48+
else:
49+
self.project_id = project_id
50+
self.project_name = self.client.project_path(self.project_id)
51+
self._metric_descriptors = {}
52+
self._last_updated = {}
53+
self.unique_identifier = None
54+
if add_unique_identifier:
55+
self.unique_identifier = "{:08x}".format(
56+
random.randint(0, 16 ** 8)
57+
)
58+
59+
def _add_resource_info(self, series: TimeSeries) -> None:
60+
"""Add Google resource specific information (e.g. instance id, region).
61+
62+
Args:
63+
series: ProtoBuf TimeSeries
64+
"""
65+
# TODO: Leverage this better
66+
67+
def _batch_write(self, series: TimeSeries) -> None:
68+
""" Cloud Monitoring allows writing up to 200 time series at once
69+
70+
:param series: ProtoBuf TimeSeries
71+
:return:
72+
"""
73+
write_ind = 0
74+
while write_ind < len(series):
75+
self.client.create_time_series(
76+
self.project_name,
77+
series[write_ind : write_ind + MAX_BATCH_WRITE],
78+
)
79+
write_ind += MAX_BATCH_WRITE
80+
81+
def _get_metric_descriptor(
82+
self, record: MetricRecord
83+
) -> Optional[MetricDescriptor]:
84+
""" We can map Metric to MetricDescriptor using Metric.name or
85+
MetricDescriptor.type. We create the MetricDescriptor if it doesn't
86+
exist already and cache it. Note that recreating MetricDescriptors is
87+
a no-op if it already exists.
88+
89+
:param record:
90+
:return:
91+
"""
92+
instrument = record.instrument
93+
descriptor_type = "custom.googleapis.com/OpenTelemetry/{}".format(
94+
instrument.name
95+
)
96+
if descriptor_type in self._metric_descriptors:
97+
return self._metric_descriptors[descriptor_type]
98+
descriptor = {
99+
"name": None,
100+
"type": descriptor_type,
101+
"display_name": instrument.name,
102+
"description": instrument.description,
103+
"labels": [],
104+
}
105+
for key, value in record.labels:
106+
if isinstance(value, str):
107+
descriptor["labels"].append(
108+
LabelDescriptor(key=key, value_type="STRING")
109+
)
110+
elif isinstance(value, bool):
111+
descriptor["labels"].append(
112+
LabelDescriptor(key=key, value_type="BOOL")
113+
)
114+
elif isinstance(value, int):
115+
descriptor["labels"].append(
116+
LabelDescriptor(key=key, value_type="INT64")
117+
)
118+
else:
119+
logger.warning(
120+
"Label value %s is not a string, bool or integer", value
121+
)
122+
123+
if self.unique_identifier:
124+
descriptor["labels"].append(
125+
LabelDescriptor(key=UNIQUE_IDENTIFIER_KEY, value_type="STRING")
126+
)
127+
128+
if isinstance(record.aggregator, SumAggregator):
129+
descriptor["metric_kind"] = MetricDescriptor.MetricKind.GAUGE
130+
else:
131+
logger.warning(
132+
"Unsupported aggregation type %s, ignoring it",
133+
type(record.aggregator).__name__,
134+
)
135+
return None
136+
if instrument.value_type == int:
137+
descriptor["value_type"] = MetricDescriptor.ValueType.INT64
138+
elif instrument.value_type == float:
139+
descriptor["value_type"] = MetricDescriptor.ValueType.DOUBLE
140+
proto_descriptor = MetricDescriptor(**descriptor)
141+
try:
142+
descriptor = self.client.create_metric_descriptor(
143+
self.project_name, proto_descriptor
144+
)
145+
# pylint: disable=broad-except
146+
except Exception as ex:
147+
logger.error(
148+
"Failed to create metric descriptor %s",
149+
proto_descriptor,
150+
exc_info=ex,
151+
)
152+
return None
153+
self._metric_descriptors[descriptor_type] = descriptor
154+
return descriptor
155+
156+
def export(
157+
self, metric_records: Sequence[MetricRecord]
158+
) -> "MetricsExportResult":
159+
all_series = []
160+
for record in metric_records:
161+
instrument = record.instrument
162+
metric_descriptor = self._get_metric_descriptor(record)
163+
if not metric_descriptor:
164+
continue
165+
166+
series = TimeSeries()
167+
self._add_resource_info(series)
168+
series.metric.type = metric_descriptor.type
169+
for key, value in record.labels:
170+
series.metric.labels[key] = str(value)
171+
172+
if self.unique_identifier:
173+
series.metric.labels[
174+
UNIQUE_IDENTIFIER_KEY
175+
] = self.unique_identifier
176+
177+
point = series.points.add()
178+
if instrument.value_type == int:
179+
point.value.int64_value = record.aggregator.checkpoint
180+
elif instrument.value_type == float:
181+
point.value.double_value = record.aggregator.checkpoint
182+
seconds, nanos = divmod(
183+
record.aggregator.last_update_timestamp, 1e9
184+
)
185+
186+
# Cloud Monitoring API allows, for any combination of labels and
187+
# metric name, one update per WRITE_INTERVAL seconds
188+
updated_key = (metric_descriptor.type, record.labels)
189+
last_updated_seconds = self._last_updated.get(updated_key, 0)
190+
if seconds <= last_updated_seconds + WRITE_INTERVAL:
191+
continue
192+
self._last_updated[updated_key] = seconds
193+
point.interval.end_time.seconds = int(seconds)
194+
point.interval.end_time.nanos = int(nanos)
195+
all_series.append(series)
196+
try:
197+
self._batch_write(all_series)
198+
# pylint: disable=broad-except
199+
except Exception as ex:
200+
logger.error(
201+
"Error while writing to Cloud Monitoring", exc_info=ex
202+
)
203+
return MetricsExportResult.FAILURE
204+
return MetricsExportResult.SUCCESS
Lines changed: 15 additions & 0 deletions
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.10b0"

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

Whitespace-only changes.

0 commit comments

Comments
 (0)