diff --git a/ddtrace/bootstrap/sitecustomize.py b/ddtrace/bootstrap/sitecustomize.py index 6da3dab36a0..545bffb676e 100644 --- a/ddtrace/bootstrap/sitecustomize.py +++ b/ddtrace/bootstrap/sitecustomize.py @@ -13,21 +13,21 @@ # to allow injecting a custom sitecustomize.py file into the Python process to # perform the correct initialisation for the library. All the actual # initialisation logic should be placed in preload.py. -from ddtrace import LOADED_MODULES # isort:skip - +import ddtrace # isort:skip import logging # noqa:I001 import os # noqa:F401 import sys import warnings # noqa:F401 -from ddtrace.internal.telemetry import telemetry_writer from ddtrace import config # noqa:F401 from ddtrace._logger import DD_LOG_FORMAT from ddtrace.internal.logger import get_logger # noqa:F401 from ddtrace.internal.module import ModuleWatchdog # noqa:F401 from ddtrace.internal.module import is_module_installed +from ddtrace.internal.telemetry import telemetry_writer from ddtrace.internal.utils.formats import asbool # noqa:F401 + # Debug mode from the tracer will do the same here, so only need to do this otherwise. if config._logs_injection: from ddtrace import patch @@ -88,7 +88,7 @@ def drop(module_name): "wrapt", ] ) - for m in list(_ for _ in sys.modules if _ not in LOADED_MODULES): + for m in list(_ for _ in sys.modules if _ not in ddtrace.LOADED_MODULES): if any(m == _ or m.startswith(_ + ".") for _ in KEEP_MODULES): continue @@ -164,9 +164,12 @@ def _(threading): log.debug("additional sitecustomize not found") else: log.debug("additional sitecustomize found in: %s", sys.path) - - telemetry_writer.add_configuration("ddtrace_bootstrapped", True, "unknown") - telemetry_writer.add_configuration("ddtrace_auto_used", "ddtrace.auto" in sys.modules, "unknown") + # Detect if ddtrace-run is being used by checking if the bootstrap directory is in the python path + bootstrap_dir = os.path.join(os.path.dirname(ddtrace.__file__), "bootstrap") + if bootstrap_dir in sys.path: + telemetry_writer.add_configuration("instrumentation_source", "cmd_line", "code") + else: + telemetry_writer.add_configuration("instrumentation_source", "manual", "code") # Loading status used in tests to detect if the `sitecustomize` has been # properly loaded without exceptions. This must be the last action in the module # when the execution ends with a success. diff --git a/ddtrace/settings/_config.py b/ddtrace/settings/_config.py index 59607956dbb..f772f25e3ac 100644 --- a/ddtrace/settings/_config.py +++ b/ddtrace/settings/_config.py @@ -14,6 +14,7 @@ from ddtrace.internal.serverless import in_azure_function from ddtrace.internal.serverless import in_gcp_function +from ddtrace.internal.telemetry import telemetry_writer from ddtrace.internal.telemetry import validate_otel_envs from ddtrace.internal.utils.cache import cachedmethod @@ -647,9 +648,9 @@ def __init__(self): self._llmobs_ml_app = _get_config("DD_LLMOBS_ML_APP") self._llmobs_agentless_enabled = _get_config("DD_LLMOBS_AGENTLESS_ENABLED", None, asbool) - self._inject_force = _get_config("DD_INJECT_FORCE", False, asbool) + self._inject_force = _get_config("DD_INJECT_FORCE", None, asbool) self._lib_was_injected = False - self._inject_was_attempted = _get_config("_DD_INJECT_WAS_ATTEMPTED", False, asbool) + self._inject_enabled = _get_config("DD_INJECTION_ENABLED") self._inferred_proxy_services_enabled = _get_config("DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED", False, asbool) def __getattr__(self, name) -> Any: @@ -778,10 +779,7 @@ def _set_config_items(self, items): item_names.append(key) item = self._config[key] item.set_value_source(value, origin) - if self._telemetry_enabled: - from ddtrace.internal.telemetry import telemetry_writer - - telemetry_writer.add_configuration(item._name, item.value(), item.source()) + telemetry_writer.add_configuration(item._name, item.value(), item.source()) self._notify_subscribers(item_names) def _reset(self): diff --git a/lib-injection/sources/sitecustomize.py b/lib-injection/sources/sitecustomize.py index a5076da6cb7..2fdadb607dd 100644 --- a/lib-injection/sources/sitecustomize.py +++ b/lib-injection/sources/sitecustomize.py @@ -262,7 +262,6 @@ def _inject(): EXECUTABLES_DENY_LIST = build_denied_executables() integration_incomp = False runtime_incomp = False - os.environ["_DD_INJECT_WAS_ATTEMPTED"] = "true" spec = None try: # `find_spec` is only available in Python 3.4+ @@ -430,6 +429,9 @@ def _inject(): ], ), ) + # Track whether library injection was successful + ddtrace.config._lib_was_injected = True + ddtrace.internal.telemetry.telemetry_writer.add_configuration("instrumentation_source", "ssi", "code") except Exception as e: TELEMETRY_DATA.append( create_count_metric( diff --git a/tests/telemetry/test_writer.py b/tests/telemetry/test_writer.py index fc44cdfa285..7807c79481e 100644 --- a/tests/telemetry/test_writer.py +++ b/tests/telemetry/test_writer.py @@ -18,6 +18,7 @@ from ddtrace.internal.utils.version import _pep440_to_semver from ddtrace.settings._config import DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP_DEFAULT from tests.conftest import DEFAULT_DDTRACE_SUBPROCESS_TEST_SERVICE_NAME +from tests.utils import call_program from tests.utils import override_global_config @@ -159,8 +160,7 @@ def test_app_started_event(telemetry_writer, test_agent_session, mock_time): {"name": "DD_TRACE_WRITER_INTERVAL_SECONDS", "origin": "unknown", "value": 1.0}, {"name": "DD_TRACE_WRITER_MAX_PAYLOAD_SIZE_BYTES", "origin": "unknown", "value": 20 << 20}, {"name": "DD_TRACE_WRITER_REUSE_CONNECTIONS", "origin": "unknown", "value": False}, - {"name": "ddtrace_auto_used", "origin": "unknown", "value": False}, - {"name": "ddtrace_bootstrapped", "origin": "unknown", "value": False}, + {"name": "instrumentation_source", "origin": "code", "value": "manual"}, {"name": "profiling_enabled", "origin": "default", "value": "false"}, {"name": "data_streams_enabled", "origin": "default", "value": "false"}, {"name": "appsec_enabled", "origin": "default", "value": "false"}, @@ -283,6 +283,8 @@ def test_app_started_event_configuration_override(test_agent_session, run_python env["DD_API_SECURITY_ENABLED"] = "False" env["DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING_ENABLED"] = "False" env["DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE"] = "disabled" + env["DD_INJECT_FORCE"] = "true" + env["DD_INJECTION_ENABLED"] = "tracer" # By default telemetry collection is enabled after 10 seconds, so we either need to # to sleep for 10 seconds or manually call _app_started() to generate the app started event. @@ -403,6 +405,7 @@ def test_app_started_event_configuration_override(test_agent_session, run_python {"name": "DD_IAST_STACK_TRACE_ENABLED", "origin": "default", "value": True}, {"name": "DD_IAST_TELEMETRY_VERBOSITY", "origin": "default", "value": "INFORMATION"}, {"name": "DD_IAST_VULNERABILITIES_PER_REQUEST", "origin": "default", "value": 2}, + {"name": "DD_INJECTION_ENABLED", "origin": "env_var", "value": "tracer"}, {"name": "DD_INJECT_FORCE", "origin": "env_var", "value": True}, {"name": "DD_INSTRUMENTATION_INSTALL_ID", "origin": "default", "value": None}, {"name": "DD_INSTRUMENTATION_INSTALL_TYPE", "origin": "default", "value": None}, @@ -524,10 +527,8 @@ def test_app_started_event_configuration_override(test_agent_session, run_python {"name": "DD_VERSION", "origin": "default", "value": None}, {"name": "_DD_APPSEC_DEDUPLICATION_ENABLED", "origin": "default", "value": True}, {"name": "_DD_IAST_LAZY_TAINT", "origin": "default", "value": False}, - {"name": "_DD_INJECT_WAS_ATTEMPTED", "origin": "default", "value": False}, {"name": "_DD_TRACE_WRITER_LOG_ERROR_PAYLOADS", "origin": "default", "value": False}, - {"name": "ddtrace_auto_used", "origin": "unknown", "value": True}, - {"name": "ddtrace_bootstrapped", "origin": "unknown", "value": True}, + {"name": "instrumentation_source", "origin": "code", "value": "manual"}, {"name": "python_build_gnu_type", "origin": "unknown", "value": sysconfig.get_config_var("BUILD_GNU_TYPE")}, {"name": "python_host_gnu_type", "origin": "unknown", "value": sysconfig.get_config_var("HOST_GNU_TYPE")}, {"name": "python_soabi", "origin": "unknown", "value": sysconfig.get_config_var("SOABI")}, @@ -549,6 +550,30 @@ def test_update_dependencies_event(test_agent_session, ddtrace_run_python_code_i assert len(deps) == 1, deps +def test_instrumentation_source_config( + test_agent_session, ddtrace_run_python_code_in_subprocess, run_python_code_in_subprocess +): + env = os.environ.copy() + env["_DD_INSTRUMENTATION_TELEMETRY_TESTS_FORCE_APP_STARTED"] = "true" + + _, stderr, status, _ = call_program("ddtrace-run", sys.executable, "-c", "", env=env) + assert status == 0, stderr + configs = test_agent_session.get_configurations("instrumentation_source") + assert configs and configs[-1]["value"] == "cmd_line" + test_agent_session.clear() + + _, stderr, status, _ = call_program(sys.executable, "-c", "import ddtrace.auto", env=env) + assert status == 0, stderr + configs = test_agent_session.get_configurations("instrumentation_source") + assert configs and configs[-1]["value"] == "manual" + test_agent_session.clear() + + _, stderr, status, _ = call_program(sys.executable, "-c", "import ddtrace", env=env) + assert status == 0, stderr + configs = test_agent_session.get_configurations("instrumentation_source") + assert not configs, "instrumentation_source should not be set when ddtrace instrumentation is not used" + + def test_update_dependencies_event_when_disabled(test_agent_session, ddtrace_run_python_code_in_subprocess): env = os.environ.copy() # app-started events are sent 10 seconds after ddtrace imported, this configuration overrides this