diff --git a/docs/examples/auto-instrumentation/README.md b/docs/examples/auto-instrumentation/README.md index 46b0b44b2c8..7ed40f6b96e 100644 --- a/docs/examples/auto-instrumentation/README.md +++ b/docs/examples/auto-instrumentation/README.md @@ -55,11 +55,9 @@ $ source auto_instrumentation/bin/activate # Installation ```sh -$ pip install opentelemetry-api $ pip install opentelemetry-sdk $ pip install opentelemetry-auto-instrumentation -$ pip install ext/opentelemetry-ext-flask -$ pip install flask +$ pip install opentelemetry-ext-flask $ pip install requests ``` @@ -71,20 +69,46 @@ This is done in 2 separate consoles, one to run each of the scripts that make up ```sh $ source auto_instrumentation/bin/activate -$ python opentelemetry-python/opentelemetry-auto-instrumentation/example/server_instrumented.py +$ python opentelemetry-python/docs/examples/auto-instrumentation/server_instrumented.py ``` ```sh $ source auto_instrumentation/bin/activate -$ python opentelemetry-python/opentelemetry-auto-instrumentation/example/client.py testing +$ python opentelemetry-python/docs/examples/auto-instrumentation/client.py testing ``` The execution of `server_instrumented.py` should return an output similar to: ```sh -Hello, testing! -Span(name="serv_request", context=SpanContext(trace_id=0x9c0e0ce8f7b7dbb51d1d6e744a4dad49, span_id=0xd1ba3ec4c76a0d7f, trace_state={}), kind=SpanKind.INTERNAL, parent=None, start_time=2020-03-19T00:06:31.275719Z, end_time=2020-03-19T00:06:31.275920Z) -127.0.0.1 - - [18/Mar/2020 18:06:31] "GET /serv_request?helloStr=Hello%2C+testing%21 HTTP/1.1" 200 - +{ + "name": "server_request", + "context": { + "trace_id": "0xfa002aad260b5f7110db674a9ddfcd23", + "span_id": "0x8b8bbaf3ca9c5131", + "trace_state": "{}" + }, + "kind": "SpanKind.SERVER", + "parent_id": null, + "start_time": "2020-04-30T17:28:57.886397Z", + "end_time": "2020-04-30T17:28:57.886490Z", + "status": { + "canonical_code": "OK" + }, + "attributes": { + "component": "http", + "http.method": "GET", + "http.server_name": "127.0.0.1", + "http.scheme": "http", + "host.port": 8082, + "http.host": "localhost:8082", + "http.target": "/server_request?param=testing", + "net.peer.ip": "127.0.0.1", + "net.peer.port": 52872, + "http.flavor": "1.1" + }, + "events": [], + "links": [] +} ``` ## Execution of an automatically instrumented server @@ -92,21 +116,50 @@ Span(name="serv_request", context=SpanContext(trace_id=0x9c0e0ce8f7b7dbb51d1d6e7 Now, kill the execution of `server_instrumented.py` with `ctrl + c` and run this instead: ```sh -$ opentelemetry-auto-instrumentation opentelemetry-python/opentelemetry-auto-instrumentation/example/server_uninstrumented.py +$ opentelemetry-auto-instrumentation python docs/examples/auto-instrumentation/server_uninstrumented.py ``` In the console where you previously executed `client.py`, run again this again: ```sh -$ python opentelemetry-python/opentelemetry-auto-instrumentation/example/client.py testing +$ python opentelemetry-python/docs/examples/auto-instrumentation/client.py testing ``` The execution of `server_uninstrumented.py` should return an output similar to: ```sh -Hello, testing! -Span(name="serv_request", context=SpanContext(trace_id=0xf26b28b5243e48f5f96bfc753f95f3f0, span_id=0xbeb179a095d087ed, trace_state={}), kind=SpanKind.SERVER, parent=, start_time=2020-03-19T00:24:18.828561Z, end_time=2020-03-19T00:24:18.845127Z) -127.0.0.1 - - [18/Mar/2020 18:24:18] "GET /serv_request?helloStr=Hello%2C+testing%21 HTTP/1.1" 200 - +{ + "name": "server_request", + "context": { + "trace_id": "0x9f528e0b76189f539d9c21b1a7a2fc24", + "span_id": "0xd79760685cd4c269", + "trace_state": "{}" + }, + "kind": "SpanKind.SERVER", + "parent_id": "0xb4fb7eee22ef78e4", + "start_time": "2020-04-30T17:10:02.400604Z", + "end_time": "2020-04-30T17:10:02.401858Z", + "status": { + "canonical_code": "OK" + }, + "attributes": { + "component": "http", + "http.method": "GET", + "http.server_name": "127.0.0.1", + "http.scheme": "http", + "host.port": 8082, + "http.host": "localhost:8082", + "http.target": "/server_request?param=testing", + "net.peer.ip": "127.0.0.1", + "net.peer.port": 48240, + "http.flavor": "1.1", + "http.route": "/server_request", + "http.status_text": "OK", + "http.status_code": 200 + }, + "events": [], + "links": [] +} ``` -As you can see, both outputs are equivalentsince the automatic instrumentation does what the manual instrumentation does too. +Both outputs are equivalent since the automatic instrumentation does what the manual instrumentation does too. diff --git a/docs/examples/auto-instrumentation/server_instrumented.py b/docs/examples/auto-instrumentation/server_instrumented.py index 1c78aab15d8..528b107e03c 100644 --- a/docs/examples/auto-instrumentation/server_instrumented.py +++ b/docs/examples/auto-instrumentation/server_instrumented.py @@ -15,6 +15,7 @@ from flask import Flask, request from opentelemetry import propagators, trace +from opentelemetry.ext.wsgi import collect_request_attributes from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, @@ -38,6 +39,8 @@ def server_request(): parent=propagators.extract( lambda dict_, key: dict_.get(key, []), request.headers )["current-span"], + kind=trace.SpanKind.SERVER, + attributes=collect_request_attributes(request.environ), ): print(request.args.get("param")) return "served" diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py index adfb4fd461d..0d8d7dff271 100644 --- a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py @@ -20,9 +20,9 @@ :: - opentelemetry-auto-instrumentation program.py + opentelemetry-auto-instrumentation python program.py The code in ``program.py`` needs to use one of the packages for which there is -an OpenTelemetry extension. For a list of the available extensions please check -`here `_. +an OpenTelemetry integration. For a list of the available integrations please +check `here `_. """ diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py index 00ccf6a0ea9..893b8939b93 100644 --- a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py @@ -15,22 +15,40 @@ # limitations under the License. from logging import getLogger -from runpy import run_path +from os import environ, execl, getcwd +from os.path import abspath, dirname, pathsep +from shutil import which from sys import argv -from pkg_resources import iter_entry_points - logger = getLogger(__file__) def run() -> None: - for entry_point in iter_entry_points("opentelemetry_instrumentor"): - try: - entry_point.load()().instrument() # type: ignore - logger.debug("Instrumented %s", entry_point.name) + python_path = environ.get("PYTHONPATH") + + if not python_path: + python_path = [] + + else: + python_path = python_path.split(pathsep) + + cwd_path = getcwd() + + # This is being added to support applications that are being run from their + # own executable, like Django. + # FIXME investigate if there is another way to achieve this + if cwd_path not in python_path: + python_path.insert(0, cwd_path) + + filedir_path = dirname(abspath(__file__)) + + python_path = [path for path in python_path if path != filedir_path] + + python_path.insert(0, filedir_path) + + environ["PYTHONPATH"] = pathsep.join(python_path) - except Exception: # pylint: disable=broad-except - logger.exception("Instrumenting of %s failed", entry_point.name) + executable = which(argv[1]) - run_path(argv[1], run_name="__main__") # type: ignore + execl(executable, executable, *argv[2:]) diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/sitecustomize.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/sitecustomize.py new file mode 100644 index 00000000000..b070bf5d773 --- /dev/null +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/sitecustomize.py @@ -0,0 +1,28 @@ +# 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 pkg_resources import iter_entry_points + +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) + + except Exception: # pylint: disable=broad-except + logger.exception("Instrumenting of %s failed", entry_point.name) diff --git a/opentelemetry-auto-instrumentation/tests/test_run.py b/opentelemetry-auto-instrumentation/tests/test_run.py new file mode 100644 index 00000000000..8b37882f5b9 --- /dev/null +++ b/opentelemetry-auto-instrumentation/tests/test_run.py @@ -0,0 +1,106 @@ +# 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, getcwd +from os.path import abspath, dirname, pathsep +from unittest import TestCase +from unittest.mock import patch + +from opentelemetry.auto_instrumentation import auto_instrumentation + + +class TestRun(TestCase): + auto_instrumentation_path = dirname(abspath(auto_instrumentation.__file__)) + + @classmethod + def setUpClass(cls): + cls.argv_patcher = patch( + "opentelemetry.auto_instrumentation.auto_instrumentation.argv" + ) + cls.execl_patcher = patch( + "opentelemetry.auto_instrumentation.auto_instrumentation.execl" + ) + cls.which_patcher = patch( + "opentelemetry.auto_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.dict("os.environ", {"PYTHONPATH": ""}) + def test_empty(self): + auto_instrumentation.run() + self.assertEqual( + environ["PYTHONPATH"], + pathsep.join([self.auto_instrumentation_path, getcwd()]), + ) + + @patch.dict("os.environ", {"PYTHONPATH": "abc"}) + def test_non_empty(self): + auto_instrumentation.run() + self.assertEqual( + environ["PYTHONPATH"], + pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), + ) + + @patch.dict( + "os.environ", + {"PYTHONPATH": pathsep.join(["abc", auto_instrumentation_path])}, + ) + def test_after_path(self): + auto_instrumentation.run() + self.assertEqual( + environ["PYTHONPATH"], + pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), + ) + + @patch.dict( + "os.environ", + { + "PYTHONPATH": pathsep.join( + [auto_instrumentation_path, "abc", auto_instrumentation_path] + ) + }, + ) + def test_single_path(self): + auto_instrumentation.run() + self.assertEqual( + environ["PYTHONPATH"], + pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), + ) + + +class TestExecl(TestCase): + @patch( + "opentelemetry.auto_instrumentation.auto_instrumentation.argv", + new=[1, 2, 3], + ) + @patch("opentelemetry.auto_instrumentation.auto_instrumentation.which") + @patch("opentelemetry.auto_instrumentation.auto_instrumentation.execl") + def test_execl( + self, mock_execl, mock_which + ): # pylint: disable=no-self-use + mock_which.configure_mock(**{"return_value": "python"}) + + auto_instrumentation.run() + + mock_execl.assert_called_with("python", "python", 3)