Skip to content

Commit 685fb89

Browse files
click: ignore click based servers (open-telemetry#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 <[email protected]>
1 parent f38c1ce commit 685fb89

File tree

4 files changed

+68
-2
lines changed

4 files changed

+68
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3232

3333
- `opentelemetry-instrumentation-httpx` Fix `RequestInfo`/`ResponseInfo` type hints
3434
([#3105](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3105))
35+
- `opentelemetry-instrumentation-click` Disable tracing of well-known server click commands
36+
([#3174](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3174))
3537
- `opentelemetry-instrumentation` Fix `get_dist_dependency_conflicts` if no distribution requires
3638
([#3168](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3168))
3739

instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/__init__.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
# limitations under the License.
1414

1515
"""
16-
Instrument `click`_ CLI applications.
16+
Instrument `click`_ CLI applications. The instrumentor will avoid instrumenting
17+
well-known servers (e.g. *flask run* and *uvicorn*) to avoid unexpected effects
18+
like every request having the same Trace ID.
1719
1820
.. _click: https://pypi.org/project/click/
1921
@@ -47,6 +49,12 @@ def hello():
4749
import click
4850
from wrapt import wrap_function_wrapper
4951

52+
try:
53+
from flask.cli import ScriptInfo as FlaskScriptInfo
54+
except ImportError:
55+
FlaskScriptInfo = None
56+
57+
5058
from opentelemetry import trace
5159
from opentelemetry.instrumentation.click.package import _instruments
5260
from opentelemetry.instrumentation.click.version import __version__
@@ -66,6 +74,20 @@ def hello():
6674
_logger = getLogger(__name__)
6775

6876

77+
def _skip_servers(ctx: click.Context):
78+
# flask run
79+
if (
80+
ctx.info_name == "run"
81+
and FlaskScriptInfo
82+
and isinstance(ctx.obj, FlaskScriptInfo)
83+
):
84+
return True
85+
# uvicorn
86+
if ctx.info_name == "uvicorn":
87+
return True
88+
return False
89+
90+
6991
def _command_invoke_wrapper(wrapped, instance, args, kwargs, tracer):
7092
# Subclasses of Command include groups and CLI runners, but
7193
# we only want to instrument the actual commands which are
@@ -74,6 +96,12 @@ def _command_invoke_wrapper(wrapped, instance, args, kwargs, tracer):
7496
return wrapped(*args, **kwargs)
7597

7698
ctx = args[0]
99+
100+
# we don't want to create a root span for long running processes like servers
101+
# otherwise all requests would have the same trace id
102+
if _skip_servers(ctx):
103+
return wrapped(*args, **kwargs)
104+
77105
span_name = ctx.info_name
78106
span_attributes = {
79107
PROCESS_COMMAND_ARGS: sys.argv,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
asgiref==3.8.1
2+
blinker==1.7.0
23
click==8.1.7
34
Deprecated==1.2.14
5+
Flask==3.0.2
46
iniconfig==2.0.0
7+
itsdangerous==2.1.2
8+
Jinja2==3.1.4
9+
MarkupSafe==2.1.2
510
packaging==24.0
611
pluggy==1.5.0
712
py-cpuinfo==9.0.0
813
pytest==7.4.4
914
pytest-asyncio==0.23.5
1015
tomli==2.0.1
1116
typing_extensions==4.12.2
17+
Werkzeug==3.0.6
1218
wrapt==1.16.0
1319
zipp==3.19.2
1420
-e opentelemetry-instrumentation
1521
-e instrumentation/opentelemetry-instrumentation-click
22+
-e instrumentation/opentelemetry-instrumentation-flask
23+
-e instrumentation/opentelemetry-instrumentation-wsgi
24+
-e util/opentelemetry-util-http

instrumentation/opentelemetry-instrumentation-click/tests/test_click.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@
1616
from unittest import mock
1717

1818
import click
19+
import pytest
1920
from click.testing import CliRunner
2021

22+
try:
23+
from flask import cli as flask_cli
24+
except ImportError:
25+
flask_cli = None
26+
2127
from opentelemetry.instrumentation.click import ClickInstrumentor
2228
from opentelemetry.test.test_base import TestBase
2329
from opentelemetry.trace import SpanKind
@@ -60,7 +66,7 @@ def command():
6066
)
6167

6268
@mock.patch("sys.argv", ["flask", "command"])
63-
def test_flask_run_command_wrapping(self):
69+
def test_flask_command_wrapping(self):
6470
@click.command()
6571
def command():
6672
pass
@@ -162,6 +168,27 @@ def command_raises():
162168
},
163169
)
164170

171+
def test_uvicorn_cli_command_ignored(self):
172+
@click.command("uvicorn")
173+
def command_uvicorn():
174+
pass
175+
176+
runner = CliRunner()
177+
result = runner.invoke(command_uvicorn)
178+
self.assertEqual(result.exit_code, 0)
179+
180+
self.assertFalse(self.memory_exporter.get_finished_spans())
181+
182+
@pytest.mark.skipif(flask_cli is None, reason="requires flask")
183+
def test_flask_run_command_ignored(self):
184+
runner = CliRunner()
185+
result = runner.invoke(
186+
flask_cli.run_command, obj=flask_cli.ScriptInfo()
187+
)
188+
self.assertEqual(result.exit_code, 2)
189+
190+
self.assertFalse(self.memory_exporter.get_finished_spans())
191+
165192
def test_uninstrument(self):
166193
ClickInstrumentor().uninstrument()
167194

0 commit comments

Comments
 (0)