Skip to content

Commit c8b336d

Browse files
authored
api: Make the configuration object universal (#563)
The configuration object used to provide only configuration for the meter and tracer providers. Now it can be used to load any configuration value stored in an environment variable that starts with OPENTELEMETRY_PYTHON_ whose characters match with [A-Z_]. All this is explained with greater detail in the documentation. The documentation also includes a section that gathers and explains all the current environment variables that are meaningful for OpenTelemetry Python. In this way, the end user can have them all listed in one single place. If in the future, more environment variables are used, then they should be added there and documented accordingly.
1 parent b44bf41 commit c8b336d

5 files changed

Lines changed: 110 additions & 152 deletions

File tree

opentelemetry-api/src/opentelemetry/configuration/__init__.py

Lines changed: 61 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,42 @@
1818
"""
1919
Simple configuration manager
2020
21-
This is a configuration manager for the Tracer and Meter providers. It reads
22-
configuration from environment variables prefixed with
23-
``OPENTELEMETRY_PYTHON_``:
21+
This is a configuration manager for OpenTelemetry. It reads configuration
22+
values from environment variables prefixed with
23+
``OPENTELEMETRY_PYTHON_`` whose characters are only all caps and underscores.
24+
The first character after ``OPENTELEMETRY_PYTHON_`` must be an uppercase
25+
character.
2426
25-
1. ``OPENTELEMETRY_PYTHON_TRACER_PROVIDER``
26-
2. ``OPENTELEMETRY_PYTHON_METER_PROVIDER``
27+
For example, these environment variables will be read:
28+
29+
1. ``OPENTELEMETRY_PYTHON_SOMETHING``
30+
2. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_``
31+
3. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_AND__ELSE``
32+
33+
These won't:
34+
35+
1. ``OPENTELEMETRY_PYTH_SOMETHING``
36+
2. ``OPENTELEMETRY_PYTHON_something``
37+
3. ``OPENTELEMETRY_PYTHON_SOMETHING_2_AND__ELSE``
38+
4. ``OPENTELEMETRY_PYTHON_SOMETHING_%_ELSE``
39+
40+
The values stored in the environment variables can be found in an instance of
41+
``opentelemetry.configuration.Configuration``. This class can be instantiated
42+
freely because instantiating it returns a singleton.
43+
44+
For example, if the environment variable
45+
``OPENTELEMETRY_PYTHON_METER_PROVIDER`` value is ``my_meter_provider``, then
46+
``Configuration().meter_provider == "my_meter_provider"`` would be ``True``.
47+
48+
Non defined attributes will always return ``None``. This is intended to make it
49+
easier to use the ``Configuration`` object in actual code, because it won't be
50+
necessary to check for the attribute to be defined first.
51+
52+
Environment variables used by OpenTelemetry
53+
-------------------------------------------
54+
55+
1. OPENTELEMETRY_PYTHON_METER_PROVIDER
56+
2. OPENTELEMETRY_PYTHON_TRACER_PROVIDER
2757
2858
The value of these environment variables should be the name of the entry point
2959
that points to the class that implements either provider. This OpenTelemetry
@@ -47,85 +77,46 @@
4777
"default_meter_provider" (this is not actually necessary since the
4878
OpenTelemetry API provided providers are the default ones used if no
4979
configuration is found in the environment variables).
50-
51-
Once this is done, the configuration manager can be used by simply importing
52-
it from opentelemetry.configuration.Configuration. This is a class that can
53-
be instantiated as many times as needed without concern because it will
54-
always produce the same instance. Its attributes are lazy loaded and they
55-
hold an instance of their corresponding provider. So, for example, to get
56-
the configured meter provider::
57-
58-
from opentelemetry.configuration import Configuration
59-
60-
tracer_provider = Configuration().tracer_provider
61-
6280
"""
6381

64-
from logging import getLogger
6582
from os import environ
66-
67-
from pkg_resources import iter_entry_points
68-
69-
logger = getLogger(__name__)
83+
from re import fullmatch
7084

7185

7286
class Configuration:
7387
_instance = None
7488

75-
__slots__ = ("tracer_provider", "meter_provider")
89+
__slots__ = []
7690

7791
def __new__(cls) -> "Configuration":
7892
if Configuration._instance is None:
7993

80-
configuration = {
81-
key: "default_{}".format(key) for key in cls.__slots__
82-
}
83-
84-
for key, value in configuration.items():
85-
configuration[key] = environ.get(
86-
"OPENTELEMETRY_PYTHON_{}".format(key.upper()), value
87-
)
88-
89-
for key, value in configuration.items():
90-
underscored_key = "_{}".format(key)
91-
92-
setattr(Configuration, underscored_key, None)
93-
setattr(
94-
Configuration,
95-
key,
96-
property(
97-
fget=lambda cls, local_key=key, local_value=value: cls._load(
98-
key=local_key, value=local_value
99-
)
100-
),
101-
)
94+
for key, value in environ.items():
95+
96+
match = fullmatch("OPENTELEMETRY_PYTHON_([A-Z][A-Z_]*)", key)
97+
98+
if match is not None:
99+
100+
key = match.group(1).lower()
101+
102+
setattr(Configuration, "_{}".format(key), value)
103+
setattr(
104+
Configuration,
105+
key,
106+
property(
107+
fget=lambda cls, key=key: getattr(
108+
cls, "_{}".format(key)
109+
)
110+
),
111+
)
112+
113+
Configuration.__slots__.append(key)
114+
115+
Configuration.__slots__ = tuple(Configuration.__slots__)
102116

103117
Configuration._instance = object.__new__(cls)
104118

105119
return cls._instance
106120

107-
@classmethod
108-
def _load(cls, key=None, value=None):
109-
underscored_key = "_{}".format(key)
110-
111-
if getattr(cls, underscored_key) is None:
112-
try:
113-
setattr(
114-
cls,
115-
underscored_key,
116-
next(
117-
iter_entry_points(
118-
"opentelemetry_{}".format(key), name=value,
119-
)
120-
).load()(),
121-
)
122-
except Exception: # pylint: disable=broad-except
123-
# FIXME Decide on how to handle this. Should an exception be
124-
# raised here, or only a message should be logged and should
125-
# we fall back to the default meter provider?
126-
logger.error(
127-
"Failed to load configured provider %s", value,
128-
)
129-
raise
130-
131-
return getattr(cls, underscored_key)
121+
def __getattr__(self, name):
122+
return None

opentelemetry-api/src/opentelemetry/metrics/__init__.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from logging import getLogger
3535
from typing import Callable, Dict, Sequence, Tuple, Type, TypeVar
3636

37-
from opentelemetry.configuration import Configuration # type: ignore
37+
from opentelemetry.util import _load_provider
3838

3939
logger = getLogger(__name__)
4040
ValueT = TypeVar("ValueT", int, float)
@@ -410,8 +410,6 @@ def get_meter_provider() -> MeterProvider:
410410
global _METER_PROVIDER # pylint: disable=global-statement
411411

412412
if _METER_PROVIDER is None:
413-
_METER_PROVIDER = (
414-
Configuration().meter_provider # type: ignore # pylint: disable=no-member
415-
)
413+
_METER_PROVIDER = _load_provider("meter_provider")
416414

417-
return _METER_PROVIDER # type: ignore
415+
return _METER_PROVIDER

opentelemetry-api/src/opentelemetry/trace/__init__.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,8 @@
7777
from contextlib import contextmanager
7878
from logging import getLogger
7979

80-
from opentelemetry.configuration import Configuration # type: ignore
8180
from opentelemetry.trace.status import Status
82-
from opentelemetry.util import types
81+
from opentelemetry.util import _load_provider, types
8382

8483
logger = getLogger(__name__)
8584

@@ -701,8 +700,6 @@ def get_tracer_provider() -> TracerProvider:
701700
global _TRACER_PROVIDER # pylint: disable=global-statement
702701

703702
if _TRACER_PROVIDER is None:
704-
_TRACER_PROVIDER = (
705-
Configuration().tracer_provider # type: ignore # pylint: disable=no-member
706-
)
703+
_TRACER_PROVIDER = _load_provider("tracer_provider")
707704

708-
return _TRACER_PROVIDER # type: ignore
705+
return _TRACER_PROVIDER

opentelemetry-api/src/opentelemetry/util/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
import time
15+
from logging import getLogger
16+
from typing import Union
17+
18+
from pkg_resources import iter_entry_points
19+
20+
from opentelemetry.configuration import Configuration # type: ignore
21+
22+
logger = getLogger(__name__)
1523

1624
# Since we want API users to be able to provide timestamps,
1725
# this needs to be in the API.
@@ -23,3 +31,20 @@
2331

2432
def time_ns() -> int:
2533
return int(time.time() * 1e9)
34+
35+
36+
def _load_provider(provider: str) -> Union["TracerProvider", "MeterProvider"]: # type: ignore
37+
try:
38+
return next( # type: ignore
39+
iter_entry_points(
40+
"opentelemetry_{}".format(provider),
41+
name=getattr( # type: ignore
42+
Configuration(), provider, "default_{}".format(provider), # type: ignore
43+
),
44+
)
45+
).load()()
46+
except Exception: # pylint: disable=broad-except
47+
logger.error(
48+
"Failed to load configured provider %s", provider,
49+
)
50+
raise

opentelemetry-api/tests/configuration/test_configuration.py

Lines changed: 18 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -11,104 +11,51 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
# pylint: disable-all
1415

15-
from json import dumps
1616
from unittest import TestCase
1717
from unittest.mock import patch
1818

19-
from pytest import fixture # type: ignore # pylint: disable=import-error
20-
2119
from opentelemetry.configuration import Configuration # type: ignore
2220

2321

2422
class TestConfiguration(TestCase):
25-
class IterEntryPointsMock:
26-
def __init__(
27-
self, argument, name=None
28-
): # pylint: disable=unused-argument
29-
self._name = name
30-
31-
def __next__(self):
32-
return self
33-
34-
def __call__(self):
35-
return self._name
36-
37-
def load(self):
38-
return self
39-
40-
@fixture(autouse=True)
41-
def configdir(self, tmpdir): # type: ignore # pylint: disable=no-self-use
42-
tmpdir.chdir()
43-
tmpdir.mkdir(".config").join("opentelemetry_python.json").write(
44-
dumps({"tracer_provider": "overridden_tracer_provider"})
45-
)
46-
4723
def setUp(self):
48-
Configuration._instance = None # pylint: disable=protected-access
24+
from opentelemetry.configuration import Configuration # type: ignore
4925

5026
def tearDown(self):
51-
Configuration._instance = None # pylint: disable=protected-access
27+
from opentelemetry.configuration import Configuration # type: ignore
5228

5329
def test_singleton(self):
30+
self.assertIsInstance(Configuration(), Configuration)
5431
self.assertIs(Configuration(), Configuration())
5532

56-
@patch(
57-
"opentelemetry.configuration.iter_entry_points",
58-
**{"side_effect": IterEntryPointsMock} # type: ignore
59-
)
60-
def test_lazy( # type: ignore
61-
self, mock_iter_entry_points, # pylint: disable=unused-argument
62-
):
63-
configuration = Configuration()
64-
65-
self.assertIsNone(
66-
configuration._tracer_provider # pylint: disable=no-member,protected-access
67-
)
68-
69-
configuration.tracer_provider # pylint: disable=pointless-statement
70-
71-
self.assertEqual(
72-
configuration._tracer_provider, # pylint: disable=no-member,protected-access
73-
"default_tracer_provider",
74-
)
75-
76-
@patch(
77-
"opentelemetry.configuration.iter_entry_points",
78-
**{"side_effect": IterEntryPointsMock} # type: ignore
33+
@patch.dict(
34+
"os.environ", # type: ignore
35+
{
36+
"OPENTELEMETRY_PYTHON_METER_PROVIDER": "meter_provider",
37+
"OPENTELEMETRY_PYTHON_TRACER_PROVIDER": "tracer_provider",
38+
},
7939
)
80-
def test_default_values( # type: ignore
81-
self, mock_iter_entry_points # pylint: disable=unused-argument
82-
):
40+
def test_environment_variables(self): # type: ignore
8341
self.assertEqual(
84-
Configuration().tracer_provider, "default_tracer_provider"
42+
Configuration().meter_provider, "meter_provider"
8543
) # pylint: disable=no-member
8644
self.assertEqual(
87-
Configuration().meter_provider, "default_meter_provider"
45+
Configuration().tracer_provider, "tracer_provider"
8846
) # pylint: disable=no-member
8947

90-
@patch(
91-
"opentelemetry.configuration.iter_entry_points",
92-
**{"side_effect": IterEntryPointsMock} # type: ignore
93-
)
9448
@patch.dict(
95-
"os.environ",
96-
{"OPENTELEMETRY_PYTHON_METER_PROVIDER": "overridden_meter_provider"},
49+
"os.environ", # type: ignore
50+
{"OPENTELEMETRY_PYTHON_TRACER_PROVIDER": "tracer_provider"},
9751
)
98-
def test_environment_variables( # type: ignore
99-
self, mock_iter_entry_points # pylint: disable=unused-argument
100-
): # type: ignore
101-
self.assertEqual(
102-
Configuration().tracer_provider, "default_tracer_provider"
103-
) # pylint: disable=no-member
104-
self.assertEqual(
105-
Configuration().meter_provider, "overridden_meter_provider"
106-
) # pylint: disable=no-member
107-
10852
def test_property(self):
10953
with self.assertRaises(AttributeError):
11054
Configuration().tracer_provider = "new_tracer_provider"
11155

11256
def test_slots(self):
11357
with self.assertRaises(AttributeError):
11458
Configuration().xyz = "xyz" # pylint: disable=assigning-non-slot
59+
60+
def test_getattr(self):
61+
Configuration().xyz is None

0 commit comments

Comments
 (0)