Repository: open-telemetry/opentelemetry-python

Issue:
Metrics Instrument description must conform with the specified syntax
See [here](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-description).


Discussion:

Resolving patch:
diff --git a/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py
--- a/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py
+++ b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py
@@ -21,12 +21,12 @@
 from re import compile as re_compile
 from typing import (
     Callable,
+    Dict,
     Generator,
     Generic,
     Iterable,
     Optional,
     Sequence,
-    Tuple,
     TypeVar,
     Union,
 )
@@ -74,20 +74,39 @@ def __init__(
         pass
 
     @staticmethod
-    def _check_name_and_unit(name: str, unit: str) -> Tuple[bool, bool]:
+    def _check_name_unit_description(
+        name: str, unit: str, description: str
+    ) -> Dict[str, Optional[str]]:
         """
-        Checks the following instrument name and unit for compliance with the
-        spec.
+        Checks the following instrument name, unit and description for
+        compliance with the spec.
 
-        Returns a tuple of boolean value, the first one will be `True` if the
-        name is valid, `False` otherwise. The second value will be `True` if
-        the unit is valid, `False` otherwise.
+        Returns a dict with keys "name", "unit" and "description", the
+        corresponding values will be the checked strings or `None` if the value
+        is invalid. If valid, the checked strings should be used instead of the
+        original values.
         """
 
-        return (
-            _name_regex.fullmatch(name) is not None,
-            _unit_regex.fullmatch(unit) is not None,
-        )
+        result: Dict[str, Optional[str]] = {}
+
+        if _name_regex.fullmatch(name) is not None:
+            result["name"] = name
+        else:
+            result["name"] = None
+
+        if unit is None:
+            unit = ""
+        if _unit_regex.fullmatch(unit) is not None:
+            result["unit"] = unit
+        else:
+            result["unit"] = None
+
+        if description is None:
+            result["description"] = ""
+        else:
+            result["description"] = description
+
+        return result
 
 
 class _ProxyInstrument(ABC, Generic[InstrumentT]):
diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py
--- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py
+++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py
@@ -50,13 +50,18 @@ def __init__(
         description: str = "",
     ):
         # pylint: disable=no-member
-        is_name_valid, is_unit_valid = self._check_name_and_unit(name, unit)
+        result = self._check_name_unit_description(name, unit, description)
 
-        if not is_name_valid:
+        if result["name"] is None:
             raise Exception(_ERROR_MESSAGE.format(name))
 
-        if not is_unit_valid:
+        if result["unit"] is None:
             raise Exception(_ERROR_MESSAGE.format(unit))
+
+        name = result["name"]
+        unit = result["unit"]
+        description = result["description"]
+
         self.name = name.lower()
         self.unit = unit
         self.description = description
@@ -76,13 +81,18 @@ def __init__(
         description: str = "",
     ):
         # pylint: disable=no-member
-        is_name_valid, is_unit_valid = self._check_name_and_unit(name, unit)
+        result = self._check_name_unit_description(name, unit, description)
 
-        if not is_name_valid:
+        if result["name"] is None:
             raise Exception(_ERROR_MESSAGE.format(name))
 
-        if not is_unit_valid:
+        if result["unit"] is None:
             raise Exception(_ERROR_MESSAGE.format(unit))
+
+        name = result["name"]
+        unit = result["unit"]
+        description = result["description"]
+
         self.name = name.lower()
         self.unit = unit
         self.description = description


Test patch:
diff --git a/opentelemetry-api/tests/metrics/test_instruments.py b/opentelemetry-api/tests/metrics/test_instruments.py
--- a/opentelemetry-api/tests/metrics/test_instruments.py
+++ b/opentelemetry-api/tests/metrics/test_instruments.py
@@ -563,27 +563,108 @@ def test_observable_up_down_counter_callback(self):
             Signature.empty,
         )
 
-    def test_name_regex(self):
+    def test_name_check(self):
 
         instrument = ChildInstrument("name")
 
-        self.assertTrue(instrument._check_name_and_unit("a" * 63, "unit")[0])
-        self.assertTrue(instrument._check_name_and_unit("a.", "unit")[0])
-        self.assertTrue(instrument._check_name_and_unit("a-", "unit")[0])
-        self.assertTrue(instrument._check_name_and_unit("a_", "unit")[0])
-
-        self.assertFalse(instrument._check_name_and_unit("a" * 64, "unit")[0])
-        self.assertFalse(instrument._check_name_and_unit("Ñ", "unit")[0])
-        self.assertFalse(instrument._check_name_and_unit("_a", "unit")[0])
-        self.assertFalse(instrument._check_name_and_unit("1a", "unit")[0])
-        self.assertFalse(instrument._check_name_and_unit("", "unit")[0])
+        self.assertEqual(
+            instrument._check_name_unit_description(
+                "a" * 63, "unit", "description"
+            )["name"],
+            "a" * 63,
+        )
+        self.assertEqual(
+            instrument._check_name_unit_description(
+                "a.", "unit", "description"
+            )["name"],
+            "a.",
+        )
+        self.assertEqual(
+            instrument._check_name_unit_description(
+                "a-", "unit", "description"
+            )["name"],
+            "a-",
+        )
+        self.assertEqual(
+            instrument._check_name_unit_description(
+                "a_", "unit", "description"
+            )["name"],
+            "a_",
+        )
+
+        self.assertIsNone(
+            instrument._check_name_unit_description(
+                "a" * 64, "unit", "description"
+            )["name"]
+        )
+        self.assertIsNone(
+            instrument._check_name_unit_description(
+                "Ñ", "unit", "description"
+            )["name"]
+        )
+        self.assertIsNone(
+            instrument._check_name_unit_description(
+                "_a", "unit", "description"
+            )["name"]
+        )
+        self.assertIsNone(
+            instrument._check_name_unit_description(
+                "1a", "unit", "description"
+            )["name"]
+        )
+        self.assertIsNone(
+            instrument._check_name_unit_description("", "unit", "description")[
+                "name"
+            ]
+        )
 
-    def test_unit_regex(self):
+    def test_unit_check(self):
 
         instrument = ChildInstrument("name")
 
-        self.assertTrue(instrument._check_name_and_unit("name", "a" * 63)[1])
-        self.assertTrue(instrument._check_name_and_unit("name", "{a}")[1])
+        self.assertEqual(
+            instrument._check_name_unit_description(
+                "name", "a" * 63, "description"
+            )["unit"],
+            "a" * 63,
+        )
+        self.assertEqual(
+            instrument._check_name_unit_description(
+                "name", "{a}", "description"
+            )["unit"],
+            "{a}",
+        )
+
+        self.assertIsNone(
+            instrument._check_name_unit_description(
+                "name", "a" * 64, "description"
+            )["unit"]
+        )
+        self.assertIsNone(
+            instrument._check_name_unit_description(
+                "name", "Ñ", "description"
+            )["unit"]
+        )
+        self.assertEqual(
+            instrument._check_name_unit_description(
+                "name", None, "description"
+            )["unit"],
+            "",
+        )
+
+    def test_description_check(self):
 
-        self.assertFalse(instrument._check_name_and_unit("name", "a" * 64)[1])
-        self.assertFalse(instrument._check_name_and_unit("name", "Ñ")[1])
+        instrument = ChildInstrument("name")
+
+        self.assertEqual(
+            instrument._check_name_unit_description(
+                "name", "unit", "description"
+            )["description"],
+            "description",
+        )
+        self.assertEqual(
+            instrument._check_name_unit_description("name", "unit", None)[
+                "description"
+            ],
+            "",
+        )