diff --git a/leap0/_async/client.py b/leap0/_async/client.py index 6070fc5..9e90ddd 100644 --- a/leap0/_async/client.py +++ b/leap0/_async/client.py @@ -108,7 +108,7 @@ def __init__( timeout: float = DEFAULT_CLIENT_TIMEOUT, auth_header: str = "authorization", bearer: bool = True, - otel_enabled: bool | None = None, + sdk_otel_enabled: bool | None = None, ): if config is not None: provided_overrides = { @@ -117,7 +117,7 @@ def __init__( "sandbox_domain": sandbox_domain, "auth_header": auth_header if auth_header != "authorization" else None, "bearer": bearer if bearer is not True else None, - "otel_enabled": otel_enabled, + "sdk_otel_enabled": sdk_otel_enabled, } if timeout != DEFAULT_CLIENT_TIMEOUT: provided_overrides["timeout"] = timeout @@ -133,7 +133,7 @@ def __init__( timeout=timeout, auth_header=auth_header, bearer=bearer, - otel_enabled=otel_enabled, + sdk_otel_enabled=sdk_otel_enabled, ) self._transport = AsyncTransport( api_key=config.api_key, @@ -164,7 +164,7 @@ def __init__( self.code_interpreter = AsyncCodeInterpreterClient(self._transport, sandbox_domain=config.sandbox_domain) self.desktop = AsyncDesktopClient(self._transport, sandbox_domain=config.sandbox_domain) - if config.otel_enabled: + if config.sdk_otel_enabled: self._init_otel() def _init_otel(self) -> None: diff --git a/leap0/_async/sandbox.py b/leap0/_async/sandbox.py index 909c1d9..afbffa7 100644 --- a/leap0/_async/sandbox.py +++ b/leap0/_async/sandbox.py @@ -5,8 +5,14 @@ from functools import wraps from typing import TYPE_CHECKING, Generic, Protocol, TypeVar, cast +from ..constants import OTEL_EXPORTER_OTLP_ENDPOINT_ENV, OTEL_EXPORTER_OTLP_HEADERS_ENV from .._internal.types import SandboxFactory -from ..models.config import DEFAULT_MEMORY_MIB, DEFAULT_TEMPLATE_NAME, DEFAULT_TIMEOUT_MIN, DEFAULT_VCPU +from ..models.config import ( + DEFAULT_MEMORY_MIB, + DEFAULT_TEMPLATE_NAME, + DEFAULT_TIMEOUT_MIN, + DEFAULT_VCPU, +) from ..models.sandbox import CreateSandboxParams, Sandbox as SandboxData, SandboxRef, SandboxStatus, sandbox_id_of from .._schemas.sandbox import NetworkPolicyDict, SandboxCreateResponseDict, SandboxStatusResponseDict from .._utils.errors import intercept_errors @@ -20,8 +26,8 @@ _OTEL_ENV_KEYS = ( - "OTEL_EXPORTER_OTLP_ENDPOINT", - "OTEL_EXPORTER_OTLP_HEADERS", + OTEL_EXPORTER_OTLP_ENDPOINT_ENV, + OTEL_EXPORTER_OTLP_HEADERS_ENV, ) @@ -140,11 +146,18 @@ def websocket_url(self, path: str = "/", *, port: int | None = None) -> str: def _inject_otel_env(env_vars: dict[str, str] | None) -> dict[str, str] | None: - otel = {k: v for k in _OTEL_ENV_KEYS if (v := os.environ.get(k))} - if not otel and not env_vars: - return None - if not otel: - return env_vars + endpoint = os.environ.get(OTEL_EXPORTER_OTLP_ENDPOINT_ENV) + if not endpoint: + raise ValueError( + f"otel_export=True requires {OTEL_EXPORTER_OTLP_ENDPOINT_ENV} in the local environment" + ) + otel = {OTEL_EXPORTER_OTLP_ENDPOINT_ENV: endpoint} + for key in _OTEL_ENV_KEYS: + if key == OTEL_EXPORTER_OTLP_ENDPOINT_ENV: + continue + value = os.environ.get(key) + if value: + otel[key] = value merged = dict(otel) if env_vars: merged.update(env_vars) @@ -182,7 +195,7 @@ async def create( memory_mib: int = DEFAULT_MEMORY_MIB, timeout_min: int = DEFAULT_TIMEOUT_MIN, auto_pause: bool = False, - telemetry: bool = False, + otel_export: bool = False, env_vars: dict[str, str] | None = None, network_policy: NetworkPolicyDict | None = None, http_timeout: float | None = None, @@ -195,7 +208,7 @@ async def create( memory_mib: Memory in MiB (512 to 8192, must be even). timeout_min: Sandbox timeout in minutes (1 to 480). auto_pause: Whether the sandbox should auto-pause on timeout. - telemetry: Whether OpenTelemetry variables should be injected. + otel_export: Whether OpenTelemetry exporter variables should be injected. env_vars: Environment variables to set inside the sandbox. network_policy: Outbound network policy for the sandbox. http_timeout: Optional HTTP request timeout in seconds for this SDK call. @@ -209,12 +222,12 @@ async def create( memory_mib=memory_mib, timeout_min=timeout_min, auto_pause=auto_pause, - telemetry=telemetry, - env_vars=_inject_otel_env(env_vars) if telemetry else env_vars, + otel_export=otel_export, + env_vars=_inject_otel_env(env_vars) if otel_export else env_vars, network_policy=network_policy, ) payload = params.to_payload() - payload.pop("telemetry", None) + payload.pop("otel_export", None) data: SandboxCreateResponseDict = await self._transport.request_json( "POST", "/v1/sandbox", json=payload, expected_status=201, timeout=http_timeout, diff --git a/leap0/_sync/client.py b/leap0/_sync/client.py index c8bbb4b..34871ac 100644 --- a/leap0/_sync/client.py +++ b/leap0/_sync/client.py @@ -2,6 +2,7 @@ from types import TracebackType from typing import Self +import warnings from opentelemetry import metrics, trace from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter @@ -90,7 +91,16 @@ def __init__( auth_header: str = "authorization", bearer: bool = True, otel_enabled: bool | None = None, + sdk_otel_enabled: bool | None = None, ): + if otel_enabled is not None: + warnings.warn( + "otel_enabled is deprecated; use sdk_otel_enabled instead", + DeprecationWarning, + stacklevel=2, + ) + if sdk_otel_enabled is None: + sdk_otel_enabled = otel_enabled config = Leap0Config( api_key=api_key, base_url=base_url, @@ -98,7 +108,7 @@ def __init__( timeout=timeout, auth_header=auth_header, bearer=bearer, - otel_enabled=otel_enabled, + sdk_otel_enabled=sdk_otel_enabled, ) self._transport = Transport( api_key=config.api_key, @@ -128,7 +138,7 @@ def __init__( self.code_interpreter = CodeInterpreterClient(self._transport, sandbox_domain=config.sandbox_domain) self.desktop = DesktopClient(self._transport, sandbox_domain=config.sandbox_domain) - if config.otel_enabled: + if config.sdk_otel_enabled: self._init_otel() def _init_otel(self) -> None: @@ -223,5 +233,5 @@ def Leap0(config: Leap0Config) -> Leap0Client: timeout=config.timeout, auth_header=config.auth_header, bearer=config.bearer, - otel_enabled=config.otel_enabled, + sdk_otel_enabled=config.sdk_otel_enabled, ) diff --git a/leap0/_sync/sandbox.py b/leap0/_sync/sandbox.py index 8453bd8..b3d8a6c 100644 --- a/leap0/_sync/sandbox.py +++ b/leap0/_sync/sandbox.py @@ -4,8 +4,14 @@ from functools import wraps from typing import Generic, Protocol, TypeVar, cast +from ..constants import OTEL_EXPORTER_OTLP_ENDPOINT_ENV, OTEL_EXPORTER_OTLP_HEADERS_ENV from .._internal.types import SandboxFactory -from ..models.config import DEFAULT_MEMORY_MIB, DEFAULT_TEMPLATE_NAME, DEFAULT_TIMEOUT_MIN, DEFAULT_VCPU +from ..models.config import ( + DEFAULT_MEMORY_MIB, + DEFAULT_TEMPLATE_NAME, + DEFAULT_TIMEOUT_MIN, + DEFAULT_VCPU, +) from ..models.sandbox import CreateSandboxParams, Sandbox as SandboxData, SandboxRef, SandboxStatus, sandbox_id_of from .._schemas.sandbox import NetworkPolicyDict, SandboxCreateResponseDict, SandboxStatusResponseDict from .._utils.errors import intercept_errors @@ -14,8 +20,8 @@ _OTEL_ENV_KEYS = ( - "OTEL_EXPORTER_OTLP_ENDPOINT", - "OTEL_EXPORTER_OTLP_HEADERS", + OTEL_EXPORTER_OTLP_ENDPOINT_ENV, + OTEL_EXPORTER_OTLP_HEADERS_ENV, ) SandboxT = TypeVar("SandboxT", SandboxData, SandboxStatus, "Sandbox") @@ -137,11 +143,18 @@ def websocket_url(self, path: str = "/", *, port: int | None = None) -> str: def _inject_otel_env(env_vars: dict[str, str] | None) -> dict[str, str] | None: - otel = {k: v for k in _OTEL_ENV_KEYS if (v := os.environ.get(k))} - if not otel and not env_vars: - return None - if not otel: - return env_vars + endpoint = os.environ.get(OTEL_EXPORTER_OTLP_ENDPOINT_ENV) + if not endpoint: + raise ValueError( + f"otel_export=True requires {OTEL_EXPORTER_OTLP_ENDPOINT_ENV} in the local environment" + ) + otel = {OTEL_EXPORTER_OTLP_ENDPOINT_ENV: endpoint} + for key in _OTEL_ENV_KEYS: + if key == OTEL_EXPORTER_OTLP_ENDPOINT_ENV: + continue + value = os.environ.get(key) + if value: + otel[key] = value merged = dict(otel) if env_vars: merged.update(env_vars) @@ -183,7 +196,8 @@ def create( memory_mib: int = DEFAULT_MEMORY_MIB, timeout_min: int = DEFAULT_TIMEOUT_MIN, auto_pause: bool = False, - telemetry: bool = False, + otel_export: bool | None = None, + telemetry: bool | None = None, env_vars: dict[str, str] | None = None, network_policy: NetworkPolicyDict | None = None, http_timeout: float | None = None, @@ -196,9 +210,10 @@ def create( memory_mib: Memory in MiB (512 to 8192, must be even). timeout_min: Sandbox timeout in minutes (1 to 480, default 5). auto_pause: Automatically pause the sandbox into a snapshot on timeout. - telemetry: Enable OpenTelemetry export. Reads ``OTEL_EXPORTER_OTLP_ENDPOINT`` - and ``OTEL_EXPORTER_OTLP_HEADERS`` from the local environment and - injects them into the sandbox. + otel_export: Inject OpenTelemetry exporter environment into the sandbox. + Requires ``OTEL_EXPORTER_OTLP_ENDPOINT`` in the local environment and + also forwards ``OTEL_EXPORTER_OTLP_HEADERS`` when present. + telemetry: Deprecated alias for ``otel_export``. Use ``otel_export`` instead. env_vars: Environment variables to set inside the sandbox. network_policy: Outbound network policy for the sandbox. http_timeout: Optional HTTP request timeout in seconds for this SDK call. @@ -206,18 +221,20 @@ def create( Returns: SandboxT | SandboxData | SandboxStatus: Created sandbox object. """ + effective_otel_export = otel_export if otel_export is not None else bool(telemetry) + params = CreateSandboxParams( template_name=template_name, vcpu=vcpu, memory_mib=memory_mib, timeout_min=timeout_min, auto_pause=auto_pause, - telemetry=telemetry, - env_vars=_inject_otel_env(env_vars) if telemetry else env_vars, + otel_export=effective_otel_export, + env_vars=_inject_otel_env(env_vars) if effective_otel_export else env_vars, network_policy=network_policy, ) payload = params.to_payload() - payload.pop("telemetry", None) + payload.pop("otel_export", None) data: SandboxCreateResponseDict = self._transport.request_json( "POST", "/v1/sandbox", json=payload, expected_status=201, timeout=http_timeout, diff --git a/leap0/constants.py b/leap0/constants.py new file mode 100644 index 0000000..2f47953 --- /dev/null +++ b/leap0/constants.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +LEAP0_API_KEY_ENV = "LEAP0_API_KEY" + +LEAP0_BASE_URL_ENV = "LEAP0_BASE_URL" + +LEAP0_SANDBOX_DOMAIN_ENV = "LEAP0_SANDBOX_DOMAIN" + +LEAP0_SDK_OTEL_ENABLED_ENV = "LEAP0_SDK_OTEL_ENABLED" + +OTEL_EXPORTER_OTLP_ENDPOINT_ENV = "OTEL_EXPORTER_OTLP_ENDPOINT" + +OTEL_EXPORTER_OTLP_HEADERS_ENV = "OTEL_EXPORTER_OTLP_HEADERS" diff --git a/leap0/models/config.py b/leap0/models/config.py index ac2f472..fe959a6 100644 --- a/leap0/models/config.py +++ b/leap0/models/config.py @@ -4,6 +4,14 @@ import os from pydantic import BaseModel, ConfigDict, model_validator +from ..constants import ( + LEAP0_API_KEY_ENV, + LEAP0_BASE_URL_ENV, + LEAP0_SANDBOX_DOMAIN_ENV, + LEAP0_SDK_OTEL_ENABLED_ENV, + OTEL_EXPORTER_OTLP_ENDPOINT_ENV, +) + DEFAULT_BASE_URL = "https://api.leap0.dev" DEFAULT_SANDBOX_DOMAIN = "sandbox.leap0.dev" @@ -40,7 +48,7 @@ class Leap0Config(BaseModel): timeout: float = DEFAULT_CLIENT_TIMEOUT auth_header: str = "authorization" bearer: bool = True - otel_enabled: bool | None = None + sdk_otel_enabled: bool | None = None @model_validator(mode="after") def _resolve_and_validate(self) -> Leap0Config: @@ -53,10 +61,10 @@ def _resolve_and_validate(self) -> Leap0Config: api_key = self.api_key if api_key is None: - api_key = os.environ.get("LEAP0_API_KEY") + api_key = os.environ.get(LEAP0_API_KEY_ENV) api_key = api_key.strip() if api_key else api_key if not api_key: - raise ValueError("api_key is required or set LEAP0_API_KEY") + raise ValueError(f"api_key is required or set {LEAP0_API_KEY_ENV}") auth_header = self.auth_header.strip() if not auth_header: @@ -65,14 +73,17 @@ def _resolve_and_validate(self) -> Leap0Config: self.timeout = timeout self.api_key = api_key self.auth_header = auth_header - if self.otel_enabled is None: - self.otel_enabled = os.environ.get("LEAP0_OTEL_ENABLED") == "true" or bool( - os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") - ) - self.base_url = _resolve_env_str(self.base_url, "LEAP0_BASE_URL", DEFAULT_BASE_URL) + if self.sdk_otel_enabled is None: + sdk_otel_env = os.environ.get(LEAP0_SDK_OTEL_ENABLED_ENV) + sdk_otel_env = sdk_otel_env.strip() if sdk_otel_env is not None else None + if sdk_otel_env: + self.sdk_otel_enabled = sdk_otel_env.lower() == "true" + else: + self.sdk_otel_enabled = bool(os.environ.get(OTEL_EXPORTER_OTLP_ENDPOINT_ENV)) + self.base_url = _resolve_env_str(self.base_url, LEAP0_BASE_URL_ENV, DEFAULT_BASE_URL) self.sandbox_domain = _resolve_env_str( self.sandbox_domain, - "LEAP0_SANDBOX_DOMAIN", + LEAP0_SANDBOX_DOMAIN_ENV, DEFAULT_SANDBOX_DOMAIN, ) return self diff --git a/leap0/models/sandbox.py b/leap0/models/sandbox.py index f4c8bda..f6e8804 100644 --- a/leap0/models/sandbox.py +++ b/leap0/models/sandbox.py @@ -35,7 +35,7 @@ class CreateSandboxParams(BaseModel): memory_mib: int = DEFAULT_MEMORY_MIB timeout_min: int = DEFAULT_TIMEOUT_MIN auto_pause: bool = False - telemetry: bool = False + otel_export: bool = False env_vars: dict[str, str] | None = None network_policy: NetworkPolicyDict | None = None diff --git a/tests/_async/test_sandboxes.py b/tests/_async/test_sandboxes.py index 831babe..9e5c9a5 100644 --- a/tests/_async/test_sandboxes.py +++ b/tests/_async/test_sandboxes.py @@ -1,9 +1,11 @@ from __future__ import annotations import asyncio +import pytest from types import SimpleNamespace from leap0._async.sandbox import AsyncSandbox, AsyncSandboxesClient +from leap0.models.errors import Leap0Error from leap0.models.sandbox import Sandbox @@ -37,3 +39,36 @@ async def run() -> None: assert isinstance(result, AsyncSandbox) asyncio.run(run()) + + def test_create_injects_otel_env_when_enabled(self, async_mock_transport, monkeypatch): + async def run() -> None: + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4318") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_HEADERS", "authorization=token") + async_mock_transport.request_json.return_value = { + "id": "sbx-1", "template_id": "tpl-1", "vcpu": 2, "memory_mib": 2048, + "disk_mib": 10240, "state": "starting", "auto_pause": False, "created_at": "", + } + + await AsyncSandboxesClient(async_mock_transport, sandbox_domain="s.dev").create( + otel_export=True, + env_vars={"APP_ENV": "test"}, + ) + + sent_env = async_mock_transport.request_json.call_args.kwargs["json"]["env_vars"] + assert sent_env == { + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://collector:4318", + "OTEL_EXPORTER_OTLP_HEADERS": "authorization=token", + "APP_ENV": "test", + } + + asyncio.run(run()) + + def test_create_rejects_otel_export_without_endpoint(self, async_mock_transport, monkeypatch): + async def run() -> None: + monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False) + monkeypatch.delenv("OTEL_EXPORTER_OTLP_HEADERS", raising=False) + + with pytest.raises(Leap0Error, match="OTEL_EXPORTER_OTLP_ENDPOINT"): + await AsyncSandboxesClient(async_mock_transport, sandbox_domain="s.dev").create(otel_export=True) + + asyncio.run(run()) diff --git a/tests/_sync/test_client_config.py b/tests/_sync/test_client_config.py index e892201..01dab00 100644 --- a/tests/_sync/test_client_config.py +++ b/tests/_sync/test_client_config.py @@ -3,19 +3,39 @@ from leap0.models.config import Leap0Config -def test_otel_enabled_defaults_from_standard_otel_env(monkeypatch): +def test_sdk_otel_enabled_defaults_from_standard_otel_env(monkeypatch): monkeypatch.setenv("LEAP0_API_KEY", "test-key") monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318") config = Leap0Config() - assert config.otel_enabled is True + assert config.sdk_otel_enabled is True -def test_otel_enabled_can_be_disabled_explicitly(monkeypatch): +def test_sdk_otel_enabled_can_be_disabled_explicitly(monkeypatch): monkeypatch.setenv("LEAP0_API_KEY", "test-key") monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318") - config = Leap0Config(otel_enabled=False) + config = Leap0Config(sdk_otel_enabled=False) - assert config.otel_enabled is False + assert config.sdk_otel_enabled is False + + +def test_explicit_sdk_flag_precedence(monkeypatch): + monkeypatch.setenv("LEAP0_API_KEY", "test-key") + monkeypatch.setenv("LEAP0_SDK_OTEL_ENABLED", "false") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318") + + config = Leap0Config() + + assert config.sdk_otel_enabled is False + + +def test_legacy_otel_env_no_longer_enables_sdk(monkeypatch): + monkeypatch.setenv("LEAP0_API_KEY", "test-key") + monkeypatch.setenv("LEAP0_OTEL_ENABLED", "true") + monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False) + + config = Leap0Config() + + assert config.sdk_otel_enabled is False diff --git a/tests/_sync/test_sandboxes.py b/tests/_sync/test_sandboxes.py index 988804c..1030893 100644 --- a/tests/_sync/test_sandboxes.py +++ b/tests/_sync/test_sandboxes.py @@ -1,5 +1,4 @@ from __future__ import annotations - from unittest.mock import MagicMock from types import SimpleNamespace @@ -77,6 +76,60 @@ def test_create_validates_input(self, mock_transport): with pytest.raises(Leap0Error, match="memory_mib"): SandboxesClient(mock_transport, sandbox_domain="s.dev").create(memory_mib=513) + def test_create_injects_otel_env_when_enabled(self, mock_transport, monkeypatch): + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4318") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_HEADERS", "authorization=token") + mock_transport.request_json.return_value = { + "id": "sbx-1", "template_id": "tpl-1", "vcpu": 2, "memory_mib": 2048, + "disk_mib": 10240, "state": "starting", "auto_pause": False, "created_at": "", + } + + SandboxesClient(mock_transport, sandbox_domain="s.dev").create( + otel_export=True, + env_vars={"APP_ENV": "test"}, + ) + + sent_env = mock_transport.request_json.call_args.kwargs["json"]["env_vars"] + assert sent_env == { + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://collector:4318", + "OTEL_EXPORTER_OTLP_HEADERS": "authorization=token", + "APP_ENV": "test", + } + + def test_create_accepts_legacy_telemetry_flag(self, mock_transport, monkeypatch): + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4318") + mock_transport.request_json.return_value = { + "id": "sbx-1", "template_id": "tpl-1", "vcpu": 2, "memory_mib": 2048, + "disk_mib": 10240, "state": "starting", "auto_pause": False, "created_at": "", + } + + SandboxesClient(mock_transport, sandbox_domain="s.dev").create(telemetry=True) + + sent_env = mock_transport.request_json.call_args.kwargs["json"]["env_vars"] + assert sent_env == {"OTEL_EXPORTER_OTLP_ENDPOINT": "http://collector:4318"} + + def test_create_prefers_otel_export_over_legacy_telemetry(self, mock_transport, monkeypatch): + mock_transport.request_json.return_value = { + "id": "sbx-1", "template_id": "tpl-1", "vcpu": 2, "memory_mib": 2048, + "disk_mib": 10240, "state": "starting", "auto_pause": False, "created_at": "", + } + + SandboxesClient(mock_transport, sandbox_domain="s.dev").create( + otel_export=False, + telemetry=True, + env_vars={"APP_ENV": "test"}, + ) + + sent_env = mock_transport.request_json.call_args.kwargs["json"]["env_vars"] + assert sent_env == {"APP_ENV": "test"} + + def test_create_rejects_otel_export_without_endpoint(self, mock_transport, monkeypatch): + monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False) + monkeypatch.delenv("OTEL_EXPORTER_OTLP_HEADERS", raising=False) + + with pytest.raises(Leap0Error, match="OTEL_EXPORTER_OTLP_ENDPOINT"): + SandboxesClient(mock_transport, sandbox_domain="s.dev").create(otel_export=True) + class TestCreateSandboxParams: def test_default_template_is_bookworm(self): diff --git a/tests/models/test_config.py b/tests/models/test_config.py index 15b2b70..e388ab4 100644 --- a/tests/models/test_config.py +++ b/tests/models/test_config.py @@ -58,3 +58,23 @@ def test_creates_with_key(self): def test_context_manager(self): with Leap0Client(api_key="test-key") as client: assert client.sandboxes is not None + + def test_otel_enabled_deprecation_shim(self): + with pytest.warns(DeprecationWarning, match="sdk_otel_enabled"): + client = Leap0Client(api_key="test-key", otel_enabled=False) + + client.close() + + def test_sdk_otel_enabled_wins_over_otel_enabled(self, monkeypatch): + calls: list[bool] = [] + + def fake_init_otel(self): + calls.append(True) + + monkeypatch.setattr(Leap0Client, "_init_otel", fake_init_otel) + + with pytest.warns(DeprecationWarning, match="sdk_otel_enabled"): + client = Leap0Client(api_key="test-key", otel_enabled=True, sdk_otel_enabled=False) + + client.close() + assert calls == []