Repository: open-telemetry/opentelemetry-python

Issue:
Prometheus exporter target_info key-value pairs must be string
**Describe your environment** 
...

**Steps to reproduce**
Prometheus expects labels to be string key-value pairs.
Currently, the labels are not cast to string for the `target_info`

Here is a minimal example which causes this problem:
```python
import time

from prometheus_client import start_http_server

from opentelemetry import metrics
from opentelemetry.exporter.prometheus import PrometheusMetricReader
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.resources import SERVICE_NAME, Resource

resource = Resource(attributes={
    SERVICE_NAME: "your-service-name",
    "foo": "bar",
    "hist": 1,
})

start_http_server(port=8000, addr="localhost")
reader = PrometheusMetricReader()
provider = MeterProvider(resource=resource, metric_readers=[reader])
metrics.set_meter_provider(provider)

meter = metrics.get_meter("my.meter.name")

work_counter = meter.create_counter(
    "work.counter", unit="1", description="Counts the amount of work done"
)

while True:
    work_counter.add(1, {"work.type": "Debug"})
    print("doing some work...")
    time.sleep(1)
```
It raises an exception in the Prometheus client: `AttributeError: ("'int' object has no attribute 'replace'", Metric(target, Target metadata, info ...` here `prometheus_client\openmetrics\exposition.py", line 33, in <listcomp> k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))`

**What is the expected behavior?**
Find a string representation for the `target_info` labels as it is done for the other metrics.

**What is the actual behavior?**
The `target_info` labels are exported in their original representation (non-strings).

**Additional context**
...


Discussion:

Resolving patch:
diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py
--- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py
+++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py
@@ -380,7 +380,8 @@ def _create_info_metric(
         """Create an Info Metric Family with list of attributes"""
         # sanitize the attribute names according to Prometheus rule
         attributes = {
-            self._sanitize(key): value for key, value in attributes.items()
+            self._sanitize(key): self._check_value(value)
+            for key, value in attributes.items()
         }
         info = InfoMetricFamily(name, description, labels=attributes)
         info.add_metric(labels=list(attributes.keys()), value=attributes)


Test patch:
diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py
--- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py
+++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py
@@ -351,30 +351,33 @@ def test_target_info_enabled_by_default(self):
         metric_reader = PrometheusMetricReader()
         provider = MeterProvider(
             metric_readers=[metric_reader],
-            resource=Resource({"os": "Unix", "histo": 1}),
+            resource=Resource({"os": "Unix", "version": "1.2.3"}),
         )
         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")
+        self.assertEqual(len(result), 2)
+
+        prometheus_metric = 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["version"], "1.2.3"
+        )
 
     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}),
+            resource=Resource({"os": "Unix", "version": "1.2.3"}),
         )
         meter = provider.get_meter("getting-started", "0.1.2")
         counter = meter.create_counter("counter")
@@ -388,7 +391,7 @@ def test_target_info_disabled(self):
                 prometheus_metric.documentation, "Target metadata"
             )
             self.assertNotIn("os", prometheus_metric.samples[0].labels)
-            self.assertNotIn("histo", prometheus_metric.samples[0].labels)
+            self.assertNotIn("version", prometheus_metric.samples[0].labels)
 
     def test_target_info_sanitize(self):
         metric_reader = PrometheusMetricReader()
@@ -398,6 +401,8 @@ def test_target_info_sanitize(self):
                 {
                     "system.os": "Unix",
                     "system.name": "Prometheus Target Sanitize",
+                    "histo": 1,
+                    "ratio": 0.1,
                 }
             ),
         )
@@ -411,7 +416,7 @@ def test_target_info_sanitize(self):
         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.assertTrue(len(prometheus_metric.samples[0].labels) == 4)
         self.assertTrue("system_os" in prometheus_metric.samples[0].labels)
         self.assertEqual(
             prometheus_metric.samples[0].labels["system_os"], "Unix"
@@ -421,3 +426,13 @@ def test_target_info_sanitize(self):
             prometheus_metric.samples[0].labels["system_name"],
             "Prometheus Target Sanitize",
         )
+        self.assertTrue("histo" in prometheus_metric.samples[0].labels)
+        self.assertEqual(
+            prometheus_metric.samples[0].labels["histo"],
+            "1",
+        )
+        self.assertTrue("ratio" in prometheus_metric.samples[0].labels)
+        self.assertEqual(
+            prometheus_metric.samples[0].labels["ratio"],
+            "0.1",
+        )