Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/5699.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-sdk`: added support for `OTEL_PYTHON*` environment variables to be used when OTEL_CONFIG_FILE is set
13 changes: 7 additions & 6 deletions docs/sdk/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,13 @@ Behavior notes
components the file enables (for example resource detectors) and via
``${env:VAR}`` substitution.
* Python-implementation extensions (``OTEL_PYTHON_*`` variables such as
``OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED`` or
``OTEL_PYTHON_TRACER_CONFIGURATOR``) are **not** applied when
``OTEL_CONFIG_FILE`` is set: the env-var initialisation path is skipped
entirely. If your app currently relies on one of these and you are
migrating to a config file, plan to capture the equivalent behaviour in
the file (or in code) instead.
``OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED`` and
``OTEL_PYTHON_TRACER_CONFIGURATOR``) are applied after the configured
providers are created. The logging variable adds the SDK logging handler;
tracer, meter, and logger configurators replace the corresponding
provider's default configurator when that provider section is present.
Extensions are skipped when ``disabled: true``. Configurator variables do
not create providers for signal sections omitted from the file.
* Sections omitted from the file leave the corresponding global provider
unset (a no-op provider), per the specification.
* Setting ``disabled: true`` at the top level turns the SDK into a no-op.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
from os import environ
from typing import Any, Literal, Protocol

from opentelemetry._logs import set_logger_provider
from opentelemetry._logs import get_logger_provider, set_logger_provider
from opentelemetry.environment_variables import (
OTEL_LOGS_EXPORTER,
OTEL_METRICS_EXPORTER,
OTEL_PYTHON_ID_GENERATOR,
OTEL_TRACES_EXPORTER,
)
from opentelemetry.metrics import set_meter_provider
from opentelemetry.metrics import get_meter_provider, set_meter_provider
from opentelemetry.sdk._logs import (
LoggerProvider,
LoggingHandler,
Expand Down Expand Up @@ -65,7 +65,7 @@
from opentelemetry.sdk.trace.id_generator import IdGenerator
from opentelemetry.sdk.trace.sampling import Sampler
from opentelemetry.semconv.resource import ResourceAttributes
from opentelemetry.trace import set_tracer_provider
from opentelemetry.trace import get_tracer_provider, set_tracer_provider
from opentelemetry.util._importlib_metadata import entry_points

_EXPORTER_OTLP = "otlp"
Expand Down Expand Up @@ -604,6 +604,46 @@ def _initialize_components(
_init_opamp(resource)


def _apply_python_extensions(config: Any) -> None:
"""Apply Python-specific environment extensions to configured providers."""
if config.disabled:
return

setup_logging_handler = (
os.getenv(_OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, "false").strip().lower() == "true"
)
if setup_logging_handler:
logger_provider = get_logger_provider()
handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider)
logging.getLogger().addHandler(handler)

logger_configurator_name = _get_logger_configurator()
meter_configurator_name = _get_meter_configurator()
tracer_configurator_name = _get_tracer_configurator()

logger_configurator = _import_logger_configurator(logger_configurator_name)
meter_configurator = _import_meter_configurator(meter_configurator_name)
tracer_configurator = _import_tracer_configurator(tracer_configurator_name)

if logger_configurator and config.logger_provider:
logger_provider = get_logger_provider()
if isinstance(logger_provider, LoggerProvider):
# pylint: disable-next=protected-access
logger_provider._set_logger_configurator(logger_configurator=logger_configurator)

if meter_configurator and config.meter_provider:
meter_provider = get_meter_provider()
if isinstance(meter_provider, MeterProvider):
# pylint: disable-next=protected-access
meter_provider._set_meter_configurator(meter_configurator=meter_configurator)

if tracer_configurator and config.tracer_provider:
tracer_provider = get_tracer_provider()
if isinstance(tracer_provider, TracerProvider):
# pylint: disable-next=protected-access
tracer_provider._set_tracer_configurator(tracer_configurator=tracer_configurator)


class _BaseConfigurator(ABC):
"""An ABC for configurators

Expand Down Expand Up @@ -654,10 +694,7 @@ def _configure(self, **kwargs):
# silence the static-analysis no-name-in-module on the
# conditional import.
# pylint: disable=import-outside-toplevel,no-name-in-module
from opentelemetry.configuration import ( # noqa: PLC0415
configure_sdk,
load_config_file,
)
from opentelemetry.configuration import configure_sdk, load_config_file # noqa: PLC0415
except ImportError as exc:
raise RuntimeError(
f"{OTEL_CONFIG_FILE} is set but "
Expand All @@ -671,6 +708,8 @@ def _configure(self, **kwargs):
OTEL_CONFIG_FILE,
sorted(kwargs),
)
configure_sdk(load_config_file(config_file))
config = load_config_file(config_file)
configure_sdk(config)
_apply_python_extensions(config)
return
_initialize_components(**kwargs)
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@
``${env:VAR}`` substitution inside the file.

Python-implementation extensions outside the spec (``OTEL_PYTHON_*``
variables such as ``OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED`` or
:envvar:`OTEL_PYTHON_TRACER_CONFIGURATOR`) are also bypassed when
:envvar:`OTEL_CONFIG_FILE` is set, because the env-var initialisation path
is skipped entirely in favour of the declarative file. Honouring these
alongside a config file is tracked separately.
variables such as ``OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED`` and
:envvar:`OTEL_PYTHON_TRACER_CONFIGURATOR`) are applied after the declarative
providers are configured. The logging variable adds the SDK logging handler;
tracer, meter, and logger configurators are applied to their corresponding
provider only when that provider is configured in the file. These extensions
are skipped when the configuration sets ``disabled: true``.

See the OpenTelemetry declarative configuration specification for details.
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import types
import unittest
from types import SimpleNamespace
from unittest.mock import MagicMock, patch

from opentelemetry.sdk._configuration import _OTelSDKConfigurator
Expand Down Expand Up @@ -48,7 +49,9 @@ def test_env_var_unset_runs_env_var_path(self, mock_init_components):
@patch("opentelemetry.sdk._configuration._initialize_components")
def test_env_var_set_routes_to_declarative_path(self, mock_init_components):
fake = _FakeConfigurationModule()
sentinel_config = object()
sentinel_config = SimpleNamespace(
disabled=False, logger_provider=None, meter_provider=None, tracer_provider=None
)
fake.load_config_file.return_value = sentinel_config

with patch.dict("sys.modules", {"opentelemetry.configuration": fake}):
Expand All @@ -58,6 +61,30 @@ def test_env_var_set_routes_to_declarative_path(self, mock_init_components):
fake.configure_sdk.assert_called_once_with(sentinel_config)
mock_init_components.assert_not_called()

@patch.dict("os.environ", {OTEL_CONFIG_FILE: "/tmp/otel.yaml"})
def test_python_extensions_run_after_declarative_configuration(self):
fake = _FakeConfigurationModule()
sentinel_config = SimpleNamespace(
disabled=False, logger_provider=None, meter_provider=None, tracer_provider=None
)
fake.load_config_file.return_value = sentinel_config
call_order = []
fake.configure_sdk.side_effect = lambda config: call_order.append(("configure_sdk", config))
configurator = _OTelSDKConfigurator()

with patch(
"opentelemetry.sdk._configuration._apply_python_extensions",
side_effect=lambda config: call_order.append(("python_extensions", config)),
) as apply_python_extensions:
with patch.dict("sys.modules", {"opentelemetry.configuration": fake}):
configurator._configure()

self.assertEqual(
call_order,
[("configure_sdk", sentinel_config), ("python_extensions", sentinel_config)],
)
apply_python_extensions.assert_called_once_with(sentinel_config)

@patch.dict("os.environ", {OTEL_CONFIG_FILE: "/tmp/otel.yaml"})
@patch.dict("sys.modules", {"opentelemetry.configuration": None}, clear=False)
@patch("opentelemetry.sdk._configuration._initialize_components")
Expand All @@ -73,7 +100,12 @@ def test_env_var_set_but_package_missing_raises(self, mock_init_components):
@patch.dict("os.environ", {OTEL_CONFIG_FILE: "/tmp/otel.yaml"})
def test_env_var_set_with_kwargs_warns_and_ignores(self):
fake = _FakeConfigurationModule()
fake.load_config_file.return_value = object()
fake.load_config_file.return_value = SimpleNamespace(
disabled=False,
logger_provider=None,
meter_provider=None,
tracer_provider=None,
)

with patch.dict("sys.modules", {"opentelemetry.configuration": fake}):
with self.assertLogs("opentelemetry.sdk._configuration", level="WARNING") as captured:
Expand Down
143 changes: 141 additions & 2 deletions opentelemetry-sdk/tests/test_configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from collections.abc import Iterable, Sequence
from logging import WARNING, getLogger
from os import environ
from types import SimpleNamespace
from unittest import TestCase, mock
from unittest.mock import Mock, patch

Expand All @@ -22,6 +23,7 @@
_EXPORTER_OTLP,
_EXPORTER_OTLP_PROTO_GRPC,
_EXPORTER_OTLP_PROTO_HTTP,
_apply_python_extensions,
_get_exporter_names,
_get_id_generator,
_get_logger_configurator,
Expand All @@ -41,7 +43,7 @@
_initialize_components,
_OTelSDKConfigurator,
)
from opentelemetry.sdk._logs import LoggingHandler, LogRecordProcessor
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler, LogRecordProcessor
from opentelemetry.sdk._logs._internal import _RuleBasedLoggerConfigurator
from opentelemetry.sdk._logs._internal.export import LogRecordExporter
from opentelemetry.sdk._logs.export import (
Expand All @@ -68,7 +70,7 @@
)
from opentelemetry.sdk.metrics.view import Aggregation
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import SpanProcessor, _RuleBasedTracerConfigurator
from opentelemetry.sdk.trace import SpanProcessor, TracerProvider, _RuleBasedTracerConfigurator
from opentelemetry.sdk.trace.export import (
ConsoleSpanExporter,
SimpleSpanProcessor,
Expand Down Expand Up @@ -1362,6 +1364,143 @@ def test_custom_configurator(self, mock_init_comp):
}
mock_init_comp.assert_called_once_with(**kwargs)

def test_python_configurators_are_applied_to_configured_global_providers(self):
config = SimpleNamespace(
disabled=False,
logger_provider=object(),
meter_provider=object(),
tracer_provider=object(),
)
logger_configurator = Mock()
meter_configurator = Mock()
tracer_configurator = Mock()
logger_provider = LoggerProvider(shutdown_on_exit=False)
meter_provider = MeterProvider(shutdown_on_exit=False)
tracer_provider = TracerProvider(shutdown_on_exit=False)

with (
patch.dict(
environ,
{
"OTEL_PYTHON_LOGGER_CONFIGURATOR": "logger_configurator",
"OTEL_PYTHON_METER_CONFIGURATOR": "meter_configurator",
"OTEL_PYTHON_TRACER_CONFIGURATOR": "tracer_configurator",
},
clear=True,
),
patch("opentelemetry.sdk._configuration.get_logger_provider", return_value=logger_provider),
patch("opentelemetry.sdk._configuration.get_meter_provider", return_value=meter_provider),
patch("opentelemetry.sdk._configuration.get_tracer_provider", return_value=tracer_provider),
patch(
"opentelemetry.sdk._configuration._import_logger_configurator",
return_value=logger_configurator,
),
patch(
"opentelemetry.sdk._configuration._import_meter_configurator",
return_value=meter_configurator,
),
patch(
"opentelemetry.sdk._configuration._import_tracer_configurator",
return_value=tracer_configurator,
),
patch.object(logger_provider, "_set_logger_configurator") as set_logger_configurator,
patch.object(meter_provider, "_set_meter_configurator") as set_meter_configurator,
patch.object(tracer_provider, "_set_tracer_configurator") as set_tracer_configurator,
patch("opentelemetry.sdk._configuration._initialize_components") as initialize_components,
):
_apply_python_extensions(config)

set_logger_configurator.assert_called_once_with(logger_configurator=logger_configurator)
set_meter_configurator.assert_called_once_with(meter_configurator=meter_configurator)
set_tracer_configurator.assert_called_once_with(tracer_configurator=tracer_configurator)
initialize_components.assert_not_called()

def test_python_configurators_are_not_applied_when_provider_sections_are_omitted(self):
config = SimpleNamespace(
disabled=False,
logger_provider=None,
meter_provider=None,
tracer_provider=None,
)

with (
patch.dict(
environ,
{
"OTEL_PYTHON_LOGGER_CONFIGURATOR": "logger_configurator",
"OTEL_PYTHON_METER_CONFIGURATOR": "meter_configurator",
"OTEL_PYTHON_TRACER_CONFIGURATOR": "tracer_configurator",
},
clear=True,
),
patch(
"opentelemetry.sdk._configuration._import_logger_configurator",
return_value=Mock(),
),
patch(
"opentelemetry.sdk._configuration._import_meter_configurator",
return_value=Mock(),
),
patch(
"opentelemetry.sdk._configuration._import_tracer_configurator",
return_value=Mock(),
),
patch("opentelemetry.sdk._configuration.get_logger_provider") as get_logger_provider,
patch("opentelemetry.sdk._configuration.get_meter_provider") as get_meter_provider,
patch("opentelemetry.sdk._configuration.get_tracer_provider") as get_tracer_provider,
):
_apply_python_extensions(config)

get_logger_provider.assert_not_called()
get_meter_provider.assert_not_called()
get_tracer_provider.assert_not_called()

def test_python_configurators_are_not_applied_when_config_is_disabled(self):
config = SimpleNamespace(
disabled=True,
logger_provider=object(),
meter_provider=object(),
tracer_provider=object(),
)
logger_provider = LoggerProvider(shutdown_on_exit=False)
meter_provider = MeterProvider(shutdown_on_exit=False)
tracer_provider = TracerProvider(shutdown_on_exit=False)

with (
patch.dict(
environ,
{
"OTEL_PYTHON_LOGGER_CONFIGURATOR": "logger_configurator",
"OTEL_PYTHON_METER_CONFIGURATOR": "meter_configurator",
"OTEL_PYTHON_TRACER_CONFIGURATOR": "tracer_configurator",
},
clear=True,
),
patch("opentelemetry.sdk._configuration.get_logger_provider", return_value=logger_provider),
patch("opentelemetry.sdk._configuration.get_meter_provider", return_value=meter_provider),
patch("opentelemetry.sdk._configuration.get_tracer_provider", return_value=tracer_provider),
patch(
"opentelemetry.sdk._configuration._import_logger_configurator",
return_value=Mock(),
),
patch(
"opentelemetry.sdk._configuration._import_meter_configurator",
return_value=Mock(),
),
patch(
"opentelemetry.sdk._configuration._import_tracer_configurator",
return_value=Mock(),
),
patch.object(logger_provider, "_set_logger_configurator") as set_logger_configurator,
patch.object(meter_provider, "_set_meter_configurator") as set_meter_configurator,
patch.object(tracer_provider, "_set_tracer_configurator") as set_tracer_configurator,
):
_apply_python_extensions(config)

set_logger_configurator.assert_not_called()
set_meter_configurator.assert_not_called()
set_tracer_configurator.assert_not_called()

def test_custom_configurator_with_init_args(self):
class ConfiguratorWithArgs(_OTelSDKConfigurator):
def __init__(self, name, strict=False):
Expand Down
Loading