diff --git a/src/usr/share/opentelemetry_shell/agent.instrumentation.python.deep.py b/src/usr/share/opentelemetry_shell/agent.instrumentation.python.deep.py index 3dc7d8bab..995ec03d2 100644 --- a/src/usr/share/opentelemetry_shell/agent.instrumentation.python.deep.py +++ b/src/usr/share/opentelemetry_shell/agent.instrumentation.python.deep.py @@ -1,42 +1,80 @@ +import sys import os -from opentelemetry.context import attach -from opentelemetry.trace.propagation import tracecontext - -traceparent = os.getenv("TRACEPARENT") -if traceparent: - propagator = tracecontext.TraceContextTextMapPropagator() - carrier = { "traceparent": traceparent } - new_context = propagator.extract(carrier=carrier) - attach(new_context) - -# import subprocess -# import functools -# -# def inject_arguments(*args): -# return [ '/bin/sh', '-c', '. otel.sh\n' + args[0] + ' "$@"', 'python' ] + args[1:] -# -# TODO get current span and set traceparent and tracestate as env var -# TODO set additional env var like auto injection, ... -# -# def observed_subprocess_run(original_subprocess_run, *args, **kwargs): -# if len(args) > 0: -# args = inject_arguments(args) -# return original_subprocess_run(*args, **kwargs) -# -# def observed_subprocess_call(original_subprocess_call, *args, **kwargs): -# if len(args) > 0: -# args = inject_arguments(args) -# return original_subprocess_call(*args, **kwargs) -# -# def observed_subprocess_Popen___init__(original_subprocess_call, *args, **kwargs): -# if len(args) > 0: -# args = inject_arguments(args) -# return observed_subprocess_Popen___init__(*args, **kwargs) -# -# def instrument(observed_function, original_function): -# # functools.update_wrapper(observed_function, original_function) # TODO why do we need this? -# return functools.partial(observed_function, original_function) -# -# subprocess.run = instrument(observed_subprocess_run, subprocess.run) -# subprocess.call = instrument(observed_subprocess_call, subprocess.call) -# subprocess.Popen.__init__ = instrument(observed_subprocess_Popen___init__, subprocess.Popen.__init__) +import subprocess + +try: + import opentelemetry + from opentelemetry.context import attach + from opentelemetry.trace.propagation import tracecontext + + traceparent = os.getenv("TRACEPARENT") + if traceparent: + propagator = tracecontext.TraceContextTextMapPropagator() + carrier = { "traceparent": traceparent } + new_context = propagator.extract(carrier=carrier) + attach(new_context) + + def inject_env(env): + if not env: + env = os.environ.copy() + carrier = {} + tracecontext.TraceContextTextMapPropagator().inject(carrier, opentelemetry.trace.set_span_in_context(opentelemetry.trace.get_current_span(), None)) + if 'traceparent' in carrier: + env["OTEL_TRACEPARENT"] = carrier["traceparent"] + if 'tracestate' in carrier: + env["OTEL_TRACESTATE"] = carrier["tracestate"] + return env; + +except ModuleNotFoundError: + def inject_env(env): + if not env: + env = os.environ.copy() + return env + +import functools + +def inject_file(file): + return '/bin/sh' + +def inject_arguments(file, args, is_file=True): + try: + file = file.decode() + except (UnicodeDecodeError, AttributeError): + pass + if is_file: + if not '/' in file: + file = './' + file + if not os.path.exists(file) or not os.path.isfile(file) or not os.access(file, os.X_OK): + raise FileNotFoundError(file) # python will just trial and error all possible paths if the 'p' variants of exec are used + file = "_otel_inject '" + file + "'" + return [ '-c', '. otel.sh\n' + file + ' "$@"', 'python' ] + args + +original_os_execve = os.execve +original_subprocess_Popen___init__ = subprocess.Popen.__init__ + +def observed_os_execv(file, args): + if type(args) is tuple: + args = list(args) + return original_os_execve(inject_file(file), [ args[0] ] + inject_arguments(file, args[1:]), inject_env(None)) + +def observed_os_execve(file, args, env): + if type(args) is tuple: + args = list(args) + return original_os_execve(inject_file(file), [ args[0] ] + inject_arguments(file, args[1:]), inject_env(env)) + +def observed_subprocess_Popen___init__(self, *args, **kwargs): + args = list(args) + if len(args) > 0 and type(args[0]) is list: + args = args[0] + args = ([ inject_file(args[0]) ] + inject_arguments(args[0], args[1:], not kwargs.get('shell', False))) + kwargs['env'] = inject_env(kwargs.get('env', None)) + if kwargs.get('shell', False): + kwargs['env']['OTEL_SHELL_COMMANDLINE_OVERRIDE'] = '/bin/sh -c ' + ' '.join(args) + kwargs['env']['OTEL_SHELL_COMMANDLINE_OVERRIDE_SIGNATURE'] = str(os.getpid()) + kwargs['env']['OTEL_SHELL_AUTO_INJECTED'] = 'FALSE' + kwargs['shell'] = False + return original_subprocess_Popen___init__(self, args, **kwargs); + +os.execv = observed_os_execv +os.execve = observed_os_execve +subprocess.Popen.__init__ = observed_subprocess_Popen___init__ diff --git a/src/usr/share/opentelemetry_shell/agent.instrumentation.python.sh b/src/usr/share/opentelemetry_shell/agent.instrumentation.python.sh index 2ce9f2cdb..641f179c3 100755 --- a/src/usr/share/opentelemetry_shell/agent.instrumentation.python.sh +++ b/src/usr/share/opentelemetry_shell/agent.instrumentation.python.sh @@ -1,17 +1,22 @@ #!/bin/false _otel_inject_python() { - if \[ "${OTEL_SHELL_CONFIG_INJECT_DEEP:-FALSE}" = TRUE ] && \[ -d "/opt/opentelemetry_shell/venv" ] && _otel_string_starts_with "$(\eval "$1 -V" | \cut -d ' ' -f 2)" "3." && ! _otel_string_ends_with "${2:-}" /pip && ! _otel_string_ends_with "${2:-}" /pip3; then + if \[ -d "/opt/opentelemetry_shell/venv" ] && _otel_string_starts_with "$(\eval "$1 -V" | \cut -d ' ' -f 2)" "3." && ! _otel_string_ends_with "${2:-}" /pip && ! _otel_string_ends_with "${2:-}" /pip3; then local cmdline="$(_otel_dollar_star "$@")" local cmdline="${cmdline#\\}" - if _otel_python_is_customize_injectable; then - local command="$1"; shift - set -- "$command" /opt/opentelemetry_shell/venv/bin/opentelemetry-instrument "${command#\\}" "$@" + if _otel_python_is_customize_injectable && \false; then + if \[ "${OTEL_SHELL_CONFIG_INJECT_DEEP:-FALSE}" = TRUE ]; then + local command="$1"; shift + set -- "$command" /opt/opentelemetry_shell/venv/bin/opentelemetry-instrument "${command#\\}" "$@" + fi OTEL_SHELL_COMMANDLINE_OVERRIDE="$cmdline" OTEL_SHELL_COMMANDLINE_OVERRIDE_SIGNATURE="0" OTEL_SHELL_AUTO_INJECTED=TRUE PYTHONPATH=/opt/opentelemetry_shell/venv/lib/"$(\ls /opt/opentelemetry_shell/venv/lib/)"/site-packages/:"${PYTHONPATH:-}" OTEL_BSP_MAX_EXPORT_BATCH_SIZE=1 _otel_call "$@" else + _otel_python_inject_args "$@" > /dev/null \eval "set -- $(_otel_python_inject_args "$@")" - local command="$1"; shift - set -- "$command" /opt/opentelemetry_shell/venv/bin/opentelemetry-instrument "${command#\\}" "$@" + if \[ "${OTEL_SHELL_CONFIG_INJECT_DEEP:-FALSE}" = TRUE ]; then + local command="$1"; shift + set -- "$command" /opt/opentelemetry_shell/venv/bin/opentelemetry-instrument "${command#\\}" "$@" + fi if \[ "${_otel_python_code_source:-}" = stdin ]; then unset _otel_python_code_source { \cat /usr/share/opentelemetry_shell/agent.instrumentation.python.deep.py; \cat; } | OTEL_SHELL_COMMANDLINE_OVERRIDE="$cmdline" OTEL_SHELL_COMMANDLINE_OVERRIDE_SIGNATURE="0" OTEL_SHELL_AUTO_INJECTED=TRUE PYTHONPATH=/opt/opentelemetry_shell/venv/lib/"$(\ls /opt/opentelemetry_shell/venv/lib/)"/site-packages/:"${PYTHONPATH:-}" OTEL_BSP_MAX_EXPORT_BATCH_SIZE=1 _otel_call "$@" @@ -70,6 +75,14 @@ _otel_python_inject_args() { _otel_escape_arg "$(\cat /usr/share/opentelemetry_shell/agent.instrumentation.python.deep.py) $arg" local injected=cmdline + elif \[ "$arg" = -m ]; then + _otel_escape_args -c + \echo -n ' ' + local arg="$1"; shift + _otel_escape_arg "$(\cat /usr/share/opentelemetry_shell/agent.instrumentation.python.deep.py) +import runpy # SKIP_DEPENDENCY_CHECK +runpy.run_module('$arg', run_name='__main__')" + local injected=module elif \[ -f "$arg" ]; then _otel_escape_args -c "$(\cat /usr/share/opentelemetry_shell/agent.instrumentation.python.deep.py) with open('$arg', 'r') as file: # SKIP_DEPENDENCY_CHECK diff --git a/tests/auto/test_auto_injection_python.shell b/tests/auto/test_auto_injection_python.shell index dcb5a4678..86b85d2cb 100755 --- a/tests/auto/test_auto_injection_python.shell +++ b/tests/auto/test_auto_injection_python.shell @@ -3,7 +3,112 @@ if ! which python3; then exit 0; fi . otel.sh -#TODO test subprocesses +echo ' +import os +os.execl("/bin/echo", "echo", "hello", "world", "0") +' | python3 +assert_equals 0 $? +span="$(resolve_span '.name == "echo hello world 0"')" +assert_equals "SpanKind.INTERNAL" $(\echo "$span" | \jq -r '.kind') +assert_not_equals null $(\echo "$span" | \jq -r '.parent_id') + +\echo ' +import os +os.execv("/bin/echo", [ "echo", "hello", "world", "1" ]) +' | python3 +assert_equals 0 $? +span="$(resolve_span '.name == "echo hello world 1"')" +assert_equals "SpanKind.INTERNAL" $(\echo "$span" | \jq -r '.kind') +assert_not_equals null $(\echo "$span" | \jq -r '.parent_id') + +echo ' +import os +os.spawnl(os.P_WAIT, "/bin/echo", "echo", "hello", "world", "2") +' | python3 +assert_equals 0 $? +span="$(resolve_span '.name == "echo hello world 2"')" +assert_equals "SpanKind.INTERNAL" $(\echo "$span" | \jq -r '.kind') +assert_not_equals null $(\echo "$span" | \jq -r '.parent_id') + +echo ' +import os +os.spawnlp(os.P_WAIT, "echo", "echo", "hello", "world", "3") +' | python3 +assert_equals 0 $? +span="$(resolve_span '.name == "echo hello world 3"')" +assert_equals "SpanKind.INTERNAL" $(\echo "$span" | \jq -r '.kind') +assert_not_equals null $(\echo "$span" | \jq -r '.parent_id') + +echo ' +import os +os.spawnv(os.P_WAIT, "/bin/echo", ["echo", "hello", "world", "4"]) +' | python3 +assert_equals 0 $? +span="$(resolve_span '.name == "echo hello world 4"')" +assert_equals "SpanKind.INTERNAL" $(\echo "$span" | \jq -r '.kind') +assert_not_equals null $(\echo "$span" | \jq -r '.parent_id') + +echo ' +import os +os.spawnvp(os.P_WAIT, "echo", ["echo", "hello", "world", "5"]) +' | python3 +assert_equals 0 $? +span="$(resolve_span '.name == "echo hello world 5"')" +assert_equals "SpanKind.INTERNAL" $(\echo "$span" | \jq -r '.kind') +assert_not_equals null $(\echo "$span" | \jq -r '.parent_id') + +printf '%s' ' +import subprocess +with subprocess.Popen(["/usr/bin/echo", "hello", "world", "6"], stdout=subprocess.DEVNULL) as proc: + pass +' | python3 +assert_equals 0 $? +span="$(resolve_span '.name == "echo hello world 6"')" +assert_equals "SpanKind.INTERNAL" $(\echo "$span" | \jq -r '.kind') +assert_not_equals null $(\echo "$span" | \jq -r '.parent_id') + +dir=$(mktemp -d) +python3 -m venv --system-site-packages "$dir"/venv || exit 1 +. "$dir"/venv/bin/activate +pip3 install opentelemetry-distro opentelemetry-exporter-otlp +opentelemetry-bootstrap --action install || exit 1 +printf '%s' ' +import subprocess +with subprocess.Popen(["/usr/bin/echo", "hello", "world", "7"], stdout=subprocess.DEVNULL) as proc: + pass +' | python3 +assert_equals 0 $? +deactivate +span="$(resolve_span '.name == "echo hello world 7"')" +assert_equals "SpanKind.INTERNAL" $(\echo "$span" | \jq -r '.kind') +assert_not_equals null $(\echo "$span" | \jq -r '.parent_id') + +echo ' +import subprocess +subprocess.run(["/usr/bin/echo", "hello", "world", "8"]) +' | python3 +assert_equals 0 $? +span="$(resolve_span '.name == "echo hello world 8"')" +assert_equals "SpanKind.INTERNAL" $(\echo "$span" | \jq -r '.kind') +assert_not_equals null $(\echo "$span" | \jq -r '.parent_id') + +echo ' +import subprocess +subprocess.run("echo hello world 9", shell=True) +' | python3 +assert_equals 0 $? +span="$(resolve_span '.name == "echo hello world 9"')" +assert_equals "SpanKind.INTERNAL" $(\echo "$span" | \jq -r '.kind') +assert_not_equals null $(\echo "$span" | \jq -r '.parent_id') + +echo ' +import subprocess +subprocess.run(["echo", "hello", "world", "10"], shell=True) +' | python3 +assert_equals 0 $? +span="$(resolve_span '.name == "echo hello world 10"')" +assert_equals "SpanKind.INTERNAL" $(\echo "$span" | \jq -r '.kind') +assert_not_equals null $(\echo "$span" | \jq -r '.parent_id') export OTEL_SHELL_CONFIG_INJECT_DEEP=TRUE