Repository: open-telemetry/opentelemetry-python

Issue:
Configuration class should have convenience methods to access common config options
A lot of instrumentations use very similar configuration options such as `OTEL_PYTHON_<instrumentation>_TRACED_REQUEST_ATTRS` or `OTEL_PYTHON_<instrumentation>_EXCLUDED_URLS`. Each instrumentation duplicates code to read and parse these values using the Configuration class. It would be nice if the Configuration class had convenience methods for such common config options. It could look something like:

```python
cfg = Configuration()
traced_method = cfg.traced_methods("django")
excluded_urls = cfg.excluded_urls("django")
```




Discussion:
@owais There is an [issue](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/147) on contrib repo about adding exclude urls capability to fastapi. It also requires the reading and splitting or returning empty list if evn var isn't set. I will pick this up.
Thanks @lonewolf3739 
@owais I was thinking it would be better to return an instance of `ExcludeList`  when `cfg.excluded_urls(...)` is called. https://github.com/open-telemetry/opentelemetry-python/blob/07b89455f11b00d50d89a884176dc31d1f138228/opentelemetry-api/src/opentelemetry/util/__init__.py#L70-L79

There is a problem here. This `util` has a dependency on `Configuration` to load trace provider and meter provider. If I try to import `ExcludeList` list from here it will introduce a circular dependency. Would there be any concern about moving `ExcludeList` to `Configuraion` file? We can make instrumentations depend on `cfg.excluded_urls`.

