From 15d2d192659f896107ffc0eb7f17ebce7e345997 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Mon, 24 Aug 2020 04:40:22 +0530 Subject: [PATCH] Add true auto-instrumentation support to opentelemetry-instrument This commit extends the instrument command so it automatically configures tracing with a provider, span processor and exporter. Most of the component used can be customized with env vars or CLI arguments. Details can be found on opentelemetry-instrumentation's README package. Fixes #663 --- .../instrumentation/django/middleware.py | 3 +- opentelemetry-instrumentation/CHANGELOG.md | 3 +- opentelemetry-instrumentation/README.rst | 80 ++++++++- .../auto_instrumentation/__init__.py | 62 ++++++- .../auto_instrumentation/components.py | 158 ++++++++++++++++++ .../auto_instrumentation/sitecustomize.py | 40 ++++- .../instrumentation/bootstrap.py | 65 ++++++- .../opentelemetry/instrumentation/symbols.py | 23 +++ .../tests/test_auto_tracing.py | 157 +++++++++++++++++ .../tests/test_bootstrap.py | 33 +++- .../tests/test_run.py | 38 +++-- 11 files changed, 617 insertions(+), 45 deletions(-) create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/components.py create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/symbols.py create mode 100644 opentelemetry-instrumentation/tests/test_auto_tracing.py diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py index 07e3eb710b8..31deb7fcb7f 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -43,8 +43,7 @@ class _DjangoMiddleware(MiddlewareMixin): - """Django Middleware for OpenTelemetry - """ + """Django Middleware for OpenTelemetry""" _environ_activation_key = ( "opentelemetry-instrumentor-django.activation_key" diff --git a/opentelemetry-instrumentation/CHANGELOG.md b/opentelemetry-instrumentation/CHANGELOG.md index 13c01cc32a6..ab382e0a960 100644 --- a/opentelemetry-instrumentation/CHANGELOG.md +++ b/opentelemetry-instrumentation/CHANGELOG.md @@ -2,7 +2,8 @@ ## Unreleased -- Fixed boostrap command to correctly install opentelemetry-instrumentation-falcon instead of opentelemetry-instrumentation-flask +- Fixed boostrap command to correctly install opentelemetry-instrumentation-falcon instead of opentelemetry-instrumentation-flask. ([#1138](https://github.com/open-telemetry/opentelemetry-python/pull/1138)) +- Added support for `OTEL_EXPORTER` to the `opentelemetry-instrument` command ([#1036](https://github.com/open-telemetry/opentelemetry-python/pull/1036)) ## Version 0.13b0 diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst index 6be744251b2..3ce1d4a5215 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 for both spans and metrics. This can +be overriden by specifying another exporter using the `--exporter` or `-e` CLI flag. The flag +accepts multiple values to install multiple exporters. Run `opentelemetry-bootstrap --help` +to list down all supported exporters. + +Manually specifying exporters to install: + +:: + + opentelemetry-bootstrap -e otlp zipkin + + opentelemetry-instrument ------------------------ @@ -23,23 +48,62 @@ 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: + + +* ``--exporter`` or ``OTEL_EXPORTER`` + +Used to specify which trace exporter to use. Can be set to one or more +of the well-known exporter names (see below) or a fully +qualified Python import path to a span exporter implementation. + + - Defaults to `otlp`. + - Can be set to `none` to disbale automatic tracer initialization. + +You can pass multiple values to configure multiple exporters e.g, ``zipkin,prometheus`` + +Well known trace exporter names: + + - datadog + - jaeger + - opencensus + - otlp + - otlp_span + - otlp_metric + - zipkin + +``otlp`` is an alias for ``otlp_span,otlp_metric``. + +* ``--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 `_ +Examples +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -opentelemetry-bootstrap ------------------------ +:: + + opentelemetry-instrument -e otlp flask run --port=3000 + +The above command will pass ``-e otlp`` to the instrument command and ``--port=3000`` to ``flask run``. :: - opentelemetry-bootstrap --action=install|requirements + opentelemetry-instrument -e zipkin,otlp celery -A tasks worker --loglevel=info -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 configure global trace provider, attach zipkin and otlp exporters to it and then +start celery with the rest of the arguments. 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..1ab22e356be 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py @@ -14,16 +14,71 @@ # 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( + "-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", help="Your Python application.") + parser.add_argument( + "command_args", + help="Arguments for your application.", + nargs=argparse.REMAINDER, + ) + return parser.parse_args() + + +def load_config_from_cli_args(args): + if args.exporter: + environ["OTEL_EXPORTER"] = args.exporter + if args.service_name: + environ["OTEL_SERVICE_NAME"] = args.service_name + + def run() -> None: + args = parse_args() + load_config_from_cli_args(args) python_path = environ.get("PYTHONPATH") @@ -49,6 +104,5 @@ def run() -> None: environ["PYTHONPATH"] = pathsep.join(python_path) - executable = which(argv[1]) - - execl(executable, executable, *argv[2:]) + executable = which(args.command) + execl(executable, executable, *args.command_args) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/components.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/components.py new file mode 100644 index 00000000000..8e3fa41dd6a --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/components.py @@ -0,0 +1,158 @@ +# 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 logging import getLogger +from typing import Sequence, Tuple + +from opentelemetry import trace +from opentelemetry.configuration import Configuration +from opentelemetry.instrumentation import symbols +from opentelemetry.sdk.metrics.export import MetricsExporter +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + BatchExportSpanProcessor, + SpanExporter, + SpanProcessor, +) + +logger = getLogger(__file__) + +_DEFAULT_EXPORTER = symbols.exporter_otlp + +known_exporters = { + symbols.exporter_otlp: ( + "opentelemetry.exporter.otlp.trace_exporter.OTLPSpanExporter", + "opentelemetry.exporter.otlp.metrics_exporter.OTLPMetricsExporter", + ), + symbols.exporter_dd: ( + "opentelemetry.exporter.datadog.DatadogSpanExporter", + ), + symbols.exporter_oc: ( + "opentelemetry.exporter.opencensus.trace_exporter.OpenCensusSpanExporter", + ), + symbols.exporter_otlp_span: ( + "opentelemetry.exporter.otlp.trace_exporter.OTLPSpanExporter", + ), + symbols.exporter_otlp_metric: ( + "opentelemetry.exporter.otlp.metrics_exporter.OTLPMetricsExporter" + ), + symbols.exporter_jaeger: ( + "opentelemetry.exporter.jaeger.JaegerSpanExporter", + ), + symbols.exporter_zipkin: ( + "opentelemetry.exporter.zipkin.ZipkinSpanExporter", + ), + symbols.exporter_prometheus: ( + "opentelemetry.exporter.prometheus.PrometheusMetricsExporter", + ), +} + + +def _import(import_path: str) -> any: + 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 get_service_name() -> str: + return Configuration().SERVICE_NAME or "" + + +def get_exporter_names() -> Sequence[str]: + exporter = Configuration().EXPORTER or _DEFAULT_EXPORTER + if exporter.lower().strip() == "none": + return [] + + return [e.strip() for e in exporter.split(",")] + + +def get_tracer_provider_class() -> trace.TracerProvider: + return TracerProvider + + +def get_processor_class_for_exporter(exporter_name: str) -> SpanProcessor: + if exporter_name == symbols.exporter_dd: + return _import( + "opentelemetry.exporter.datadog.DatadogExportSpanProcessor" + ) + return BatchExportSpanProcessor + + +def init_tracing(exporters: Sequence[SpanExporter]): + service_name = get_service_name() + provider = get_tracer_provider_class()( + resource=Resource.create({"service.name": service_name}), + ) + trace.set_tracer_provider(provider) + + for exporter_name, exporter_class in exporters.items(): + processor_class = get_processor_class_for_exporter(exporter_name) + + exporter_args = {} + if exporter_name == symbols.exporter_dd: + exporter_args["service"] = service_name + elif exporter_name not in [ + symbols.exporter_otlp, + symbols.exporter_otlp_span, + ]: + exporter_args["service_name"] = service_name + + provider.add_span_processor( + processor_class(exporter_class(**exporter_args)) + ) + + +def init_metrics(exporters: Sequence[MetricsExporter]): + if exporters: + logger.warning("automatic metric initialization is not supported yet.") + + +def import_exporters( + exporter_names: Sequence[str], +) -> Tuple[Sequence[SpanExporter], Sequence[MetricsExporter]]: + trace_exporters, metric_exporters = {}, {} + for exporter_name in exporter_names: + print(">> ", exporter_name) + for exporter_path in known_exporters.get( + exporter_name, [exporter_name] + ): + exporter_impl = _import(exporter_path) + if issubclass(exporter_impl, SpanExporter): + trace_exporters[exporter_name] = exporter_impl + elif issubclass(exporter_impl, MetricsExporter): + metric_exporters[exporter_name] = exporter_impl + else: + raise RuntimeError( + "{0} ({1}) is neither a trace exporter nor a metric exporter".format( + exporter_name, exporter_path + ) + ) + return trace_exporters, metric_exporters + + +def initialize_components(): + exporter_names = get_exporter_names() + trace_exporters, metric_exporters = import_exporters(exporter_names) + init_tracing(trace_exporters) + + # We don't support automatic initialization for metric yet but have added + # some boilerplate in order to make sure current implementation does not + # lock us out of supporting metrics later without major surgery. + init_metrics(metric_exporters) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py index b070bf5d773..737986cb9ba 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py @@ -12,17 +12,45 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os +import sys from logging import getLogger from pkg_resources import iter_entry_points +from opentelemetry.instrumentation.auto_instrumentation.components import ( + initialize_components, +) + 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) + + +def initialize(): + initialize_components() + auto_instrument() + + +if ( + hasattr(sys, "argv") + and sys.argv[0].split(os.path.sep)[-1] == "celery" + and "worker" in sys.argv +): + from celery.signals import worker_process_init # pylint:disable=E0401 + + @worker_process_init.connect(weak=False) + def init_celery(*args, **kwargs): + initialize() + - except Exception: # pylint: disable=broad-except - logger.exception("Instrumenting of %s failed", entry_point.name) +else: + initialize() diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index 4b6a677a84f..e15b5994482 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -20,8 +20,14 @@ import sys from logging import getLogger +from opentelemetry.instrumentation import symbols +from opentelemetry.instrumentation.version import __version__ + logger = getLogger(__file__) +exporters = [ + e for e in symbols.exporters if e not in ["otlp_span", "otlp_metric"] +] # target library to desired instrumentor path/versioned package name instrumentations = { @@ -53,7 +59,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",), @@ -84,7 +91,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). @@ -106,6 +113,10 @@ def _install_package(library, instrumentation): _sys_pip_install(instrumentation) +def _install_exporter(package): + _sys_pip_install(package) + + def _syscall(func): def wrapper(package=None): try: @@ -163,7 +174,9 @@ 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( @@ -187,17 +200,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(instrumentation_packages, exporters): + packages = {} + packages.update(instrumentation_packages) + 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(instrumentation_packages, exporters): + for pkg, inst in instrumentation_packages.items(): + _install_instrumentation(pkg, inst) + + for pkg, inst in exporters.items(): + _install_exporter(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" @@ -220,10 +252,27 @@ def run() -> None: be piped and appended to a requirements.txt file. """, ) + parser.add_argument( + "-e", + "--exporter", + nargs="+", + choices=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..6f3157496b1 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/symbols.py @@ -0,0 +1,23 @@ +exporter_dd = "datadog" +exporter_jaeger = "jaeger" +exporter_oc = "opencensus" +exporter_otlp = "otlp" +exporter_otlp_span = "otlp_span" +exporter_otlp_metric = "otlp_metric" +exporter_prometheus = "prometheus" +exporter_zipkin = "zipkin" + +trace_exporters = ( + exporter_dd, + exporter_jaeger, + exporter_oc, + exporter_otlp_span, + exporter_zipkin, +) + +metric_exportrs = ( + exporter_otlp_metric, + exporter_prometheus, +) + +exporters = (exporter_otlp,) + trace_exporters + metric_exportrs diff --git a/opentelemetry-instrumentation/tests/test_auto_tracing.py b/opentelemetry-instrumentation/tests/test_auto_tracing.py new file mode 100644 index 00000000000..436724b51e3 --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_auto_tracing.py @@ -0,0 +1,157 @@ +# 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.auto_instrumentation import components +from opentelemetry.sdk.resources import Resource + + +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 + + def shutdown(self): + pass + + +class DDExporter: + def __init__(self, service): + self.service = service + + def shutdown(self): + pass + + +class OTLPExporter: + pass + + +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): + # pylint: disable=protected-access + def test_import(self): # pylint: disable=no-self-use + with self.assertRaises(ImportError): + components._import("non-existent-module") + + imported = components._import( + "opentelemetry.instrumentation.auto_instrumentation.components" + ) + self.assertEqual(imported, components) + + +class TestTraceInit(TestCase): + def setUp(self): + super() + self.get_provider_patcher = patch( + "opentelemetry.instrumentation.auto_instrumentation.components.get_tracer_provider_class", + return_value=Provider, + ) + self.get_processor_patcher = patch( + "opentelemetry.instrumentation.auto_instrumentation.components.get_processor_class_for_exporter", + return_value=Processor, + ) + self.set_provider_patcher = patch( + "opentelemetry.trace.set_tracer_provider" + ) + + self.get_provider_mock = self.get_provider_patcher.start() + self.get_processor_mock = self.get_processor_patcher.start() + self.set_provider_mock = self.set_provider_patcher.start() + + def tearDown(self): + super() + self.get_provider_patcher.stop() + self.get_processor_patcher.stop() + self.set_provider_patcher.stop() + + # pylint: disable=protected-access + def test_trace_init_default(self): + # mock_get_provider.return_value = Provider + environ["OTEL_SERVICE_NAME"] = "my-test-service" + Configuration._reset() + components.init_tracing({"zipkin": Exporter}) + # , Provider, Processor) + + print(self.set_provider_mock.call_args) + self.assertEqual(self.set_provider_mock.call_count, 1) + provider = self.set_provider_mock.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" + ) + + def test_trace_init_otlp(self): + environ["OTEL_SERVICE_NAME"] = "my-otlp-test-service" + Configuration._reset() + components.init_tracing({"otlp": OTLPExporter}) + + self.assertEqual(self.set_provider_mock.call_count, 1) + provider = self.set_provider_mock.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.attributes.get("service.name"), + "my-otlp-test-service", + ) + del environ["OTEL_SERVICE_NAME"] + + def test_trace_init_dd(self): + environ["OTEL_SERVICE_NAME"] = "my-dd-test-service" + Configuration._reset() + components.init_tracing({"datadog": DDExporter}) + + self.assertEqual(self.set_provider_mock.call_count, 1) + provider = self.set_provider_mock.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" + ) diff --git a/opentelemetry-instrumentation/tests/test_bootstrap.py b/opentelemetry-instrumentation/tests/test_bootstrap.py index e5a1a86dda5..86be2a82785 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", "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..9bff8514b14 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,26 @@ 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, _): # pylint: disable=no-self-use + with patch("sys.argv", ["instrument", "2"]): + auto_instrumentation.run() + self.assertIsNone(environ.get("OTEL_EXPORTER")) + + with patch("sys.argv", ["instrument", "-e", "zipkin", "1", "2"]): + auto_instrumentation.run() + self.assertEqual(environ.get("OTEL_EXPORTER"), "zipkin") + + @patch("opentelemetry.instrumentation.auto_instrumentation.execl") + def test_service_name(self, _): # 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")