From b4a571dea0b505d958f1de3c248a665e7496b5b1 Mon Sep 17 00:00:00 2001 From: nerstak Date: Fri, 28 Apr 2023 18:38:19 +0200 Subject: [PATCH] fix: linting and removing unecessary public symbols --- .../exporter/prometheus/__init__.py | 712 +++++++++--------- .../tests/test_prometheus_exporter.py | 684 ++++++++--------- 2 files changed, 699 insertions(+), 697 deletions(-) diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index 7b01c53a461..c1f0bd8802f 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -1,356 +1,356 @@ -# 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.exporter.prometheus import PrometheusMetricReader - from opentelemetry.metrics import get_meter_provider, set_meter_provider - 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 ---- -""" - -from collections import deque -from itertools import chain -from json import dumps -from logging import getLogger -from re import IGNORECASE, UNICODE, compile -from typing import Dict, Sequence, Tuple, Union - -from prometheus_client.core import ( - REGISTRY, - CounterMetricFamily, - GaugeMetricFamily, - HistogramMetricFamily, - InfoMetricFamily, -) -from prometheus_client.core import Metric as PrometheusMetric - -from opentelemetry.sdk.metrics import Counter -from opentelemetry.sdk.metrics import Histogram as HistogramInstrument -from opentelemetry.sdk.metrics import ( - ObservableCounter, - ObservableGauge, - ObservableUpDownCounter, - UpDownCounter, -) -from opentelemetry.sdk.metrics.export import ( - AggregationTemporality, - Gauge, - Histogram, - HistogramDataPoint, - MetricReader, - MetricsData, - Sum, -) - -_logger = getLogger(__name__) - -TARGET_INFO_NAME = "target" -TARGET_INFO_DESCRIPTION = "Target metadata" - - -def _convert_buckets( - bucket_counts: Sequence[int], explicit_bounds: Sequence[float] -) -> Sequence[Tuple[str, int]]: - buckets = [] - total_count = 0 - for upper_bound, count in zip( - chain(explicit_bounds, ["+Inf"]), - bucket_counts, - ): - total_count += count - buckets.append((f"{upper_bound}", total_count)) - - return buckets - - -class PrometheusMetricReader(MetricReader): - """Prometheus metric exporter for OpenTelemetry.""" - - def __init__(self, disable_target_info: bool = False) -> None: - super().__init__( - preferred_temporality={ - Counter: AggregationTemporality.CUMULATIVE, - UpDownCounter: AggregationTemporality.CUMULATIVE, - HistogramInstrument: AggregationTemporality.CUMULATIVE, - ObservableCounter: AggregationTemporality.CUMULATIVE, - ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, - ObservableGauge: AggregationTemporality.CUMULATIVE, - } - ) - self._collector = _CustomCollector(disable_target_info) - REGISTRY.register(self._collector) - self._collector._callback = self.collect - - def _receive_metrics( - self, - metrics_data: MetricsData, - timeout_millis: float = 10_000, - **kwargs, - ) -> None: - if metrics_data is None: - return - self._collector.add_metrics_data(metrics_data) - - def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: - REGISTRY.unregister(self._collector) - - -class _CustomCollector: - """_CustomCollector represents the Prometheus Collector object - - See more: - https://github.com/prometheus/client_python#custom-collectors - """ - - def __init__(self, disable_target_info: bool = False): - self._callback = None - self._metrics_datas = deque() - self._non_letters_digits_underscore_re = compile( - r"[^\w]", UNICODE | IGNORECASE - ) - self._disable_target_info = disable_target_info - self._target_info = None - - def add_metrics_data(self, metrics_data: MetricsData) -> None: - """Add metrics to Prometheus data""" - self._metrics_datas.append(metrics_data) - - 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() - - metric_family_id_metric_family = {} - - if len(self._metrics_datas) != 0: - if not self._disable_target_info: - if self._target_info is None: - attributes = {} - for res in self._metrics_datas[0].resource_metrics: - attributes = {**attributes, **res.resource.attributes} - - self._target_info = self._create_info_metric( - TARGET_INFO_NAME, TARGET_INFO_DESCRIPTION, attributes - ) - metric_family_id_metric_family[ - TARGET_INFO_NAME - ] = self._target_info - - while self._metrics_datas: - self._translate_to_prometheus( - self._metrics_datas.popleft(), metric_family_id_metric_family - ) - - if metric_family_id_metric_family: - for metric_family in metric_family_id_metric_family.values(): - yield metric_family - - # pylint: disable=too-many-locals,too-many-branches - def _translate_to_prometheus( - self, - metrics_data: MetricsData, - metric_family_id_metric_family: Dict[str, PrometheusMetric], - ): - metrics = [] - - for resource_metrics in metrics_data.resource_metrics: - for scope_metrics in resource_metrics.scope_metrics: - for metric in scope_metrics.metrics: - metrics.append(metric) - - for metric in metrics: - label_valuess = [] - values = [] - - pre_metric_family_ids = [] - - metric_name = "" - metric_name += self._sanitize(metric.name) - - metric_description = metric.description or "" - - for number_data_point in metric.data.data_points: - label_keys = [] - label_values = [] - - for key, value in number_data_point.attributes.items(): - label_keys.append(self._sanitize(key)) - label_values.append(self._check_value(value)) - - pre_metric_family_ids.append( - "|".join( - [ - metric_name, - metric_description, - "%".join(label_keys), - metric.unit, - ] - ) - ) - - label_valuess.append(label_values) - if isinstance(number_data_point, HistogramDataPoint): - values.append( - { - "bucket_counts": number_data_point.bucket_counts, - "explicit_bounds": ( - number_data_point.explicit_bounds - ), - "sum": number_data_point.sum, - } - ) - else: - values.append(number_data_point.value) - - for pre_metric_family_id, label_values, value in zip( - pre_metric_family_ids, label_valuess, values - ): - if isinstance(metric.data, Sum): - - metric_family_id = "|".join( - [pre_metric_family_id, CounterMetricFamily.__name__] - ) - - if metric_family_id not in metric_family_id_metric_family: - metric_family_id_metric_family[ - metric_family_id - ] = CounterMetricFamily( - name=metric_name, - documentation=metric_description, - labels=label_keys, - unit=metric.unit, - ) - metric_family_id_metric_family[ - metric_family_id - ].add_metric(labels=label_values, value=value) - elif isinstance(metric.data, Gauge): - - metric_family_id = "|".join( - [pre_metric_family_id, GaugeMetricFamily.__name__] - ) - - if ( - metric_family_id - not in metric_family_id_metric_family.keys() - ): - metric_family_id_metric_family[ - metric_family_id - ] = GaugeMetricFamily( - name=metric_name, - documentation=metric_description, - labels=label_keys, - unit=metric.unit, - ) - metric_family_id_metric_family[ - metric_family_id - ].add_metric(labels=label_values, value=value) - elif isinstance(metric.data, Histogram): - - metric_family_id = "|".join( - [pre_metric_family_id, HistogramMetricFamily.__name__] - ) - - if ( - metric_family_id - not in metric_family_id_metric_family.keys() - ): - metric_family_id_metric_family[ - metric_family_id - ] = HistogramMetricFamily( - name=metric_name, - documentation=metric_description, - labels=label_keys, - unit=metric.unit, - ) - metric_family_id_metric_family[ - metric_family_id - ].add_metric( - labels=label_values, - buckets=_convert_buckets( - value["bucket_counts"], value["explicit_bounds"] - ), - sum_value=value["sum"], - ) - else: - _logger.warning( - "Unsupported metric data. %s", type(metric.data) - ) - - 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) - - # pylint: disable=no-self-use - def _check_value(self, value: Union[int, float, str, Sequence]) -> str: - """Check the label value and return is appropriate representation""" - if not isinstance(value, str): - return dumps(value, default=str) - return str(value) - - def _create_info_metric( - self, name: str, description: str, attributes: Dict[str, str] - ) -> InfoMetricFamily: - """Create an Info Metric Family with list of attributes""" - i = InfoMetricFamily(name, description, labels=attributes) - i.add_metric(labels=list(attributes.keys()), value=attributes) - return i +# 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.exporter.prometheus import PrometheusMetricReader + from opentelemetry.metrics import get_meter_provider, set_meter_provider + 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 +--- +""" + +from collections import deque +from itertools import chain +from json import dumps +from logging import getLogger +from re import IGNORECASE, UNICODE, compile +from typing import Dict, Sequence, Tuple, Union + +from prometheus_client.core import ( + REGISTRY, + CounterMetricFamily, + GaugeMetricFamily, + HistogramMetricFamily, + InfoMetricFamily, +) +from prometheus_client.core import Metric as PrometheusMetric + +from opentelemetry.sdk.metrics import Counter +from opentelemetry.sdk.metrics import Histogram as HistogramInstrument +from opentelemetry.sdk.metrics import ( + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + Gauge, + Histogram, + HistogramDataPoint, + MetricReader, + MetricsData, + Sum, +) + +_logger = getLogger(__name__) + +_TARGET_INFO_NAME = "target" +_TARGET_INFO_DESCRIPTION = "Target metadata" + + +def _convert_buckets( + bucket_counts: Sequence[int], explicit_bounds: Sequence[float] +) -> Sequence[Tuple[str, int]]: + buckets = [] + total_count = 0 + for upper_bound, count in zip( + chain(explicit_bounds, ["+Inf"]), + bucket_counts, + ): + total_count += count + buckets.append((f"{upper_bound}", total_count)) + + return buckets + + +class PrometheusMetricReader(MetricReader): + """Prometheus metric exporter for OpenTelemetry.""" + + def __init__(self, disable_target_info: bool = False) -> None: + super().__init__( + preferred_temporality={ + Counter: AggregationTemporality.CUMULATIVE, + UpDownCounter: AggregationTemporality.CUMULATIVE, + HistogramInstrument: AggregationTemporality.CUMULATIVE, + ObservableCounter: AggregationTemporality.CUMULATIVE, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + ) + self._collector = _CustomCollector(disable_target_info) + REGISTRY.register(self._collector) + self._collector._callback = self.collect + + def _receive_metrics( + self, + metrics_data: MetricsData, + timeout_millis: float = 10_000, + **kwargs, + ) -> None: + if metrics_data is None: + return + self._collector.add_metrics_data(metrics_data) + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + REGISTRY.unregister(self._collector) + + +class _CustomCollector: + """_CustomCollector represents the Prometheus Collector object + + See more: + https://github.com/prometheus/client_python#custom-collectors + """ + + def __init__(self, disable_target_info: bool = False): + self._callback = None + self._metrics_datas = deque() + self._non_letters_digits_underscore_re = compile( + r"[^\w]", UNICODE | IGNORECASE + ) + self._disable_target_info = disable_target_info + self._target_info = None + + def add_metrics_data(self, metrics_data: MetricsData) -> None: + """Add metrics to Prometheus data""" + self._metrics_datas.append(metrics_data) + + 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() + + metric_family_id_metric_family = {} + + if len(self._metrics_datas) != 0: + if not self._disable_target_info: + if self._target_info is None: + attributes = {} + for res in self._metrics_datas[0].resource_metrics: + attributes = {**attributes, **res.resource.attributes} + + self._target_info = self._create_info_metric( + _TARGET_INFO_NAME, _TARGET_INFO_DESCRIPTION, attributes + ) + metric_family_id_metric_family[ + _TARGET_INFO_NAME + ] = self._target_info + + while self._metrics_datas: + self._translate_to_prometheus( + self._metrics_datas.popleft(), metric_family_id_metric_family + ) + + if metric_family_id_metric_family: + for metric_family in metric_family_id_metric_family.values(): + yield metric_family + + # pylint: disable=too-many-locals,too-many-branches + def _translate_to_prometheus( + self, + metrics_data: MetricsData, + metric_family_id_metric_family: Dict[str, PrometheusMetric], + ): + metrics = [] + + for resource_metrics in metrics_data.resource_metrics: + for scope_metrics in resource_metrics.scope_metrics: + for metric in scope_metrics.metrics: + metrics.append(metric) + + for metric in metrics: + label_valuess = [] + values = [] + + pre_metric_family_ids = [] + + metric_name = "" + metric_name += self._sanitize(metric.name) + + metric_description = metric.description or "" + + for number_data_point in metric.data.data_points: + label_keys = [] + label_values = [] + + for key, value in number_data_point.attributes.items(): + label_keys.append(self._sanitize(key)) + label_values.append(self._check_value(value)) + + pre_metric_family_ids.append( + "|".join( + [ + metric_name, + metric_description, + "%".join(label_keys), + metric.unit, + ] + ) + ) + + label_valuess.append(label_values) + if isinstance(number_data_point, HistogramDataPoint): + values.append( + { + "bucket_counts": number_data_point.bucket_counts, + "explicit_bounds": ( + number_data_point.explicit_bounds + ), + "sum": number_data_point.sum, + } + ) + else: + values.append(number_data_point.value) + + for pre_metric_family_id, label_values, value in zip( + pre_metric_family_ids, label_valuess, values + ): + if isinstance(metric.data, Sum): + + metric_family_id = "|".join( + [pre_metric_family_id, CounterMetricFamily.__name__] + ) + + if metric_family_id not in metric_family_id_metric_family: + metric_family_id_metric_family[ + metric_family_id + ] = CounterMetricFamily( + name=metric_name, + documentation=metric_description, + labels=label_keys, + unit=metric.unit, + ) + metric_family_id_metric_family[ + metric_family_id + ].add_metric(labels=label_values, value=value) + elif isinstance(metric.data, Gauge): + + metric_family_id = "|".join( + [pre_metric_family_id, GaugeMetricFamily.__name__] + ) + + if ( + metric_family_id + not in metric_family_id_metric_family.keys() + ): + metric_family_id_metric_family[ + metric_family_id + ] = GaugeMetricFamily( + name=metric_name, + documentation=metric_description, + labels=label_keys, + unit=metric.unit, + ) + metric_family_id_metric_family[ + metric_family_id + ].add_metric(labels=label_values, value=value) + elif isinstance(metric.data, Histogram): + + metric_family_id = "|".join( + [pre_metric_family_id, HistogramMetricFamily.__name__] + ) + + if ( + metric_family_id + not in metric_family_id_metric_family.keys() + ): + metric_family_id_metric_family[ + metric_family_id + ] = HistogramMetricFamily( + name=metric_name, + documentation=metric_description, + labels=label_keys, + unit=metric.unit, + ) + metric_family_id_metric_family[ + metric_family_id + ].add_metric( + labels=label_values, + buckets=_convert_buckets( + value["bucket_counts"], value["explicit_bounds"] + ), + sum_value=value["sum"], + ) + else: + _logger.warning( + "Unsupported metric data. %s", type(metric.data) + ) + + 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) + + # pylint: disable=no-self-use + def _check_value(self, value: Union[int, float, str, Sequence]) -> str: + """Check the label value and return is appropriate representation""" + if not isinstance(value, str): + return dumps(value, default=str) + return str(value) + + def _create_info_metric( + self, name: str, description: str, attributes: Dict[str, str] + ) -> InfoMetricFamily: + """Create an Info Metric Family with list of attributes""" + i = InfoMetricFamily(name, description, labels=attributes) + i.add_metric(labels=list(attributes.keys()), value=attributes) + return i diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index 0856562d645..70e38f227c7 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -1,341 +1,343 @@ -# 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. - -from textwrap import dedent -from unittest import TestCase -from unittest.mock import Mock, patch - -from prometheus_client import generate_latest -from prometheus_client.core import ( - CounterMetricFamily, - GaugeMetricFamily, - InfoMetricFamily, -) - -from opentelemetry.exporter.prometheus import ( - PrometheusMetricReader, - _CustomCollector, -) -from opentelemetry.sdk.metrics import MeterProvider -from opentelemetry.sdk.metrics.export import ( - AggregationTemporality, - Histogram, - HistogramDataPoint, - Metric, - MetricsData, - ResourceMetrics, - ScopeMetrics, -) -from opentelemetry.sdk.resources import Resource -from opentelemetry.test.metrictestutil import ( - _generate_gauge, - _generate_sum, - _generate_unsupported_metric, -) - - -class TestPrometheusMetricReader(TestCase): - def setUp(self): - self._mock_registry_register = Mock() - self._registry_register_patch = 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: - _ = PrometheusMetricReader() - self.assertTrue(self._mock_registry_register.called) - - def test_shutdown(self): - with 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): - metric = Metric( - name="test@name", - description="foo", - unit="s", - data=Histogram( - data_points=[ - HistogramDataPoint( - attributes={"histo": 1}, - start_time_unix_nano=1641946016139533244, - time_unix_nano=1641946016139533244, - count=6, - sum=579.0, - bucket_counts=[1, 3, 2], - explicit_bounds=[123.0, 456.0], - min=1, - max=457, - ) - ], - aggregation_temporality=AggregationTemporality.DELTA, - ), - ) - metrics_data = MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Mock(), - scope_metrics=[ - ScopeMetrics( - scope=Mock(), - metrics=[metric], - schema_url="schema_url", - ) - ], - schema_url="schema_url", - ) - ] - ) - - collector = _CustomCollector(disable_target_info=True) - collector.add_metrics_data(metrics_data) - result_bytes = generate_latest(collector) - result = result_bytes.decode("utf-8") - self.assertEqual( - result, - dedent( - """\ - # HELP test_name_s foo - # TYPE test_name_s histogram - test_name_s_bucket{histo="1",le="123.0"} 1.0 - test_name_s_bucket{histo="1",le="456.0"} 4.0 - test_name_s_bucket{histo="1",le="+Inf"} 6.0 - test_name_s_count{histo="1"} 6.0 - test_name_s_sum{histo="1"} 579.0 - """ - ), - ) - - def test_sum_to_prometheus(self): - labels = {"environment@": "staging", "os": "Windows"} - metric = _generate_sum( - "test@sum", - 123, - attributes=labels, - description="testdesc", - unit="testunit", - ) - - metrics_data = MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Mock(), - scope_metrics=[ - ScopeMetrics( - scope=Mock(), - metrics=[metric], - schema_url="schema_url", - ) - ], - schema_url="schema_url", - ) - ] - ) - - collector = _CustomCollector(disable_target_info=True) - collector.add_metrics_data(metrics_data) - - for prometheus_metric in collector.collect(): - self.assertEqual(type(prometheus_metric), CounterMetricFamily) - self.assertEqual(prometheus_metric.name, "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"} - metric = _generate_gauge( - "test@gauge", - 123, - attributes=labels, - description="testdesc", - unit="testunit", - ) - - metrics_data = MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Mock(), - scope_metrics=[ - ScopeMetrics( - scope=Mock(), - metrics=[metric], - schema_url="schema_url", - ) - ], - schema_url="schema_url", - ) - ] - ) - - collector = _CustomCollector(disable_target_info=True) - collector.add_metrics_data(metrics_data) - - for prometheus_metric in collector.collect(): - self.assertEqual(type(prometheus_metric), GaugeMetricFamily) - self.assertEqual(prometheus_metric.name, "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() - collector.add_metrics_data([record]) - collector.collect() - self.assertLogs("opentelemetry.exporter.prometheus", level="WARNING") - - def test_sanitize(self): - collector = _CustomCollector() - 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") - - def test_list_labels(self): - labels = {"environment@": ["1", "2", "3"], "os": "Unix"} - metric = _generate_gauge( - "test@gauge", - 123, - attributes=labels, - description="testdesc", - unit="testunit", - ) - metrics_data = MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Mock(), - scope_metrics=[ - ScopeMetrics( - scope=Mock(), - metrics=[metric], - schema_url="schema_url", - ) - ], - schema_url="schema_url", - ) - ] - ) - collector = _CustomCollector(disable_target_info=True) - collector.add_metrics_data(metrics_data) - - for prometheus_metric in collector.collect(): - self.assertEqual(type(prometheus_metric), GaugeMetricFamily) - self.assertEqual(prometheus_metric.name, "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_"], - '["1", "2", "3"]', - ) - self.assertEqual(prometheus_metric.samples[0].labels["os"], "Unix") - - def test_check_value(self): - - collector = _CustomCollector() - - self.assertEqual(collector._check_value(1), "1") - self.assertEqual(collector._check_value(1.0), "1.0") - self.assertEqual(collector._check_value("a"), "a") - self.assertEqual(collector._check_value([1, 2]), "[1, 2]") - self.assertEqual(collector._check_value((1, 2)), "[1, 2]") - self.assertEqual(collector._check_value(["a", 2]), '["a", 2]') - self.assertEqual(collector._check_value(True), "true") - self.assertEqual(collector._check_value(False), "false") - self.assertEqual(collector._check_value(None), "null") - - def test_multiple_collection_calls(self): - - metric_reader = PrometheusMetricReader() - provider = MeterProvider(metric_readers=[metric_reader]) - meter = provider.get_meter("getting-started", "0.1.2") - counter = meter.create_counter("counter") - counter.add(1) - result_0 = list(metric_reader._collector.collect()) - result_1 = list(metric_reader._collector.collect()) - result_2 = list(metric_reader._collector.collect()) - self.assertEqual(result_0, result_1) - self.assertEqual(result_1, result_2) - - def test_target_info_enabled_by_default(self): - metric_reader = PrometheusMetricReader() - provider = MeterProvider( - metric_readers=[metric_reader], resource=Resource({"os": "Unix", "histo": 1}) - ) - meter = provider.get_meter("getting-started", "0.1.2") - counter = meter.create_counter("counter") - counter.add(1) - result = list(metric_reader._collector.collect()) - - for prometheus_metric in result[:0]: - self.assertEqual(type(prometheus_metric), InfoMetricFamily) - self.assertEqual(prometheus_metric.name, "target") - self.assertEqual( - prometheus_metric.documentation, "Target metadata" - ) - self.assertTrue(len(prometheus_metric.samples) == 1) - self.assertEqual(prometheus_metric.samples[0].value, 1) - self.assertTrue(len(prometheus_metric.samples[0].labels) == 2) - self.assertEqual(prometheus_metric.samples[0].labels["os"], "Unix") - self.assertEqual(prometheus_metric.samples[0].labels["histo"], "1") - - def test_target_info_disabled(self): - metric_reader = PrometheusMetricReader(disable_target_info=True) - provider = MeterProvider( - metric_readers=[metric_reader], resource=Resource({"os": "Unix", "histo": 1}) - ) - meter = provider.get_meter("getting-started", "0.1.2") - counter = meter.create_counter("counter") - counter.add(1) - result = list(metric_reader._collector.collect()) - - for prometheus_metric in result: - self.assertNotEquals(type(prometheus_metric), InfoMetricFamily) - self.assertNotEquals(prometheus_metric.name, "target") - self.assertNotEquals( - prometheus_metric.documentation, "Target metadata" - ) - self.assertNotIn("os", prometheus_metric.samples[0].labels) - self.assertNotIn("histo", prometheus_metric.samples[0].labels) +# 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. + +from textwrap import dedent +from unittest import TestCase +from unittest.mock import Mock, patch + +from prometheus_client import generate_latest +from prometheus_client.core import ( + CounterMetricFamily, + GaugeMetricFamily, + InfoMetricFamily, +) + +from opentelemetry.exporter.prometheus import ( + PrometheusMetricReader, + _CustomCollector, +) +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + Histogram, + HistogramDataPoint, + Metric, + MetricsData, + ResourceMetrics, + ScopeMetrics, +) +from opentelemetry.sdk.resources import Resource +from opentelemetry.test.metrictestutil import ( + _generate_gauge, + _generate_sum, + _generate_unsupported_metric, +) + + +class TestPrometheusMetricReader(TestCase): + def setUp(self): + self._mock_registry_register = Mock() + self._registry_register_patch = 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: + _ = PrometheusMetricReader() + self.assertTrue(self._mock_registry_register.called) + + def test_shutdown(self): + with 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): + metric = Metric( + name="test@name", + description="foo", + unit="s", + data=Histogram( + data_points=[ + HistogramDataPoint( + attributes={"histo": 1}, + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=6, + sum=579.0, + bucket_counts=[1, 3, 2], + explicit_bounds=[123.0, 456.0], + min=1, + max=457, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Mock(), + scope_metrics=[ + ScopeMetrics( + scope=Mock(), + metrics=[metric], + schema_url="schema_url", + ) + ], + schema_url="schema_url", + ) + ] + ) + + collector = _CustomCollector(disable_target_info=True) + collector.add_metrics_data(metrics_data) + result_bytes = generate_latest(collector) + result = result_bytes.decode("utf-8") + self.assertEqual( + result, + dedent( + """\ + # HELP test_name_s foo + # TYPE test_name_s histogram + test_name_s_bucket{histo="1",le="123.0"} 1.0 + test_name_s_bucket{histo="1",le="456.0"} 4.0 + test_name_s_bucket{histo="1",le="+Inf"} 6.0 + test_name_s_count{histo="1"} 6.0 + test_name_s_sum{histo="1"} 579.0 + """ + ), + ) + + def test_sum_to_prometheus(self): + labels = {"environment@": "staging", "os": "Windows"} + metric = _generate_sum( + "test@sum", + 123, + attributes=labels, + description="testdesc", + unit="testunit", + ) + + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Mock(), + scope_metrics=[ + ScopeMetrics( + scope=Mock(), + metrics=[metric], + schema_url="schema_url", + ) + ], + schema_url="schema_url", + ) + ] + ) + + collector = _CustomCollector(disable_target_info=True) + collector.add_metrics_data(metrics_data) + + for prometheus_metric in collector.collect(): + self.assertEqual(type(prometheus_metric), CounterMetricFamily) + self.assertEqual(prometheus_metric.name, "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"} + metric = _generate_gauge( + "test@gauge", + 123, + attributes=labels, + description="testdesc", + unit="testunit", + ) + + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Mock(), + scope_metrics=[ + ScopeMetrics( + scope=Mock(), + metrics=[metric], + schema_url="schema_url", + ) + ], + schema_url="schema_url", + ) + ] + ) + + collector = _CustomCollector(disable_target_info=True) + collector.add_metrics_data(metrics_data) + + for prometheus_metric in collector.collect(): + self.assertEqual(type(prometheus_metric), GaugeMetricFamily) + self.assertEqual(prometheus_metric.name, "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() + collector.add_metrics_data([record]) + collector.collect() + self.assertLogs("opentelemetry.exporter.prometheus", level="WARNING") + + def test_sanitize(self): + collector = _CustomCollector() + 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") + + def test_list_labels(self): + labels = {"environment@": ["1", "2", "3"], "os": "Unix"} + metric = _generate_gauge( + "test@gauge", + 123, + attributes=labels, + description="testdesc", + unit="testunit", + ) + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Mock(), + scope_metrics=[ + ScopeMetrics( + scope=Mock(), + metrics=[metric], + schema_url="schema_url", + ) + ], + schema_url="schema_url", + ) + ] + ) + collector = _CustomCollector(disable_target_info=True) + collector.add_metrics_data(metrics_data) + + for prometheus_metric in collector.collect(): + self.assertEqual(type(prometheus_metric), GaugeMetricFamily) + self.assertEqual(prometheus_metric.name, "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_"], + '["1", "2", "3"]', + ) + self.assertEqual(prometheus_metric.samples[0].labels["os"], "Unix") + + def test_check_value(self): + + collector = _CustomCollector() + + self.assertEqual(collector._check_value(1), "1") + self.assertEqual(collector._check_value(1.0), "1.0") + self.assertEqual(collector._check_value("a"), "a") + self.assertEqual(collector._check_value([1, 2]), "[1, 2]") + self.assertEqual(collector._check_value((1, 2)), "[1, 2]") + self.assertEqual(collector._check_value(["a", 2]), '["a", 2]') + self.assertEqual(collector._check_value(True), "true") + self.assertEqual(collector._check_value(False), "false") + self.assertEqual(collector._check_value(None), "null") + + def test_multiple_collection_calls(self): + + metric_reader = PrometheusMetricReader() + provider = MeterProvider(metric_readers=[metric_reader]) + meter = provider.get_meter("getting-started", "0.1.2") + counter = meter.create_counter("counter") + counter.add(1) + result_0 = list(metric_reader._collector.collect()) + result_1 = list(metric_reader._collector.collect()) + result_2 = list(metric_reader._collector.collect()) + self.assertEqual(result_0, result_1) + self.assertEqual(result_1, result_2) + + def test_target_info_enabled_by_default(self): + metric_reader = PrometheusMetricReader() + provider = MeterProvider( + metric_readers=[metric_reader], + resource=Resource({"os": "Unix", "histo": 1}), + ) + meter = provider.get_meter("getting-started", "0.1.2") + counter = meter.create_counter("counter") + counter.add(1) + result = list(metric_reader._collector.collect()) + + for prometheus_metric in result[:0]: + self.assertEqual(type(prometheus_metric), InfoMetricFamily) + self.assertEqual(prometheus_metric.name, "target") + self.assertEqual( + prometheus_metric.documentation, "Target metadata" + ) + self.assertTrue(len(prometheus_metric.samples) == 1) + self.assertEqual(prometheus_metric.samples[0].value, 1) + self.assertTrue(len(prometheus_metric.samples[0].labels) == 2) + self.assertEqual(prometheus_metric.samples[0].labels["os"], "Unix") + self.assertEqual(prometheus_metric.samples[0].labels["histo"], "1") + + def test_target_info_disabled(self): + metric_reader = PrometheusMetricReader(disable_target_info=True) + provider = MeterProvider( + metric_readers=[metric_reader], + resource=Resource({"os": "Unix", "histo": 1}), + ) + meter = provider.get_meter("getting-started", "0.1.2") + counter = meter.create_counter("counter") + counter.add(1) + result = list(metric_reader._collector.collect()) + + for prometheus_metric in result: + self.assertNotEquals(type(prometheus_metric), InfoMetricFamily) + self.assertNotEquals(prometheus_metric.name, "target") + self.assertNotEquals( + prometheus_metric.documentation, "Target metadata" + ) + self.assertNotIn("os", prometheus_metric.samples[0].labels) + self.assertNotIn("histo", prometheus_metric.samples[0].labels)