From adbcf820f6ff24de80278113ce9629e67111ef8e Mon Sep 17 00:00:00 2001 From: nerstak <33179821+nerstak@users.noreply.github.com> Date: Wed, 28 Jun 2023 18:34:34 +0200 Subject: [PATCH] Exporting resources attributes on target_info for Prometheus Exporter (#3279) Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 2 + .../exporter/prometheus/__init__.py | 35 +++++++++-- .../tests/test_prometheus_exporter.py | 58 +++++++++++++++++-- 3 files changed, 86 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a22a6c7c7a..d6d98b5e974 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased + - Add max_scale option to Exponential Bucket Histogram Aggregation ([#3323](https://github.com/open-telemetry/opentelemetry-python/pull/3323)) - Use BoundedAttributes instead of raw dict to extract attributes from LogRecord @@ -35,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add speced out environment variables and arguments for BatchLogRecordProcessor ([#3237](https://github.com/open-telemetry/opentelemetry-python/pull/3237)) + ## Version 1.17.0/0.38b0 (2023-03-22) - Implement LowMemory temporality 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 7442b7b242d..9ece76755cb 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -74,6 +74,7 @@ CounterMetricFamily, GaugeMetricFamily, HistogramMetricFamily, + InfoMetricFamily, ) from prometheus_client.core import Metric as PrometheusMetric @@ -97,6 +98,9 @@ _logger = getLogger(__name__) +_TARGET_INFO_NAME = "target" +_TARGET_INFO_DESCRIPTION = "Target metadata" + def _convert_buckets( bucket_counts: Sequence[int], explicit_bounds: Sequence[float] @@ -116,8 +120,7 @@ def _convert_buckets( class PrometheusMetricReader(MetricReader): """Prometheus metric exporter for OpenTelemetry.""" - def __init__(self) -> None: - + def __init__(self, disable_target_info: bool = False) -> None: super().__init__( preferred_temporality={ Counter: AggregationTemporality.CUMULATIVE, @@ -128,7 +131,7 @@ def __init__(self) -> None: ObservableGauge: AggregationTemporality.CUMULATIVE, } ) - self._collector = _CustomCollector() + self._collector = _CustomCollector(disable_target_info) REGISTRY.register(self._collector) self._collector._callback = self.collect @@ -153,12 +156,14 @@ class _CustomCollector: https://github.com/prometheus/client_python#custom-collectors """ - def __init__(self): + 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""" @@ -175,6 +180,20 @@ def collect(self) -> None: metric_family_id_metric_family = {} + if len(self._metrics_datas): + 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 @@ -327,3 +346,11 @@ def _check_value(self, value: Union[int, float, str, Sequence]) -> str: 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""" + info = InfoMetricFamily(name, description, labels=attributes) + info.add_metric(labels=list(attributes.keys()), value=attributes) + return info diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index 1180fac6141..c7ce1afae19 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -17,7 +17,11 @@ from unittest.mock import Mock, patch from prometheus_client import generate_latest -from prometheus_client.core import CounterMetricFamily, GaugeMetricFamily +from prometheus_client.core import ( + CounterMetricFamily, + GaugeMetricFamily, + InfoMetricFamily, +) from opentelemetry.exporter.prometheus import ( PrometheusMetricReader, @@ -33,6 +37,7 @@ ResourceMetrics, ScopeMetrics, ) +from opentelemetry.sdk.resources import Resource from opentelemetry.test.metrictestutil import ( _generate_gauge, _generate_sum, @@ -101,7 +106,7 @@ def test_histogram_to_prometheus(self): ] ) - collector = _CustomCollector() + collector = _CustomCollector(disable_target_info=True) collector.add_metrics_data(metrics_data) result_bytes = generate_latest(collector) result = result_bytes.decode("utf-8") @@ -146,7 +151,7 @@ def test_sum_to_prometheus(self): ] ) - collector = _CustomCollector() + collector = _CustomCollector(disable_target_info=True) collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): @@ -189,7 +194,7 @@ def test_gauge_to_prometheus(self): ] ) - collector = _CustomCollector() + collector = _CustomCollector(disable_target_info=True) collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): @@ -251,7 +256,7 @@ def test_list_labels(self): ) ] ) - collector = _CustomCollector() + collector = _CustomCollector(disable_target_info=True) collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): @@ -293,3 +298,46 @@ def test_multiple_collection_calls(self): 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.assertNotEqual(type(prometheus_metric), InfoMetricFamily) + self.assertNotEqual(prometheus_metric.name, "target") + self.assertNotEqual( + prometheus_metric.documentation, "Target metadata" + ) + self.assertNotIn("os", prometheus_metric.samples[0].labels) + self.assertNotIn("histo", prometheus_metric.samples[0].labels)