Repository: open-telemetry/opentelemetry-python

Issue:
Prometheus metric target_info with SERVICE_NAME contains a dot
**Describe your environment**
- Python = 3.11.4
- opentelemetry-api = 1.21.0
- opentelemetry-sdk = 1.21.0
- opentelemetry-exporter-prometheus = 0.42b0
- prometheus-client = 0.19.0
- Prometheus = 2.48.0
- Windows

**Steps to reproduce**
When using the Prometheus exporter and initializing the `Resource` with the attribute `opentelemetry.sdk.resources.SERVICE_NAME` as suggested in the [docs](https://opentelemetry.io/docs/instrumentation/python/exporters/#prometheus), a metric `target_info` gets exported with the attribute `service.name`.
Prometheus does not like the dot `.` in the attribute name and fails scraping the target.

Minimal code to reproduce the issue:
```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

# Service name is required for most backends
resource = Resource(attributes={
    SERVICE_NAME: "your-service-name"
})

# Start Prometheus client
start_http_server(port=8000, addr="localhost")
# Initialize PrometheusMetricReader which pulls metrics from the SDK
# on-demand to respond to scrape requests
reader = PrometheusMetricReader()
provider = MeterProvider(resource=resource, metric_readers=[reader])
metrics.set_meter_provider(provider)

# Creates a meter from the global meter 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)
```

When browsing to `localhost:8000/metrics` these are the metrics:
```
# HELP python_gc_objects_collected_total Objects collected during gc
# TYPE python_gc_objects_collected_total counter
python_gc_objects_collected_total{generation="0"} 356.0
python_gc_objects_collected_total{generation="1"} 289.0
python_gc_objects_collected_total{generation="2"} 0.0
# HELP python_gc_objects_uncollectable_total Uncollectable objects found during GC
# TYPE python_gc_objects_uncollectable_total counter
python_gc_objects_uncollectable_total{generation="0"} 0.0
python_gc_objects_uncollectable_total{generation="1"} 0.0
python_gc_objects_uncollectable_total{generation="2"} 0.0
# HELP python_gc_collections_total Number of times this generation was collected
# TYPE python_gc_collections_total counter
python_gc_collections_total{generation="0"} 55.0
python_gc_collections_total{generation="1"} 4.0
python_gc_collections_total{generation="2"} 0.0
# HELP python_info Python platform information
# TYPE python_info gauge
python_info{implementation="CPython",major="3",minor="11",patchlevel="4",version="3.11.4"} 1.0
# HELP target_info Target metadata
# TYPE target_info gauge
target_info{service.name="your-service-name"} 1.0
# HELP work_counter_1_total Counts the amount of work done
# TYPE work_counter_1_total counter
work_counter_1_total{work_type="Debug"} 9.0
```

When checking the status of the target in the Prometheus UI, the following error is displayed: `expected equal, got "." ("INVALID") while parsing: "target_info{service."` because of the dot `.`.

**What is the expected behavior?**
The attribute of the metric `target_info` should not have a dot `.` in its name.
E.g. `service_name`.

**What is the actual behavior?**
The attribute of the metric `target_info` has a dot `.` in its name `service.name` (`target_info{service.name="your-service-name"} 1.0`).



Discussion:
I was able to find the cause of the issue, it is this line https://github.com/open-telemetry/opentelemetry-python/blob/3dfe2249cc4a203bf24578483b192fec7266596b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py#L382
for each key of `attributes` the method `_sanitize` has to be called
https://github.com/open-telemetry/opentelemetry-python/blob/3dfe2249cc4a203bf24578483b192fec7266596b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py#L364
I'll write a pull request to fix this issue.

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
@@ -378,6 +378,10 @@ def _create_info_metric(
         self, name: str, description: str, attributes: Dict[str, str]
     ) -> InfoMetricFamily:
         """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()
+        }
         info = InfoMetricFamily(name, description, labels=attributes)
         info.add_metric(labels=list(attributes.keys()), value=attributes)
         return info


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
@@ -389,3 +389,35 @@ def test_target_info_disabled(self):
             )
             self.assertNotIn("os", prometheus_metric.samples[0].labels)
             self.assertNotIn("histo", prometheus_metric.samples[0].labels)
+
+    def test_target_info_sanitize(self):
+        metric_reader = PrometheusMetricReader()
+        provider = MeterProvider(
+            metric_readers=[metric_reader],
+            resource=Resource(
+                {
+                    "system.os": "Unix",
+                    "system.name": "Prometheus Target Sanitize",
+                }
+            ),
+        )
+        meter = provider.get_meter("getting-started", "0.1.2")
+        counter = meter.create_counter("counter")
+        counter.add(1)
+        prometheus_metric = list(metric_reader._collector.collect())[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.assertTrue("system_os" in prometheus_metric.samples[0].labels)
+        self.assertEqual(
+            prometheus_metric.samples[0].labels["system_os"], "Unix"
+        )
+        self.assertTrue("system_name" in prometheus_metric.samples[0].labels)
+        self.assertEqual(
+            prometheus_metric.samples[0].labels["system_name"],
+            "Prometheus Target Sanitize",
+        )