From fa568f0f6f5b6695d6288c7ffeb0ed26c6309500 Mon Sep 17 00:00:00 2001 From: nerstak Date: Sat, 22 Apr 2023 01:34:34 +0200 Subject: [PATCH] feat: exporting resources attributes on target_info (optional) --- .../exporter/prometheus/__init__.py | 35 ++++++++++-- .../tests/test_prometheus_exporter.py | 56 +++++++++++++++++-- 2 files changed, 82 insertions(+), 9 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 7442b7b242d..7b01c53a461 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) != 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 @@ -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""" + 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 1180fac6141..0856562d645 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,44 @@ 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.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)