From e75a7e8fce710f9d612ee802caeebb875c0a5596 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Mon, 24 Aug 2020 04:40:22 +0530 Subject: [PATCH] Automatic exporter/provider setup for opentelemetry-instrument command. #1036 This commit adds support to the opentelemetry-instrument command to automatically configure a tracer provider and exporter. By default, it configures the OTLP exporter (like other Otel auto-instrumentations. e.g, Java: https://github.com/open-telemetry/opentelemetry-java-instrumentation#getting-started). It also allows using a different in-built or 3rd party via a CLI argument or env variable. Details can be found on opentelemetry-instrumentation's README package. Fixes #663 --- opentelemetry-instrumentation/README.rst | 78 ++++++++- .../auto_instrumentation/__init__.py | 72 +++++++- .../auto_instrumentation/sitecustomize.py | 21 ++- .../auto_instrumentation/tracing.py | 154 ++++++++++++++++++ .../instrumentation/bootstrap.py | 62 ++++++- .../opentelemetry/instrumentation/symbols.py | 13 ++ .../tests/test_auto_tracing.py | 142 ++++++++++++++++ .../tests/test_bootstrap.py | 33 +++- .../tests/test_run.py | 53 ++++-- 9 files changed, 586 insertions(+), 42 deletions(-) create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/tracing.py create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/symbols.py create mode 100644 opentelemetry-instrumentation/tests/test_auto_tracing.py diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst index 6be744251b2..05cd263235c 100644 --- a/opentelemetry-instrumentation/README.rst +++ b/opentelemetry-instrumentation/README.rst @@ -16,6 +16,31 @@ Installation This package provides a couple of commands that help automatically instruments a program: + +opentelemetry-bootstrap +----------------------- + +:: + + opentelemetry-bootstrap --action=install|requirements + +This commands inspects the active Python site-packages and figures out which +instrumentation packages the user might want to install. By default it prints out +a list of the suggested instrumentation packages which can be added to a requirements.txt +file. It also supports installing the suggested packages when run with :code:`--action=install` +flag. + +The command also installs the OTLP exporter by default. This can be overriden by specifying another +exporter using the `--exporter` or `-e` CLI flag. Multiple exporters can be installed by specifying +the flag more than once. Run `opentelemetry-bootstrap --help` to list down all supported exporters. + +Manually specifying exporters to install: + +:: + + opentelemetry-bootstrap -e=otlp -e=zipkin + + opentelemetry-instrument ------------------------ @@ -23,23 +48,60 @@ opentelemetry-instrument opentelemetry-instrument python program.py +The instrument command will try to automatically detect packages used by your python program +and when possible, apply automatic tracing instrumentation on them. This means your program +will get automatic distrubuted tracing for free without having to make any code changes +at all. This will also configure a global tracer and tracing exporter without you having to +make any code changes. By default, the instrument command will use the OTLP exporter but +this can be overrided when needed. + +The command supports the following configuration options as CLI arguments and environments vars: + + +* ``--trace-exporter`` or ``OTEL_TRACE_EXPORTER`` + +Used to specify which trace exporter to use. Can be set to one of the well-known +exporter names (see below) or a fully qualified Python import path to a trace +exporter implementation. + + - Defaults to `otlp`. + - Can be set to `none` to disbale automatic tracer initialization. + +Well known trace exporter names: + + - datadog + - jaeger + - opencensus + - otlp + - zipkin + +* ``--tracer-provider`` or ``OTEL_TRACER_PROVIDER`` + +Must be a fully qualified Python import path to a Tracer Provider implementation or +a callable that returns a tracer provider instance. + +Defaults to `opentelemetry.sdk.trace.TracerProvider` + + +* ``--service-name`` or ``OTEL_SERVICE_NAME`` + +When present the value is passed on to the relevant exporter initializer as ``service_name`` argument. + The code in ``program.py`` needs to use one of the packages for which there is an OpenTelemetry integration. For a list of the available integrations please check `here `_ +Passing arguments to program +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -opentelemetry-bootstrap ------------------------ +Any arguments passed apply to the instrument command by default. You can still pass arguments to your program by +separating them from the rest of the command with ``--``. For example, :: - opentelemetry-bootstrap --action=install|requirements + opentelemetry-instrument -e otlp flask run -- --port=3000 -This commands inspects the active Python site-packages and figures out which -instrumentation packages the user might want to install. By default it prints out -a list of the suggested instrumentation packages which can be added to a requirements.txt -file. It also supports installing the suggested packages when run with :code:`--action=install` -flag. +The above command will pass ``-e otlp` to the instrument command and ``--port=3000`` to ``flask run``. References ---------- diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py index 893b8939b93..1fd667a8880 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py @@ -14,16 +14,80 @@ # See the License for the specific language governing permissions and # limitations under the License. +import argparse from logging import getLogger from os import environ, execl, getcwd from os.path import abspath, dirname, pathsep from shutil import which -from sys import argv + +from opentelemetry.instrumentation import symbols logger = getLogger(__file__) +def parse_args(): + parser = argparse.ArgumentParser( + description=""" + opentelemetry-instrument automatically instruments a Python + program and it's dependencies and then runs the program. + """ + ) + + parser.add_argument( + "-tp", + "--tracer-provider", + required=False, + help=""" + Uses the specified tracer provider. + Must be a fully qualified python import path to a tracer + provider implementation or a callable that returns a new + instance of a tracer provider. + """, + ) + + parser.add_argument( + "-e", + "--exporter", + required=False, + help=""" + Uses the specified exporter to export spans. + + Must be one of the following: + - Name of a well-known trace exporter. Choices are: + {0} + - A fully qualified python import path to a trace exporter + implementation or a callable that returns a new instance + of a trace exporter. + """.format( + symbols.trace_exporters + ), + ) + + parser.add_argument( + "-s", + "--service-name", + required=False, + help=""" + The service name that should be passed to a trace exporter. + """, + ) + + parser.add_argument("command", nargs="+") + return parser.parse_args() + + +def set_default_env_vars(args): + if args.exporter: + environ["OTEL_TRACE_EXPORTER"] = args.exporter + if args.tracer_provider: + environ["OTEL_TRACE_PROVIDER"] = args.tracer_provider + if args.service_name: + environ["OTEL_SERVICE_NAME"] = args.service_name + + def run() -> None: + args = parse_args() + set_default_env_vars(args) python_path = environ.get("PYTHONPATH") @@ -49,6 +113,6 @@ def run() -> None: environ["PYTHONPATH"] = pathsep.join(python_path) - executable = which(argv[1]) - - execl(executable, executable, *argv[2:]) + command = args.command + executable = which(command[0]) + execl(executable, executable, *command[1:]) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py index b070bf5d773..603af89f3e0 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py @@ -16,13 +16,22 @@ from pkg_resources import iter_entry_points +from opentelemetry.instrumentation.auto_instrumentation.tracing import ( + initialize_tracing, +) + logger = getLogger(__file__) -for entry_point in iter_entry_points("opentelemetry_instrumentor"): - try: - entry_point.load()().instrument() # type: ignore - logger.debug("Instrumented %s", entry_point.name) +def auto_instrument(): + for entry_point in iter_entry_points("opentelemetry_instrumentor"): + try: + entry_point.load()().instrument() # type: ignore + logger.debug("Instrumented %s", entry_point.name) + + except Exception: # pylint: disable=broad-except + logger.exception("Instrumenting of %s failed", entry_point.name) + - except Exception: # pylint: disable=broad-except - logger.exception("Instrumenting of %s failed", entry_point.name) +initialize_tracing() +auto_instrument() diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/tracing.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/tracing.py new file mode 100644 index 00000000000..50447b9f81a --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/tracing.py @@ -0,0 +1,154 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import defaultdict +from functools import partial +from logging import getLogger + +from opentelemetry import trace +from opentelemetry.configuration import Configuration +from opentelemetry.instrumentation import symbols +from opentelemetry.sdk.resources import Resource + +logger = getLogger(__file__) + + +# Defaults +_DEFAULT_TRACE_EXPORTER = symbols.exporter_otlp +_DEFAULT_TRACER_PROVIDER = "opentelemetry.sdk.trace.TracerProvider" +_DEFAULT_SPAN_PROCESSOR = ( + "opentelemetry.sdk.trace.export.BatchExportSpanProcessor" +) + + +_trace_exporter_classes = { + symbols.exporter_dd: ( + "opentelemetry.exporter.datadog.DatadogSpanExporter" + ), + symbols.exporter_oc: ( + "opentelemetry.exporter.opencensus" + ".trace_exporter.OpenCensusSpanExporter" + ), + symbols.exporter_otlp: ( + "opentelemetry.exporter.otlp.trace_exporter.OTLPSpanExporter" + ), + symbols.exporter_jaeger: ( + "opentelemetry.exporter.jaeger.JaegerSpanExporter" + ), + symbols.exporter_zipkin: ( + "opentelemetry.exporter.zipkin.ZipkinSpanExporter" + ), +} + +_span_processors_by_exporter = defaultdict( + lambda: _DEFAULT_SPAN_PROCESSOR, + { + symbols.exporter_dd: ( + "opentelemetry.exporter.datadog.DatadogExportSpanProcessor" + ) + }, +) + + +def get_service_name(): + return Configuration().SERVICE_NAME or "" + + +def get_tracer_provider(): + return Configuration().TRACER_PROVIDER or _DEFAULT_TRACER_PROVIDER + + +def get_exporter_name(): + return Configuration().TRACE_EXPORTER or _DEFAULT_TRACE_EXPORTER + + +def _trace_init( + trace_exporter, tracer_provider, span_processor, +): + exporter = trace_exporter() + processor = span_processor(exporter) + provider = tracer_provider() + trace.set_tracer_provider(provider) + provider.add_span_processor(processor) + + +def _default_trace_init(exporter, provider, processor): + service_name = get_service_name() + if service_name: + exporter = partial(exporter, service_name=get_service_name()) + _trace_init(exporter, provider, processor) + + +def _otlp_trace_init(exporter, provider, processor): + resource = Resource(labels={"service_name": get_service_name()}) + provider = partial(provider, resource=resource) + _trace_init(exporter, provider, processor) + + +def _dd_trace_init(exporter, provider, processor): + exporter = partial(exporter, service=get_service_name()) + _trace_init(exporter, provider, processor) + + +_initializers = defaultdict( + lambda: _default_trace_init, + { + symbols.exporter_dd: _dd_trace_init, + symbols.exporter_otlp: _otlp_trace_init, + }, +) + + +def _import(import_path): + split_path = import_path.rsplit(".", 1) + if len(split_path) < 2: + raise ImportError( + "could not import module or class: {0}".format(import_path) + ) + module, class_name = split_path + mod = __import__(module, fromlist=[class_name]) + return getattr(mod, class_name) + + +def _load_component(components, name): + if name.lower() == "none": + return None + + component = components.get(name.lower(), name) + if not component: + logger.info("component not found with name: {0}".format(name)) + return + + if isinstance(component, str): + try: + return _import(component) + except ImportError as exc: + logger.error(exc.msg) + return None + return component + + +def initialize_tracing(): + exporter_name = get_exporter_name() + print("exporter: ", get_exporter_name()) + TraceExporter = _load_component(_trace_exporter_classes, exporter_name) + if TraceExporter is None: + logger.info("not using any trace exporter") + return + + print("provider: ", get_tracer_provider()) + TracerProvider = _load_component({}, get_tracer_provider()) + SpanProcessor = _import(_span_processors_by_exporter[exporter_name]) + initializer = _initializers[exporter_name] + initializer(TraceExporter, TracerProvider, SpanProcessor) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index 125032b95da..5430d033697 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -20,6 +20,10 @@ import sys from logging import getLogger +from opentelemetry.instrumentation import symbols + +from .version import __version__ + logger = getLogger(__file__) @@ -51,7 +55,8 @@ "wsgi": "opentelemetry-instrumentation-wsgi>=0.8b0", } -# relevant instrumentors and tracers to uninstall and check for conflicts for target libraries +# relevant instrumentors and tracers to uninstall and check for +# conflicts for target libraries libraries = { "asgi": ("opentelemetry-instrumentation-asgi",), "asyncpg": ("opentelemetry-instrumentation-asyncpg",), @@ -80,7 +85,7 @@ } -def _install_package(library, instrumentation): +def _install_instrumentation(library, instrumentation): """ Ensures that desired version is installed w/o upgrading its dependencies by uninstalling where necessary (if `target` is not provided). @@ -102,6 +107,10 @@ def _install_package(library, instrumentation): _sys_pip_install(instrumentation) +def _install_exporter(name, package): + _sys_pip_install(package) + + def _syscall(func): def wrapper(package=None): try: @@ -159,7 +168,8 @@ def _pip_check(): Clean check reported as: 'No broken requirements found.' Dependency conflicts are reported as: - 'opentelemetry-instrumentation-flask 1.0.1 has requirement opentelemetry-sdk<2.0,>=1.0, but you have opentelemetry-sdk 0.5.' + 'opentelemetry-instrumentation-flask 1.0.1 has requirement + opentelemetry-sdk<2.0,>=1.0, but you have opentelemetry-sdk 0.5.' To not be too restrictive, we'll only check for relevant packages. """ check_pipe = subprocess.Popen( @@ -183,17 +193,36 @@ def _find_installed_libraries(): return {k: v for k, v in instrumentations.items() if _is_installed(k)} -def _run_requirements(packages): +def _run_requirements(instrumentations, exporters): + packages = {} + packages.update(instrumentations) + packages.update(exporters) print("\n".join(packages.values()), end="") -def _run_install(packages): - for pkg, inst in packages.items(): - _install_package(pkg, inst) +def _run_install(instrumentations, exporters): + for pkg, inst in instrumentations.items(): + _install_instrumentation(pkg, inst) + + for pkg, inst in exporters.items(): + _install_exporter(pkg, inst) _pip_check() +def _exporter_packages_from_names(exporters): + return { + exp: "opentelemetry-exporter-{0}>={1}".format(exp, __version__) + for exp in exporters + } + + +def _compile_package_list(exporters): + packages = _find_installed_libraries() + packages.update(_exporter_packages_from_names(exporters)) + return packages + + def run() -> None: action_install = "install" action_requirements = "requirements" @@ -216,10 +245,27 @@ def run() -> None: be piped and appended to a requirements.txt file. """, ) + parser.add_argument( + "-e", + "--exporter", + action="append", + choices=symbols.trace_exporters, + help=""" + Installs one or more support telemetry exporters. Supports multiple + values separated by commas. + + Defaults to `otlp`. + """, + ) args = parser.parse_args() cmd = { action_install: _run_install, action_requirements: _run_requirements, }[args.action] - cmd(_find_installed_libraries()) + cmd( + _find_installed_libraries(), + _exporter_packages_from_names( + args.exporter or [symbols.exporter_otlp] + ), + ) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/symbols.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/symbols.py new file mode 100644 index 00000000000..7698ba7269e --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/symbols.py @@ -0,0 +1,13 @@ +exporter_dd = "datadog" +exporter_jaeger = "jaeger" +exporter_oc = "opencensus" +exporter_otlp = "otlp" +exporter_zipkin = "zipkin" + +trace_exporters = ( + exporter_dd, + exporter_jaeger, + exporter_oc, + exporter_otlp, + exporter_zipkin, +) diff --git a/opentelemetry-instrumentation/tests/test_auto_tracing.py b/opentelemetry-instrumentation/tests/test_auto_tracing.py new file mode 100644 index 00000000000..5fee12bb57a --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_auto_tracing.py @@ -0,0 +1,142 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +from os import environ +from unittest import TestCase +from unittest.mock import patch + +from opentelemetry.configuration import Configuration +from opentelemetry.instrumentation import auto_instrumentation +from opentelemetry.instrumentation.auto_instrumentation import tracing +from opentelemetry.sdk.resources import Resource + + +class TestDefaultAndConfig(TestCase): + def test_initializers(self): + pass + + def test_providers(self): + pass + + def test_exporters(self): + pass + + def test_processors(self): + pass + + +class TestLoading(TestCase): + def test_import(self): # pylint: disable=no-self-use + with self.assertRaises(ImportError): + tracing._import("non-existent-module") + + imported = tracing._import( + "opentelemetry.instrumentation.auto_instrumentation.tracing" + ) + self.assertEqual(imported, tracing) + + def test_load(self): # pylint: disable=no-self-use + self.assertIsNone(tracing._load_component({}, "None")) + self.assertIsNone(tracing._load_component({}, "component")) + self.assertIsNotNone( + tracing._load_component({}, "opentelemetry.instrumentation") + ) + mod = tracing._load_component( + { + "that-module": ( + "opentelemetry.instrumentation.auto_instrumentation" + ) + }, + "that-module", + ) + self.assertEqual(mod, auto_instrumentation) + + +class TestTraceInit(TestCase): + @patch("opentelemetry.trace.set_tracer_provider") + def test_trace_init_default(self, mock_set_provider): + environ["OTEL_SERVICE_NAME"] = "my-test-service" + Configuration._reset() + tracing._default_trace_init(Exporter, Provider, Processor) + + self.assertEqual(mock_set_provider.call_count, 1) + provider = mock_set_provider.call_args[0][0] + self.assertIsInstance(provider, Provider) + self.assertIsInstance(provider.processor, Processor) + self.assertIsInstance(provider.processor.exporter, Exporter) + self.assertEqual( + provider.processor.exporter.service_name, "my-test-service" + ) + + @patch("opentelemetry.trace.set_tracer_provider") + def test_trace_init_otlp(self, mock_set_provider): + environ["OTEL_SERVICE_NAME"] = "my-otlp-test-service" + Configuration._reset() + tracing._otlp_trace_init(OTLPExporter, Provider, Processor) + + self.assertEqual(mock_set_provider.call_count, 1) + provider = mock_set_provider.call_args[0][0] + self.assertIsInstance(provider, Provider) + self.assertIsInstance(provider.processor, Processor) + self.assertIsInstance(provider.processor.exporter, OTLPExporter) + self.assertIsInstance(provider.resource, Resource) + self.assertEqual( + provider.resource.labels.get("service_name"), + "my-otlp-test-service", + ) + del environ["OTEL_SERVICE_NAME"] + + @patch("opentelemetry.trace.set_tracer_provider") + def test_trace_init_dd(self, mock_set_provider): + environ["OTEL_SERVICE_NAME"] = "my-dd-test-service" + Configuration._reset() + tracing._dd_trace_init(DDExporter, Provider, Processor) + + self.assertEqual(mock_set_provider.call_count, 1) + provider = mock_set_provider.call_args[0][0] + self.assertIsInstance(provider, Provider) + self.assertIsInstance(provider.processor, Processor) + self.assertIsInstance(provider.processor.exporter, DDExporter) + self.assertEqual( + provider.processor.exporter.service, "my-dd-test-service" + ) + + +class Provider: + def __init__(self, resource=None): + self.processor = None + self.resource = resource + + def add_span_processor(self, processor): + self.processor = processor + + +class Processor: + def __init__(self, exporter): + self.exporter = exporter + + +class Exporter: + def __init__(self, service_name): + self.service_name = service_name + + +class DDExporter: + def __init__(self, service): + self.service = service + + +class OTLPExporter: + pass diff --git a/opentelemetry-instrumentation/tests/test_bootstrap.py b/opentelemetry-instrumentation/tests/test_bootstrap.py index e5a1a86dda5..766ce90f0e5 100644 --- a/opentelemetry-instrumentation/tests/test_bootstrap.py +++ b/opentelemetry-instrumentation/tests/test_bootstrap.py @@ -19,7 +19,9 @@ from unittest import TestCase from unittest.mock import call, patch -from opentelemetry.instrumentation import bootstrap +from opentelemetry.instrumentation import bootstrap, version + +default_exporter = "otlp" def sample_packages(packages, rate): @@ -27,6 +29,13 @@ def sample_packages(packages, rate): return {k: v for k, v in packages.items() if k in sampled} +def packages_from_exporter_names(exporters): + return [ + "opentelemetry-exporter-{0}>={1}".format(exp, version.__version__) + for exp in exporters + ] + + class TestBootstrap(TestCase): installed_libraries = {} @@ -45,7 +54,10 @@ def setUpClass(cls): ) cls.pkg_patcher = patch( - "opentelemetry.instrumentation.bootstrap._find_installed_libraries", + ( + "opentelemetry.instrumentation.bootstrap" + "._find_installed_libraries" + ), return_value=cls.installed_libraries, ) @@ -91,12 +103,21 @@ def test_run_unknown_cmd(self): @patch("sys.argv", ["bootstrap", "-a", "requirements"]) def test_run_cmd_print(self): + self._test_run([default_exporter]) + + @patch("sys.argv", ["bootstrap", "-e", "zipkin", "-e", "jaeger"]) + def test_exporters(self): + self._test_run(["zipkin", "jaeger"]) + + def _test_run(self, exporters): + exporters = packages_from_exporter_names(exporters) with patch("sys.stdout", new=StringIO()) as fake_out: bootstrap.run() - self.assertEqual( - fake_out.getvalue(), - "\n".join(self.installed_libraries.values()), - ) + value = fake_out.getvalue() + for lib in ( + list(self.installed_instrumentations.values()) + exporters + ): + self.assertIn(lib, value) @patch("sys.argv", ["bootstrap", "-a", "install"]) def test_run_cmd_install(self): diff --git a/opentelemetry-instrumentation/tests/test_run.py b/opentelemetry-instrumentation/tests/test_run.py index 21f53babc6d..64e44536522 100644 --- a/opentelemetry-instrumentation/tests/test_run.py +++ b/opentelemetry-instrumentation/tests/test_run.py @@ -26,9 +26,6 @@ class TestRun(TestCase): @classmethod def setUpClass(cls): - cls.argv_patcher = patch( - "opentelemetry.instrumentation.auto_instrumentation.argv" - ) cls.execl_patcher = patch( "opentelemetry.instrumentation.auto_instrumentation.execl" ) @@ -36,16 +33,15 @@ def setUpClass(cls): "opentelemetry.instrumentation.auto_instrumentation.which" ) - cls.argv_patcher.start() cls.execl_patcher.start() cls.which_patcher.start() @classmethod def tearDownClass(cls): - cls.argv_patcher.stop() cls.execl_patcher.stop() cls.which_patcher.stop() + @patch("sys.argv", ["instrument", ""]) @patch.dict("os.environ", {"PYTHONPATH": ""}) def test_empty(self): auto_instrumentation.run() @@ -54,6 +50,7 @@ def test_empty(self): pathsep.join([self.auto_instrumentation_path, getcwd()]), ) + @patch("sys.argv", ["instrument", ""]) @patch.dict("os.environ", {"PYTHONPATH": "abc"}) def test_non_empty(self): auto_instrumentation.run() @@ -62,6 +59,7 @@ def test_non_empty(self): pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), ) + @patch("sys.argv", ["instrument", ""]) @patch.dict( "os.environ", {"PYTHONPATH": pathsep.join(["abc", auto_instrumentation_path])}, @@ -73,6 +71,7 @@ def test_after_path(self): pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), ) + @patch("sys.argv", ["instrument", ""]) @patch.dict( "os.environ", { @@ -90,10 +89,7 @@ def test_single_path(self): class TestExecl(TestCase): - @patch( - "opentelemetry.instrumentation.auto_instrumentation.argv", - new=[1, 2, 3], - ) + @patch("sys.argv", ["1", "2", "3"]) @patch("opentelemetry.instrumentation.auto_instrumentation.which") @patch("opentelemetry.instrumentation.auto_instrumentation.execl") def test_execl( @@ -103,4 +99,41 @@ def test_execl( auto_instrumentation.run() - mock_execl.assert_called_with("python", "python", 3) + mock_execl.assert_called_with("python", "python", "3") + + +class TestArgs(TestCase): + @patch("opentelemetry.instrumentation.auto_instrumentation.execl") + def test_exporter(self, mock_execl): # pylint: disable=no-self-use + with patch("sys.argv", ["instrument", "2"]): + auto_instrumentation.run() + self.assertIsNone(environ.get("OTEL_TRACE_EXPORTER")) + + with patch("sys.argv", ["instrument", "-e", "zipkin", "1", "2"]): + auto_instrumentation.run() + self.assertEqual(environ.get("OTEL_TRACE_EXPORTER"), "zipkin") + + @patch("opentelemetry.instrumentation.auto_instrumentation.execl") + def test_provider(self, mock_execl): # pylint: disable=no-self-use + with patch("sys.argv", ["instrument", "2"]): + auto_instrumentation.run() + self.assertIsNone(environ.get("OTEL_TRACE_PROVIDER")) + + with patch( + "sys.argv", + ["instrument", "-tp", "custom.provider.Provider", "1", "2"], + ): + auto_instrumentation.run() + self.assertEqual( + environ.get("OTEL_TRACE_PROVIDER"), "custom.provider.Provider" + ) + + @patch("opentelemetry.instrumentation.auto_instrumentation.execl") + def test_service_name(self, mock_execl): # pylint: disable=no-self-use + with patch("sys.argv", ["instrument", "2"]): + auto_instrumentation.run() + self.assertIsNone(environ.get("OTEL_SERVICE_NAME")) + + with patch("sys.argv", ["instrument", "-s", "my-service", "1", "2"]): + auto_instrumentation.run() + self.assertEqual(environ.get("OTEL_SERVICE_NAME"), "my-service")