diff --git a/CHANGELOG.md b/CHANGELOG.md index d5d58c8293b..fc12fea8208 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2464](https://github.com/open-telemetry/opentelemetry-python/pull/2464)) - Fix `OTEL_EXPORTER_OTLP_ENDPOINT` usage in OTLP HTTP trace exporter ([#2493](https://github.com/open-telemetry/opentelemetry-python/pull/2493)) +- [exporter/opentelemetry-exporter-prometheus] restore package using the new metrics API + ([#2321](https://github.com/open-telemetry/opentelemetry-python/pull/2321)) ## [1.9.1-0.28b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.1-0.28b1) - 2022-01-29 diff --git a/eachdist.ini b/eachdist.ini index 576d46a5fc7..f65be079bec 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -35,6 +35,7 @@ version=0.28b1 packages= opentelemetry-opentracing-shim opentelemetry-exporter-opencensus + opentelemetry-exporter-prometheus opentelemetry-distro opentelemetry-semantic-conventions opentelemetry-test-utils @@ -45,7 +46,6 @@ version=1.10a0 packages= opentelemetry-exporter-prometheus-remote-write - opentelemetry-exporter-prometheus opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc diff --git a/exporter/opentelemetry-exporter-prometheus/LICENSE b/exporter/opentelemetry-exporter-prometheus/LICENSE new file mode 100644 index 00000000000..1ef7dad2c5c --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright The OpenTelemetry Authors + + 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. diff --git a/exporter/opentelemetry-exporter-prometheus/MANIFEST.in b/exporter/opentelemetry-exporter-prometheus/MANIFEST.in new file mode 100644 index 00000000000..aed3e33273b --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/exporter/opentelemetry-exporter-prometheus/README.rst b/exporter/opentelemetry-exporter-prometheus/README.rst new file mode 100644 index 00000000000..a3eb9200005 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/README.rst @@ -0,0 +1,23 @@ +OpenTelemetry Prometheus Exporter +================================= + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-prometheus.svg + :target: https://pypi.org/project/opentelemetry-exporter-prometheus/ + +This library allows to export metrics data to `Prometheus `_. + +Installation +------------ + +:: + + pip install opentelemetry-exporter-prometheus + +References +---------- + +* `OpenTelemetry Prometheus Exporter `_ +* `Prometheus `_ +* `OpenTelemetry Project `_ diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg new file mode 100644 index 00000000000..53e9ef17ede --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/setup.cfg @@ -0,0 +1,55 @@ +# Copyright The OpenTelemetry Authors +# +# 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. +# +[metadata] +name = opentelemetry-exporter-prometheus +description = Prometheus Metric Exporter for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-prometheus +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + +[options] +python_requires = >=3.6 +package_dir= + =src +packages=find_namespace: +install_requires = + prometheus_client >= 0.5.0, < 1.0.0 + opentelemetry-api >= 1.9.1 + opentelemetry-sdk >= 1.9.1 + +[options.packages.find] +where = src + +[options.extras_require] +test = + +[options.entry_points] +opentelemetry_metric_reader = + prometheus = opentelemetry.exporter.prometheus:PrometheusMetricReader diff --git a/exporter/opentelemetry-exporter-prometheus/setup.py b/exporter/opentelemetry-exporter-prometheus/setup.py new file mode 100644 index 00000000000..9fdbd7de497 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# 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. +import os + +import setuptools + +_BASE_DIR = os.path.dirname(__file__) +_VERSION_FILENAME = os.path.join( + _BASE_DIR, "src", "opentelemetry", "exporter", "prometheus", "version.py" +) +_PACKAGE_INFO = {} +with open(_VERSION_FILENAME, encoding="utf-8") as f: + exec(f.read(), _PACKAGE_INFO) + +setuptools.setup(version=_PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py new file mode 100644 index 00000000000..a952e56bcf8 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -0,0 +1,208 @@ +# Copyright The OpenTelemetry Authors +# +# 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. + +""" +This library allows export of metrics data to `Prometheus `_. + +Usage +----- + +The **OpenTelemetry Prometheus Exporter** allows export of `OpenTelemetry`_ +metrics to `Prometheus`_. + + +.. _Prometheus: https://prometheus.io/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ + +.. code:: python + + from prometheus_client import start_http_server + + from opentelemetry._metrics import get_meter_provider, set_meter_provider + from opentelemetry.exporter.prometheus import PrometheusMetricReader + from opentelemetry.sdk._metrics import MeterProvider + + # Start Prometheus client + start_http_server(port=8000, addr="localhost") + + # Exporter to export metrics to Prometheus + prefix = "MyAppPrefix" + reader = PrometheusMetricReader(prefix) + + # Meter is responsible for creating and recording metrics + set_meter_provider(MeterProvider(metric_readers=[reader])) + meter = get_meter_provider().get_meter("myapp", "0.1.2") + + counter = meter.create_counter( + "requests", + "requests", + "number of requests", + ) + + # Labels are used to identify key-values that are associated with a specific + # metric that you want to record. These are useful for pre-aggregation and can + # be used to store custom dimensions pertaining to a metric + labels = {"environment": "staging"} + + counter.add(25, labels) + input("Press any key to exit...") + +API +--- +""" + +import collections +import logging +import re +from typing import Iterable, Optional, Sequence, Tuple + +from prometheus_client import core + +from opentelemetry.sdk._metrics.export import MetricReader +from opentelemetry.sdk._metrics.point import Gauge, Histogram, Metric, Sum + +_logger = logging.getLogger(__name__) + + +def _convert_buckets(metric: Metric) -> Sequence[Tuple[str, int]]: + buckets = [] + total_count = 0 + for index, value in enumerate(metric.point.bucket_counts): + total_count += value + buckets.append( + ( + f"{metric.point.explicit_bounds[index]}", + total_count, + ) + ) + return buckets + + +class PrometheusMetricReader(MetricReader): + """Prometheus metric exporter for OpenTelemetry. + + Args: + prefix: single-word application prefix relevant to the domain + the metric belongs to. + """ + + def __init__(self, prefix: str = "") -> None: + super().__init__() + self._collector = _CustomCollector(prefix) + core.REGISTRY.register(self._collector) + self._collector._callback = self.collect + + def _receive_metrics(self, metrics: Iterable[Metric]) -> None: + if metrics is None: + return + self._collector.add_metrics_data(metrics) + + def shutdown(self) -> bool: + core.REGISTRY.unregister(self._collector) + return True + + +class _CustomCollector: + """_CustomCollector represents the Prometheus Collector object + + See more: + https://github.com/prometheus/client_python#custom-collectors + """ + + def __init__(self, prefix: str = ""): + self._prefix = prefix + self._callback = None + self._metrics_to_export = collections.deque() + self._non_letters_digits_underscore_re = re.compile( + r"[^\w]", re.UNICODE | re.IGNORECASE + ) + + def add_metrics_data(self, export_records: Sequence[Metric]) -> None: + """Add metrics to Prometheus data""" + self._metrics_to_export.append(export_records) + + def collect(self) -> None: + """Collect fetches the metrics from OpenTelemetry + and delivers them as Prometheus Metrics. + Collect is invoked every time a ``prometheus.Gatherer`` is run + for example when the HTTP endpoint is invoked by Prometheus. + """ + if self._callback is not None: + self._callback() + + while self._metrics_to_export: + for export_record in self._metrics_to_export.popleft(): + prometheus_metric = self._translate_to_prometheus( + export_record + ) + if prometheus_metric is not None: + yield prometheus_metric + + def _translate_to_prometheus( + self, metric: Metric + ) -> Optional[core.Metric]: + prometheus_metric = None + label_values = [] + label_keys = [] + for key, value in metric.attributes.items(): + label_keys.append(self._sanitize(key)) + label_values.append(str(value)) + + metric_name = "" + if self._prefix != "": + metric_name = self._prefix + "_" + metric_name += self._sanitize(metric.name) + + description = metric.description or "" + if isinstance(metric.point, Sum): + prometheus_metric = core.CounterMetricFamily( + name=metric_name, + documentation=description, + labels=label_keys, + unit=metric.unit, + ) + prometheus_metric.add_metric( + labels=label_values, value=metric.point.value + ) + elif isinstance(metric.point, Gauge): + prometheus_metric = core.GaugeMetricFamily( + name=metric_name, + documentation=description, + labels=label_keys, + unit=metric.unit, + ) + prometheus_metric.add_metric( + labels=label_values, value=metric.point.value + ) + elif isinstance(metric.point, Histogram): + value = metric.point.sum + prometheus_metric = core.HistogramMetricFamily( + name=metric_name, + documentation=description, + labels=label_keys, + unit=metric.unit, + ) + buckets = _convert_buckets(metric) + prometheus_metric.add_metric( + labels=label_values, buckets=buckets, sum_value=value + ) + else: + _logger.warning("Unsupported metric type. %s", type(metric.point)) + return prometheus_metric + + def _sanitize(self, key: str) -> str: + """sanitize the given metric name or label according to Prometheus rule. + Replace all characters other than [A-Za-z0-9_] with '_'. + """ + return self._non_letters_digits_underscore_re.sub("_", key) diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py new file mode 100644 index 00000000000..a94d4ed4462 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# 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. + +__version__ = "0.28b1" diff --git a/exporter/opentelemetry-exporter-prometheus/tests/__init__.py b/exporter/opentelemetry-exporter-prometheus/tests/__init__.py new file mode 100644 index 00000000000..b0a6f428417 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright The OpenTelemetry Authors +# +# 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. diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py new file mode 100644 index 00000000000..af4bc7c0fdd --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -0,0 +1,154 @@ +# Copyright The OpenTelemetry Authors +# +# 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. + +import unittest +from unittest import mock + +from prometheus_client import generate_latest +from prometheus_client.core import CounterMetricFamily, GaugeMetricFamily + +from opentelemetry.exporter.prometheus import ( + PrometheusMetricReader, + _CustomCollector, +) +from opentelemetry.sdk._metrics.point import AggregationTemporality, Histogram +from opentelemetry.test.metrictestutil import ( + _generate_gauge, + _generate_metric, + _generate_sum, + _generate_unsupported_metric, +) + + +class TestPrometheusMetricReader(unittest.TestCase): + def setUp(self): + self._mock_registry_register = mock.Mock() + self._registry_register_patch = mock.patch( + "prometheus_client.core.REGISTRY.register", + side_effect=self._mock_registry_register, + ) + + # pylint: disable=protected-access + def test_constructor(self): + """Test the constructor.""" + with self._registry_register_patch: + exporter = PrometheusMetricReader("testprefix") + self.assertEqual(exporter._collector._prefix, "testprefix") + self.assertTrue(self._mock_registry_register.called) + + def test_shutdown(self): + with mock.patch( + "prometheus_client.core.REGISTRY.unregister" + ) as registry_unregister_patch: + exporter = PrometheusMetricReader() + exporter.shutdown() + self.assertTrue(registry_unregister_patch.called) + + def test_histogram_to_prometheus(self): + record = _generate_metric( + "test@name", + Histogram( + time_unix_nano=1641946016139533244, + start_time_unix_nano=1641946016139533244, + bucket_counts=[1, 1], + sum=579.0, + explicit_bounds=[123.0, 456.0], + aggregation_temporality=AggregationTemporality.DELTA, + ), + attributes={"histo": 1}, + ) + + collector = _CustomCollector("testprefix") + collector.add_metrics_data([record]) + result_bytes = generate_latest(collector) + result = result_bytes.decode("utf-8") + self.assertIn('testprefix_test_name_s_sum{histo="1"} 579.0', result) + self.assertIn('testprefix_test_name_s_count{histo="1"} 2.0', result) + + def test_sum_to_prometheus(self): + labels = {"environment@": "staging", "os": "Windows"} + record = _generate_sum( + "test@sum", + 123, + attributes=labels, + description="testdesc", + unit="testunit", + ) + collector = _CustomCollector("testprefix") + collector.add_metrics_data([record]) + + for prometheus_metric in collector.collect(): + self.assertEqual(type(prometheus_metric), CounterMetricFamily) + self.assertEqual( + prometheus_metric.name, "testprefix_test_sum_testunit" + ) + self.assertEqual(prometheus_metric.documentation, "testdesc") + self.assertTrue(len(prometheus_metric.samples) == 1) + self.assertEqual(prometheus_metric.samples[0].value, 123) + self.assertTrue(len(prometheus_metric.samples[0].labels) == 2) + self.assertEqual( + prometheus_metric.samples[0].labels["environment_"], "staging" + ) + self.assertEqual( + prometheus_metric.samples[0].labels["os"], "Windows" + ) + + def test_gauge_to_prometheus(self): + labels = {"environment@": "dev", "os": "Unix"} + record = _generate_gauge( + "test@gauge", + 123, + attributes=labels, + description="testdesc", + unit="testunit", + ) + collector = _CustomCollector("testprefix") + collector.add_metrics_data([record]) + + for prometheus_metric in collector.collect(): + self.assertEqual(type(prometheus_metric), GaugeMetricFamily) + self.assertEqual( + prometheus_metric.name, "testprefix_test_gauge_testunit" + ) + self.assertEqual(prometheus_metric.documentation, "testdesc") + self.assertTrue(len(prometheus_metric.samples) == 1) + self.assertEqual(prometheus_metric.samples[0].value, 123) + self.assertTrue(len(prometheus_metric.samples[0].labels) == 2) + self.assertEqual( + prometheus_metric.samples[0].labels["environment_"], "dev" + ) + self.assertEqual(prometheus_metric.samples[0].labels["os"], "Unix") + + def test_invalid_metric(self): + labels = {"environment": "staging"} + record = _generate_unsupported_metric( + "tesname", + attributes=labels, + description="testdesc", + unit="testunit", + ) + collector = _CustomCollector("testprefix") + collector.add_metrics_data([record]) + collector.collect() + self.assertLogs("opentelemetry.exporter.prometheus", level="WARNING") + + def test_sanitize(self): + collector = _CustomCollector("testprefix") + self.assertEqual( + collector._sanitize("1!2@3#4$5%6^7&8*9(0)_-"), + "1_2_3_4_5_6_7_8_9_0___", + ) + self.assertEqual(collector._sanitize(",./?;:[]{}"), "__________") + self.assertEqual(collector._sanitize("TestString"), "TestString") + self.assertEqual(collector._sanitize("aAbBcC_12_oi"), "aAbBcC_12_oi") diff --git a/tox.ini b/tox.ini index 44c4367588d..8d5973b166f 100644 --- a/tox.ini +++ b/tox.ini @@ -42,6 +42,9 @@ envlist = py3{6,7,8,9,10}-opentelemetry-exporter-otlp-proto-http pypy3-opentelemetry-exporter-otlp-proto-http + py3{6,7,8,9,10}-opentelemetry-exporter-prometheus + pypy3-opentelemetry-exporter-prometheus + ; opentelemetry-exporter-zipkin py3{6,7,8,9,10}-opentelemetry-exporter-zipkin-combined pypy3-opentelemetry-exporter-zipkin-combined @@ -97,6 +100,7 @@ changedir = exporter-otlp-combined: exporter/opentelemetry-exporter-otlp/tests exporter-otlp-proto-grpc: exporter/opentelemetry-exporter-otlp-proto-grpc/tests exporter-otlp-proto-http: exporter/opentelemetry-exporter-otlp-proto-http/tests + exporter-prometheus: exporter/opentelemetry-exporter-prometheus/tests exporter-zipkin-combined: exporter/opentelemetry-exporter-zipkin/tests exporter-zipkin-proto-http: exporter/opentelemetry-exporter-zipkin-proto-http/tests exporter-zipkin-json: exporter/opentelemetry-exporter-zipkin-json/tests @@ -140,6 +144,8 @@ commands_pre = opentracing-shim: pip install {toxinidir}/opentelemetry-sdk opentracing-shim: pip install {toxinidir}/shim/opentelemetry-opentracing-shim + exporter-prometheus: pip install {toxinidir}/exporter/opentelemetry-exporter-prometheus + exporter-zipkin-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-zipkin-json exporter-zipkin-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-zipkin-proto-http exporter-zipkin-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-zipkin @@ -202,6 +208,7 @@ commands_pre = python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp[test] + python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-prometheus[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin-json[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin-proto-http[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin[test]