diff --git a/ddtrace/llmobs/_llmobs.py b/ddtrace/llmobs/_llmobs.py index a3ac9501319..808cee89e0f 100644 --- a/ddtrace/llmobs/_llmobs.py +++ b/ddtrace/llmobs/_llmobs.py @@ -23,6 +23,7 @@ from ddtrace.internal.telemetry import telemetry_writer from ddtrace.internal.telemetry.constants import TELEMETRY_APM_PRODUCT from ddtrace.internal.utils.formats import asbool +from ddtrace.internal.utils.formats import parse_tags_str from ddtrace.llmobs._constants import ANNOTATIONS_CONTEXT_ID from ddtrace.llmobs._constants import INPUT_DOCUMENTS from ddtrace.llmobs._constants import INPUT_MESSAGES @@ -347,8 +348,20 @@ def flush(cls) -> None: @staticmethod def _patch_integrations() -> None: - """Patch LLM integrations.""" - patch(**{integration: True for integration in SUPPORTED_LLMOBS_INTEGRATIONS.values()}) # type: ignore[arg-type] + """ + Patch LLM integrations. Ensure that we do not ignore DD_TRACE__ENABLED or DD_PATCH_MODULES settings. + """ + integrations_to_patch = {integration: True for integration in SUPPORTED_LLMOBS_INTEGRATIONS.values()} + for module, _ in integrations_to_patch.items(): + env_var = "DD_TRACE_%s_ENABLED" % module.upper() + if env_var in os.environ: + integrations_to_patch[module] = asbool(os.environ[env_var]) + dd_patch_modules = os.getenv("DD_PATCH_MODULES") + dd_patch_modules_to_str = parse_tags_str(dd_patch_modules) + integrations_to_patch.update( + {k: asbool(v) for k, v in dd_patch_modules_to_str.items() if k in SUPPORTED_LLMOBS_INTEGRATIONS.values()} + ) + patch(**integrations_to_patch) # type: ignore[arg-type] log.debug("Patched LLM integrations: %s", list(SUPPORTED_LLMOBS_INTEGRATIONS.values())) @classmethod diff --git a/releasenotes/notes/fix-llmobs-do-not-ignore-global-patch-configs-a2adc4803f55b142.yaml b/releasenotes/notes/fix-llmobs-do-not-ignore-global-patch-configs-a2adc4803f55b142.yaml new file mode 100644 index 00000000000..b080742d74a --- /dev/null +++ b/releasenotes/notes/fix-llmobs-do-not-ignore-global-patch-configs-a2adc4803f55b142.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + LLM Observability: This fix resolves an issue where ``LLMObs.enable()`` ignored global patch configurations, specifically + the ``DD_TRACE__ENABLED`` and ``DD_PATCH_MODULES`` environment variables. diff --git a/tests/llmobs/test_llmobs_service.py b/tests/llmobs/test_llmobs_service.py index 160023f5df7..5808ed01513 100644 --- a/tests/llmobs/test_llmobs_service.py +++ b/tests/llmobs/test_llmobs_service.py @@ -31,6 +31,7 @@ from ddtrace.llmobs._constants import SPAN_KIND from ddtrace.llmobs._constants import SPAN_START_WHILE_DISABLED_WARNING from ddtrace.llmobs._constants import TAGS +from ddtrace.llmobs._llmobs import SUPPORTED_LLMOBS_INTEGRATIONS from ddtrace.llmobs._llmobs import LLMObsTraceProcessor from ddtrace.llmobs.utils import Prompt from tests.llmobs._utils import _expected_llmobs_eval_metric_event @@ -144,6 +145,65 @@ def test_service_enable_already_enabled(mock_logs): mock_logs.debug.assert_has_calls([mock.call("%s already enabled", "LLMObs")]) +@mock.patch("ddtrace.llmobs._llmobs.patch") +def test_service_enable_patches_llmobs_integrations(mock_tracer_patch): + with override_global_config(dict(_dd_api_key="", _llmobs_ml_app="")): + llmobs_service.enable() + mock_tracer_patch.assert_called_once() + kwargs = mock_tracer_patch.call_args[1] + for module in SUPPORTED_LLMOBS_INTEGRATIONS.values(): + assert kwargs[module] is True + llmobs_service.disable() + + +@mock.patch("ddtrace.llmobs._llmobs.patch") +def test_service_enable_does_not_override_global_patch_modules(mock_tracer_patch, monkeypatch): + monkeypatch.setenv("DD_PATCH_MODULES", "openai:false") + with override_global_config(dict(_dd_api_key="", _llmobs_ml_app="")): + llmobs_service.enable() + mock_tracer_patch.assert_called_once() + kwargs = mock_tracer_patch.call_args[1] + for module in SUPPORTED_LLMOBS_INTEGRATIONS.values(): + if module == "openai": + assert kwargs[module] is False + continue + assert kwargs[module] is True + llmobs_service.disable() + + +@mock.patch("ddtrace.llmobs._llmobs.patch") +def test_service_enable_does_not_override_integration_enabled_env_vars(mock_tracer_patch, monkeypatch): + monkeypatch.setenv("DD_TRACE_OPENAI_ENABLED", "false") + with override_global_config(dict(_dd_api_key="", _llmobs_ml_app="")): + llmobs_service.enable() + mock_tracer_patch.assert_called_once() + kwargs = mock_tracer_patch.call_args[1] + for module in SUPPORTED_LLMOBS_INTEGRATIONS.values(): + if module == "openai": + assert kwargs[module] is False + continue + assert kwargs[module] is True + llmobs_service.disable() + + +@mock.patch("ddtrace.llmobs._llmobs.patch") +def test_service_enable_does_not_override_global_patch_config(mock_tracer_patch, monkeypatch): + """Test that _patch_integrations() ensures `DD_PATCH_MODULES` overrides `DD_TRACE__ENABLED`.""" + monkeypatch.setenv("DD_TRACE_OPENAI_ENABLED", "true") + monkeypatch.setenv("DD_TRACE_ANTHROPIC_ENABLED", "false") + monkeypatch.setenv("DD_PATCH_MODULES", "openai:false") + with override_global_config(dict(_dd_api_key="", _llmobs_ml_app="")): + llmobs_service.enable() + mock_tracer_patch.assert_called_once() + kwargs = mock_tracer_patch.call_args[1] + for module in SUPPORTED_LLMOBS_INTEGRATIONS.values(): + if module in ("openai", "anthropic"): + assert kwargs[module] is False + continue + assert kwargs[module] is True + llmobs_service.disable() + + def test_start_span_while_disabled_logs_warning(LLMObs, mock_logs): LLMObs.disable() _ = LLMObs.llm(model_name="test_model", name="test_llm_call", model_provider="test_provider")