diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d63d0395..8c88a0243d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-httpx` Fix `RequestInfo`/`ResponseInfo` type hints ([#3105](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3105)) +- `opentelemetry-instrumentation-click` Disable tracing of well-known server click commands + ([#3174](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3174)) - `opentelemetry-instrumentation` Fix `get_dist_dependency_conflicts` if no distribution requires ([#3168](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3168)) diff --git a/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/__init__.py b/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/__init__.py index 8222bfdf5a..e820ca7d87 100644 --- a/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/__init__.py @@ -13,7 +13,9 @@ # limitations under the License. """ -Instrument `click`_ CLI applications. +Instrument `click`_ CLI applications. The instrumentor will avoid instrumenting +well-known servers (e.g. *flask run* and *uvicorn*) to avoid unexpected effects +like every request having the same Trace ID. .. _click: https://pypi.org/project/click/ @@ -47,6 +49,12 @@ def hello(): import click from wrapt import wrap_function_wrapper +try: + from flask.cli import ScriptInfo as FlaskScriptInfo +except ImportError: + FlaskScriptInfo = None + + from opentelemetry import trace from opentelemetry.instrumentation.click.package import _instruments from opentelemetry.instrumentation.click.version import __version__ @@ -66,6 +74,20 @@ def hello(): _logger = getLogger(__name__) +def _skip_servers(ctx: click.Context): + # flask run + if ( + ctx.info_name == "run" + and FlaskScriptInfo + and isinstance(ctx.obj, FlaskScriptInfo) + ): + return True + # uvicorn + if ctx.info_name == "uvicorn": + return True + return False + + def _command_invoke_wrapper(wrapped, instance, args, kwargs, tracer): # Subclasses of Command include groups and CLI runners, but # we only want to instrument the actual commands which are @@ -74,6 +96,12 @@ def _command_invoke_wrapper(wrapped, instance, args, kwargs, tracer): return wrapped(*args, **kwargs) ctx = args[0] + + # we don't want to create a root span for long running processes like servers + # otherwise all requests would have the same trace id + if _skip_servers(ctx): + return wrapped(*args, **kwargs) + span_name = ctx.info_name span_attributes = { PROCESS_COMMAND_ARGS: sys.argv, diff --git a/instrumentation/opentelemetry-instrumentation-click/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-click/test-requirements.txt index 6e9162ccde..e07bd05bb7 100644 --- a/instrumentation/opentelemetry-instrumentation-click/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-click/test-requirements.txt @@ -1,7 +1,12 @@ asgiref==3.8.1 +blinker==1.7.0 click==8.1.7 Deprecated==1.2.14 +Flask==3.0.2 iniconfig==2.0.0 +itsdangerous==2.1.2 +Jinja2==3.1.4 +MarkupSafe==2.1.2 packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 @@ -9,7 +14,11 @@ pytest==7.4.4 pytest-asyncio==0.23.5 tomli==2.0.1 typing_extensions==4.12.2 +Werkzeug==3.0.6 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-click +-e instrumentation/opentelemetry-instrumentation-flask +-e instrumentation/opentelemetry-instrumentation-wsgi +-e util/opentelemetry-util-http diff --git a/instrumentation/opentelemetry-instrumentation-click/tests/test_click.py b/instrumentation/opentelemetry-instrumentation-click/tests/test_click.py index 41d01a5bb4..1b07f6ab56 100644 --- a/instrumentation/opentelemetry-instrumentation-click/tests/test_click.py +++ b/instrumentation/opentelemetry-instrumentation-click/tests/test_click.py @@ -16,8 +16,14 @@ from unittest import mock import click +import pytest from click.testing import CliRunner +try: + from flask import cli as flask_cli +except ImportError: + flask_cli = None + from opentelemetry.instrumentation.click import ClickInstrumentor from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind @@ -60,7 +66,7 @@ def command(): ) @mock.patch("sys.argv", ["flask", "command"]) - def test_flask_run_command_wrapping(self): + def test_flask_command_wrapping(self): @click.command() def command(): pass @@ -162,6 +168,27 @@ def command_raises(): }, ) + def test_uvicorn_cli_command_ignored(self): + @click.command("uvicorn") + def command_uvicorn(): + pass + + runner = CliRunner() + result = runner.invoke(command_uvicorn) + self.assertEqual(result.exit_code, 0) + + self.assertFalse(self.memory_exporter.get_finished_spans()) + + @pytest.mark.skipif(flask_cli is None, reason="requires flask") + def test_flask_run_command_ignored(self): + runner = CliRunner() + result = runner.invoke( + flask_cli.run_command, obj=flask_cli.ScriptInfo() + ) + self.assertEqual(result.exit_code, 2) + + self.assertFalse(self.memory_exporter.get_finished_spans()) + def test_uninstrument(self): ClickInstrumentor().uninstrument()