diff --git a/opentelemetry-api/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py index 850f0d4fe0..9b23c52bee 100644 --- a/opentelemetry-api/src/opentelemetry/configuration/__init__.py +++ b/opentelemetry-api/src/opentelemetry/configuration/__init__.py @@ -18,12 +18,42 @@ """ Simple configuration manager -This is a configuration manager for the Tracer and Meter providers. It reads -configuration from environment variables prefixed with -``OPENTELEMETRY_PYTHON_``: +This is a configuration manager for OpenTelemetry. It reads configuration +values from environment variables prefixed with +``OPENTELEMETRY_PYTHON_`` whose characters are only all caps and underscores. +The first character after ``OPENTELEMETRY_PYTHON_`` must be an uppercase +character. -1. ``OPENTELEMETRY_PYTHON_TRACER_PROVIDER`` -2. ``OPENTELEMETRY_PYTHON_METER_PROVIDER`` +For example, these environment variables will be read: + +1. ``OPENTELEMETRY_PYTHON_SOMETHING`` +2. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_`` +3. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_AND__ELSE`` + +These won't: + +1. ``OPENTELEMETRY_PYTH_SOMETHING`` +2. ``OPENTELEMETRY_PYTHON_something`` +3. ``OPENTELEMETRY_PYTHON_SOMETHING_2_AND__ELSE`` +4. ``OPENTELEMETRY_PYTHON_SOMETHING_%_ELSE`` + +The values stored in the environment variables can be found in an instance of +``opentelemetry.configuration.Configuration``. This class can be instantiated +freely because instantiating it returns a singleton. + +For example, if the environment variable +``OPENTELEMETRY_PYTHON_METER_PROVIDER`` value is ``my_meter_provider``, then +``Configuration().meter_provider == "my_meter_provider"`` would be ``True``. + +Non defined attributes will always return ``None``. This is intended to make it +easier to use the ``Configuration`` object in actual code, because it won't be +necessary to check for the attribute to be defined first. + +Environment variables used by OpenTelemetry +------------------------------------------- + +1. OPENTELEMETRY_PYTHON_METER_PROVIDER +2. OPENTELEMETRY_PYTHON_TRACER_PROVIDER The value of these environment variables should be the name of the entry point that points to the class that implements either provider. This OpenTelemetry @@ -47,85 +77,46 @@ "default_meter_provider" (this is not actually necessary since the OpenTelemetry API provided providers are the default ones used if no configuration is found in the environment variables). - -Once this is done, the configuration manager can be used by simply importing -it from opentelemetry.configuration.Configuration. This is a class that can -be instantiated as many times as needed without concern because it will -always produce the same instance. Its attributes are lazy loaded and they -hold an instance of their corresponding provider. So, for example, to get -the configured meter provider:: - - from opentelemetry.configuration import Configuration - - tracer_provider = Configuration().tracer_provider - """ -from logging import getLogger from os import environ - -from pkg_resources import iter_entry_points - -logger = getLogger(__name__) +from re import fullmatch class Configuration: _instance = None - __slots__ = ("tracer_provider", "meter_provider") + __slots__ = [] def __new__(cls) -> "Configuration": if Configuration._instance is None: - configuration = { - key: "default_{}".format(key) for key in cls.__slots__ - } - - for key, value in configuration.items(): - configuration[key] = environ.get( - "OPENTELEMETRY_PYTHON_{}".format(key.upper()), value - ) - - for key, value in configuration.items(): - underscored_key = "_{}".format(key) - - setattr(Configuration, underscored_key, None) - setattr( - Configuration, - key, - property( - fget=lambda cls, local_key=key, local_value=value: cls._load( - key=local_key, value=local_value - ) - ), - ) + for key, value in environ.items(): + + match = fullmatch("OPENTELEMETRY_PYTHON_([A-Z][A-Z_]*)", key) + + if match is not None: + + key = match.group(1).lower() + + setattr(Configuration, "_{}".format(key), value) + setattr( + Configuration, + key, + property( + fget=lambda cls, key=key: getattr( + cls, "_{}".format(key) + ) + ), + ) + + Configuration.__slots__.append(key) + + Configuration.__slots__ = tuple(Configuration.__slots__) Configuration._instance = object.__new__(cls) return cls._instance - @classmethod - def _load(cls, key=None, value=None): - underscored_key = "_{}".format(key) - - if getattr(cls, underscored_key) is None: - try: - setattr( - cls, - underscored_key, - next( - iter_entry_points( - "opentelemetry_{}".format(key), name=value, - ) - ).load()(), - ) - except Exception: # pylint: disable=broad-except - # FIXME Decide on how to handle this. Should an exception be - # raised here, or only a message should be logged and should - # we fall back to the default meter provider? - logger.error( - "Failed to load configured provider %s", value, - ) - raise - - return getattr(cls, underscored_key) + def __getattr__(self, name): + return None diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index c0ab525b7d..b3fe69ab59 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -34,7 +34,7 @@ from logging import getLogger from typing import Callable, Dict, Sequence, Tuple, Type, TypeVar -from opentelemetry.configuration import Configuration # type: ignore +from opentelemetry.util import _load_provider logger = getLogger(__name__) ValueT = TypeVar("ValueT", int, float) @@ -410,8 +410,6 @@ def get_meter_provider() -> MeterProvider: global _METER_PROVIDER # pylint: disable=global-statement if _METER_PROVIDER is None: - _METER_PROVIDER = ( - Configuration().meter_provider # type: ignore # pylint: disable=no-member - ) + _METER_PROVIDER = _load_provider("meter_provider") - return _METER_PROVIDER # type: ignore + return _METER_PROVIDER diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 856745e077..773a3908ce 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -77,9 +77,8 @@ from contextlib import contextmanager from logging import getLogger -from opentelemetry.configuration import Configuration # type: ignore from opentelemetry.trace.status import Status -from opentelemetry.util import types +from opentelemetry.util import _load_provider, types logger = getLogger(__name__) @@ -701,8 +700,6 @@ def get_tracer_provider() -> TracerProvider: global _TRACER_PROVIDER # pylint: disable=global-statement if _TRACER_PROVIDER is None: - _TRACER_PROVIDER = ( - Configuration().tracer_provider # type: ignore # pylint: disable=no-member - ) + _TRACER_PROVIDER = _load_provider("tracer_provider") - return _TRACER_PROVIDER # type: ignore + return _TRACER_PROVIDER diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py index 9f68ef2d39..8701d9ffba 100644 --- a/opentelemetry-api/src/opentelemetry/util/__init__.py +++ b/opentelemetry-api/src/opentelemetry/util/__init__.py @@ -12,6 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. import time +from logging import getLogger +from typing import Union + +from pkg_resources import iter_entry_points + +from opentelemetry.configuration import Configuration # type: ignore + +logger = getLogger(__name__) # Since we want API users to be able to provide timestamps, # this needs to be in the API. @@ -23,3 +31,20 @@ def time_ns() -> int: return int(time.time() * 1e9) + + +def _load_provider(provider: str) -> Union["TracerProvider", "MeterProvider"]: # type: ignore + try: + return next( # type: ignore + iter_entry_points( + "opentelemetry_{}".format(provider), + name=getattr( # type: ignore + Configuration(), provider, "default_{}".format(provider), # type: ignore + ), + ) + ).load()() + except Exception: # pylint: disable=broad-except + logger.error( + "Failed to load configured provider %s", provider, + ) + raise diff --git a/opentelemetry-api/tests/configuration/test_configuration.py b/opentelemetry-api/tests/configuration/test_configuration.py index c69c09b52b..d5a6363091 100644 --- a/opentelemetry-api/tests/configuration/test_configuration.py +++ b/opentelemetry-api/tests/configuration/test_configuration.py @@ -11,100 +11,44 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable-all -from json import dumps from unittest import TestCase from unittest.mock import patch -from pytest import fixture # type: ignore # pylint: disable=import-error - from opentelemetry.configuration import Configuration # type: ignore class TestConfiguration(TestCase): - class IterEntryPointsMock: - def __init__( - self, argument, name=None - ): # pylint: disable=unused-argument - self._name = name - - def __next__(self): - return self - - def __call__(self): - return self._name - - def load(self): - return self - - @fixture(autouse=True) - def configdir(self, tmpdir): # type: ignore # pylint: disable=no-self-use - tmpdir.chdir() - tmpdir.mkdir(".config").join("opentelemetry_python.json").write( - dumps({"tracer_provider": "overridden_tracer_provider"}) - ) - def setUp(self): - Configuration._instance = None # pylint: disable=protected-access + from opentelemetry.configuration import Configuration # type: ignore def tearDown(self): - Configuration._instance = None # pylint: disable=protected-access + from opentelemetry.configuration import Configuration # type: ignore def test_singleton(self): + self.assertIsInstance(Configuration(), Configuration) self.assertIs(Configuration(), Configuration()) - @patch( - "opentelemetry.configuration.iter_entry_points", - **{"side_effect": IterEntryPointsMock} # type: ignore - ) - def test_lazy( # type: ignore - self, mock_iter_entry_points, # pylint: disable=unused-argument - ): - configuration = Configuration() - - self.assertIsNone( - configuration._tracer_provider # pylint: disable=no-member,protected-access - ) - - configuration.tracer_provider # pylint: disable=pointless-statement - - self.assertEqual( - configuration._tracer_provider, # pylint: disable=no-member,protected-access - "default_tracer_provider", - ) - - @patch( - "opentelemetry.configuration.iter_entry_points", - **{"side_effect": IterEntryPointsMock} # type: ignore + @patch.dict( + "os.environ", # type: ignore + { + "OPENTELEMETRY_PYTHON_METER_PROVIDER": "meter_provider", + "OPENTELEMETRY_PYTHON_TRACER_PROVIDER": "tracer_provider", + }, ) - def test_default_values( # type: ignore - self, mock_iter_entry_points # pylint: disable=unused-argument - ): + def test_environment_variables(self): # type: ignore self.assertEqual( - Configuration().tracer_provider, "default_tracer_provider" + Configuration().meter_provider, "meter_provider" ) # pylint: disable=no-member self.assertEqual( - Configuration().meter_provider, "default_meter_provider" + Configuration().tracer_provider, "tracer_provider" ) # pylint: disable=no-member - @patch( - "opentelemetry.configuration.iter_entry_points", - **{"side_effect": IterEntryPointsMock} # type: ignore - ) @patch.dict( - "os.environ", - {"OPENTELEMETRY_PYTHON_METER_PROVIDER": "overridden_meter_provider"}, + "os.environ", # type: ignore + {"OPENTELEMETRY_PYTHON_TRACER_PROVIDER": "tracer_provider"}, ) - def test_environment_variables( # type: ignore - self, mock_iter_entry_points # pylint: disable=unused-argument - ): # type: ignore - self.assertEqual( - Configuration().tracer_provider, "default_tracer_provider" - ) # pylint: disable=no-member - self.assertEqual( - Configuration().meter_provider, "overridden_meter_provider" - ) # pylint: disable=no-member - def test_property(self): with self.assertRaises(AttributeError): Configuration().tracer_provider = "new_tracer_provider" @@ -112,3 +56,6 @@ def test_property(self): def test_slots(self): with self.assertRaises(AttributeError): Configuration().xyz = "xyz" # pylint: disable=assigning-non-slot + + def test_getattr(self): + Configuration().xyz is None