From 739a8c7742b778117605935617cdaf80cc662cdd Mon Sep 17 00:00:00 2001 From: Brett Langdon Date: Mon, 18 Nov 2024 15:29:11 -0500 Subject: [PATCH] fix(lib-injection): ensure sitecustomize.py supports Python 2.7+ (#11381) --- .github/workflows/test_lib_injection.yml | 50 ++++++++++++ lib-injection/sources/sitecustomize.py | 77 +++++++++++++------ ...tion-version-support-ae2ff3a79ac1f6f2.yaml | 4 + 3 files changed, 106 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/test_lib_injection.yml create mode 100644 releasenotes/notes/fix-lib-injection-version-support-ae2ff3a79ac1f6f2.yaml diff --git a/.github/workflows/test_lib_injection.yml b/.github/workflows/test_lib_injection.yml new file mode 100644 index 00000000000..7b9418390b8 --- /dev/null +++ b/.github/workflows/test_lib_injection.yml @@ -0,0 +1,50 @@ +name: Lib-injection tests + +on: + push: + branches: + - main + pull_request: + +jobs: + test_sitecustomize: + runs-on: ubuntu-latest + strategy: + matrix: + python: + # requires openssl 1.0, which is hard to get + # - "2.6" + # - "3.4" + # segfaults + # - 3.0" + # - 3.1" + # - "3.2" + # - "3.3" + - "2.7" + - "3.5" + - "3.6" + - "3.7" + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + steps: + - uses: actions/checkout@v4 + - name: Install pyenv + run: | + export PYENV_ROOT="${HOME}/.pyenv" + export PATH="${PYENV_ROOT}/shims:${PYENV_ROOT}/bin:${PATH}" + PYENV_GIT_TAG=main curl https://pyenv.run | bash + echo "PYENV_ROOT=${PYENV_ROOT}" >> $GITHUB_ENV + echo "PATH=${PATH}" >> $GITHUB_ENV + - name: Install python ${{ matrix.python }} + run: | + which pyenv + pyenv --version + pyenv install "${{ matrix.python }}" && pyenv global "${{ matrix.python }}" + - name: Print Python version + run: python --version + - name: Validate sitecustomize.py runs with ${{ matrix.python }} + run: python lib-injection/sources/sitecustomize.py diff --git a/lib-injection/sources/sitecustomize.py b/lib-injection/sources/sitecustomize.py index 4ad07f4c60e..7e4eefb84f5 100644 --- a/lib-injection/sources/sitecustomize.py +++ b/lib-injection/sources/sitecustomize.py @@ -5,7 +5,6 @@ from collections import namedtuple import csv -import importlib.util import json import os import platform @@ -13,34 +12,42 @@ import subprocess import sys import time -from typing import Tuple Version = namedtuple("Version", ["version", "constraint"]) -def parse_version(version: str) -> Tuple: - constraint_idx = re.search(r"\d", version).start() - numeric = version[constraint_idx:] - constraint = version[:constraint_idx] - parsed_version = tuple(int(re.sub("[^0-9]", "", p)) for p in numeric.split(".")) - return Version(parsed_version, constraint) +def parse_version(version): + try: + constraint_match = re.search(r"\d", version) + if not constraint_match: + return Version((0, 0), "") + constraint_idx = constraint_match.start() + numeric = version[constraint_idx:] + constraint = version[:constraint_idx] + parsed_version = tuple(int(re.sub("[^0-9]", "", p)) for p in numeric.split(".")) + return Version(parsed_version, constraint) + except Exception: + return Version((0, 0), "") SCRIPT_DIR = os.path.dirname(__file__) RUNTIMES_ALLOW_LIST = { - "cpython": {"min": parse_version("3.7"), "max": parse_version("3.13")}, + "cpython": { + "min": Version(version=(3, 7), constraint=""), + "max": Version(version=(3, 13), constraint=""), + } } FORCE_INJECT = os.environ.get("DD_INJECT_FORCE", "").lower() in ("true", "1", "t") FORWARDER_EXECUTABLE = os.environ.get("DD_TELEMETRY_FORWARDER_PATH", "") TELEMETRY_ENABLED = "DD_INJECTION_ENABLED" in os.environ DEBUG_MODE = os.environ.get("DD_TRACE_DEBUG", "").lower() in ("true", "1", "t") -INSTALLED_PACKAGES = None -PYTHON_VERSION = None -PYTHON_RUNTIME = None -PKGS_ALLOW_LIST = None -EXECUTABLES_DENY_LIST = None +INSTALLED_PACKAGES = {} +PYTHON_VERSION = "unknown" +PYTHON_RUNTIME = "unknown" +PKGS_ALLOW_LIST = {} +EXECUTABLES_DENY_LIST = set() VERSION_COMPAT_FILE_LOCATIONS = ( os.path.abspath(os.path.join(SCRIPT_DIR, "../datadog-lib/min_compatible_versions.csv")), os.path.abspath(os.path.join(SCRIPT_DIR, "min_compatible_versions.csv")), @@ -103,7 +110,7 @@ def build_denied_executables(): cleaned = line.strip("\n") denied_executables.add(cleaned) denied_executables.add(os.path.basename(cleaned)) - _log(f"Built denied-executables list of {len(denied_executables)} entries", level="debug") + _log("Built denied-executables list of %s entries" % (len(denied_executables),), level="debug") return denied_executables @@ -143,9 +150,15 @@ def send_telemetry(event): stderr=subprocess.PIPE, universal_newlines=True, ) - p.stdin.write(event_json) - p.stdin.close() - _log("wrote telemetry to %s" % FORWARDER_EXECUTABLE, level="debug") + if p.stdin: + p.stdin.write(event_json) + p.stdin.close() + _log("wrote telemetry to %s" % FORWARDER_EXECUTABLE, level="debug") + else: + _log( + "failed to write telemetry to %s, could not write to telemetry writer stdin" % FORWARDER_EXECUTABLE, + level="error", + ) def _get_clib(): @@ -154,7 +167,7 @@ def _get_clib(): If GNU is not detected then returns MUSL. """ - libc, version = platform.libc_ver() + libc, _ = platform.libc_ver() if libc == "glibc": return "gnu" return "musl" @@ -170,7 +183,9 @@ def _log(msg, *args, **kwargs): if DEBUG_MODE: asctime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) msg = "[%s] [%s] datadog.autoinstrumentation(pid: %d): " % (asctime, level.upper(), os.getpid()) + msg % args - print(msg, file=sys.stderr) + sys.stderr.write(msg) + sys.stderr.write("\n") + sys.stderr.flush() def runtime_version_is_supported(python_runtime, python_version): @@ -197,13 +212,13 @@ def get_first_incompatible_sysarg(): _log("sys.argv not available, skipping sys.argv check", level="debug") return - _log(f"Checking sysargs: len(argv): {len(sys.argv)}", level="debug") + _log("Checking sys.args: len(sys.argv): %s" % (len(sys.argv),), level="debug") if len(sys.argv) <= 1: return argument = sys.argv[0] - _log(f"Is argument {argument} in deny-list?", level="debug") + _log("Is argument %s in deny-list?" % (argument,), level="debug") if argument in EXECUTABLES_DENY_LIST or os.path.basename(argument) in EXECUTABLES_DENY_LIST: - _log(f"argument {argument} is in deny-list", level="debug") + _log("argument %s is in deny-list" % (argument,), level="debug") return argument @@ -225,7 +240,13 @@ def _inject(): os.environ["_DD_INJECT_WAS_ATTEMPTED"] = "true" spec = None try: + # `find_spec` is only available in Python 3.4+ + # https://docs.python.org/3/library/importlib.html#importlib.util.find_spec + # DEV: It is ok to fail here on import since it'll only fail on Python versions we don't support / inject into + import importlib.util + # None is a valid return value for find_spec (module was not found), so we need to check for it explicitly + spec = importlib.util.find_spec("ddtrace") if not spec: raise ModuleNotFoundError("ddtrace") @@ -377,5 +398,11 @@ def _inject(): try: _inject() -except Exception: - pass # absolutely never allow exceptions to propagate to the app +except Exception as e: + try: + event = gen_telemetry_payload( + [create_count_metric("library_entrypoint.error", ["error_type:" + type(e).__name__.lower()])] + ) + send_telemetry(event) + except Exception: + pass # absolutely never allow exceptions to propagate to the app diff --git a/releasenotes/notes/fix-lib-injection-version-support-ae2ff3a79ac1f6f2.yaml b/releasenotes/notes/fix-lib-injection-version-support-ae2ff3a79ac1f6f2.yaml new file mode 100644 index 00000000000..3978ccd89f4 --- /dev/null +++ b/releasenotes/notes/fix-lib-injection-version-support-ae2ff3a79ac1f6f2.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + lib-injection: Support Python 2.7+ for injection compatibility check.