Resolving patch:
diff --git a/opentelemetry-api/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py
--- a/opentelemetry-api/src/opentelemetry/configuration/__init__.py
+++ b/opentelemetry-api/src/opentelemetry/configuration/__init__.py
@@ -93,14 +93,26 @@
 to override this value instead of changing it.
 """
 
+import re
 from os import environ
-from re import fullmatch
-from typing import ClassVar, Dict, Optional, TypeVar, Union
+from typing import ClassVar, Dict, List, Optional, Sequence, TypeVar, Union
 
 ConfigValue = Union[str, bool, int, float]
 _T = TypeVar("_T", ConfigValue, Optional[ConfigValue])
 
 
+class ExcludeList:
+    """Class to exclude certain paths (given as a list of regexes) from tracing requests"""
+
+    def __init__(self, excluded_urls: Sequence[str]):
+        self._non_empty = len(excluded_urls) > 0
+        if self._non_empty:
+            self._regex = re.compile("|".join(excluded_urls))
+
+    def url_disabled(self, url: str) -> bool:
+        return bool(self._non_empty and re.search(self._regex, url))
+
+
 class Configuration:
     _instance = None  # type: ClassVar[Optional[Configuration]]
     _config_map = {}  # type: ClassVar[Dict[str, ConfigValue]]
@@ -113,7 +125,7 @@ def __new__(cls) -> "Configuration":
             instance = super().__new__(cls)
             for key, value_str in environ.items():
 
-                match = fullmatch(r"OTEL_(PYTHON_)?([A-Za-z_][\w_]*)", key)
+                match = re.fullmatch(r"OTEL_(PYTHON_)?([A-Za-z_][\w_]*)", key)
 
                 if match is not None:
 
@@ -167,3 +179,20 @@ def _reset(cls) -> None:
         if cls._instance:
             cls._instance._config_map.clear()  # pylint: disable=protected-access
             cls._instance = None
+
+    def _traced_request_attrs(self, instrumentation: str) -> List[str]:
+        """Returns list of traced request attributes for instrumentation."""
+        key = "{}_TRACED_REQUEST_ATTRS".format(instrumentation.upper())
+        value = self._config_map.get(key, "")
+
+        request_attrs = (
+            [attr.strip() for attr in str.split(value, ",")] if value else []  # type: ignore
+        )
+        return request_attrs
+
+    def _excluded_urls(self, instrumentation: str) -> ExcludeList:
+        key = "{}_EXCLUDED_URLS".format(instrumentation.upper())
+        value = self._config_map.get(key, "")
+
+        urls = str.split(value, ",") if value else []  # type: ignore
+        return ExcludeList(urls)
diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py
--- a/opentelemetry-api/src/opentelemetry/util/__init__.py
+++ b/opentelemetry-api/src/opentelemetry/util/__init__.py
@@ -65,15 +65,3 @@ def _load_meter_provider(provider: str) -> "MeterProvider":
 
 def _load_trace_provider(provider: str) -> "TracerProvider":
     return cast("TracerProvider", _load_provider(provider))
-
-
-class ExcludeList:
-    """Class to exclude certain paths (given as a list of regexes) from tracing requests"""
-
-    def __init__(self, excluded_urls: Sequence[str]):
-        self._non_empty = len(excluded_urls) > 0
-        if self._non_empty:
-            self._regex = re.compile("|".join(excluded_urls))
-
-    def url_disabled(self, url: str) -> bool:
-        return bool(self._non_empty and re.search(self._regex, url))


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -10,7 +10,7 @@ env:
   # Otherwise, set variable to the commit of your branch on
   # opentelemetry-python-contrib which is compatible with these Core repo
   # changes.
-  CONTRIB_REPO_SHA: e1d4eeb951694afebb1767b4ea8f593753d894e3
+  CONTRIB_REPO_SHA: fd12b1d624fe44ca17d2c88c0ace39dc80db85df
 
 jobs:
   build:
diff --git a/opentelemetry-api/tests/configuration/test_configuration.py b/opentelemetry-api/tests/configuration/test_configuration.py
--- a/opentelemetry-api/tests/configuration/test_configuration.py
+++ b/opentelemetry-api/tests/configuration/test_configuration.py
@@ -145,3 +145,32 @@ def test_float(self) -> None:
         self.assertEqual(
             Configuration().NON_FLOAT, "-12z3.123"
         )  # pylint: disable=no-member
+
+    @patch.dict(
+        "os.environ",  # type: ignore
+        {
+            "OTEL_PYTHON_WEBFRAMEWORK_TRACED_REQUEST_ATTRS": "content_type,keep_alive",
+        },
+    )
+    def test_traced_request_attrs(self) -> None:
+        cfg = Configuration()
+        request_attrs = cfg._traced_request_attrs("webframework")
+        self.assertEqual(len(request_attrs), 2)
+        self.assertIn("content_type", request_attrs)
+        self.assertIn("keep_alive", request_attrs)
+        self.assertNotIn("authorization", request_attrs)
+
+    @patch.dict(
+        "os.environ",  # type: ignore
+        {
+            "OTEL_PYTHON_WEBFRAMEWORK_EXCLUDED_URLS": "/healthzz,path,/issues/.*/view",
+        },
+    )
+    def test_excluded_urls(self) -> None:
+        cfg = Configuration()
+        excluded_urls = cfg._excluded_urls("webframework")
+        self.assertTrue(excluded_urls.url_disabled("/healthzz"))
+        self.assertTrue(excluded_urls.url_disabled("/path"))
+        self.assertTrue(excluded_urls.url_disabled("/issues/123/view"))
+        self.assertFalse(excluded_urls.url_disabled("/issues"))
+        self.assertFalse(excluded_urls.url_disabled("/hello"))
diff --git a/opentelemetry-api/tests/util/test_exclude_list.py b/opentelemetry-api/tests/configuration/test_exclude_list.py
similarity index 97%
rename from opentelemetry-api/tests/util/test_exclude_list.py
rename to opentelemetry-api/tests/configuration/test_exclude_list.py
--- a/opentelemetry-api/tests/util/test_exclude_list.py
+++ b/opentelemetry-api/tests/configuration/test_exclude_list.py
@@ -13,9 +13,8 @@
 # limitations under the License.
 
 import unittest
-from unittest import mock
 
-from opentelemetry.util import ExcludeList
+from opentelemetry.configuration import ExcludeList
 
 
 class TestExcludeList(unittest.TestCase):