click: ignore click based servers (#3174)

* click: ignore click based servers

We don't want to create a root span for long running processes like servers
otherwise all requests would have the same trace id which is unfortunate.
---------

Co-authored-by: Tammy Baylis <96076570+tammy-baylis-swi@users.noreply.github.com>
This commit is contained in:
Riccardo Magliocchetti
2025-01-09 11:29:31 +01:00
committed by GitHub
parent 147e3f754e
commit 908437db5d
4 changed files with 68 additions and 2 deletions

View File

@ -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))

View File

@ -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 @@ from typing import Collection
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 @@ from opentelemetry.trace.status import StatusCode
_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,

View File

@ -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

View File

@ -16,8 +16,14 @@ import os
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 @@ class ClickTestCase(TestBase):
)
@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 @@ class ClickTestCase(TestBase):
},
)
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()