Skip to content

Commit b550794

Browse files
authored
Merge branch 'main' into kludex/otel-util-http-types
2 parents d3422f8 + cf6d45e commit b550794

File tree

12 files changed

+1070
-83
lines changed

12 files changed

+1070
-83
lines changed

CHANGELOG.md

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

3535
- `opentelemetry-instrumentation-httpx` Fix `RequestInfo`/`ResponseInfo` type hints
3636
([#3105](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3105))
37+
- `opentelemetry-instrumentation-click` Disable tracing of well-known server click commands
38+
([#3174](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3174))
39+
- `opentelemetry-instrumentation` Fix `get_dist_dependency_conflicts` if no distribution requires
40+
([#3168](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3168))
41+
42+
### Breaking changes
43+
44+
- `opentelemetry-instrumentation-sqlalchemy` including sqlcomment in `db.statement` span attribute value is now opt-in
45+
([#3112](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3112))
46+
47+
### Breaking changes
48+
49+
- `opentelemetry-instrumentation-dbapi` including sqlcomment in `db.statement` span attribute value is now opt-in
50+
([#3115](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3115))
3751

3852

3953
## Version 1.29.0/0.50b0 (2024-12-11)

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

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

Lines changed: 68 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def trace_integration(
7171
capture_parameters: bool = False,
7272
enable_commenter: bool = False,
7373
db_api_integration_factory=None,
74+
enable_attribute_commenter: bool = False,
7475
):
7576
"""Integrate with DB API library.
7677
https://www.python.org/dev/peps/pep-0249/
@@ -88,6 +89,7 @@ def trace_integration(
8889
enable_commenter: Flag to enable/disable sqlcommenter.
8990
db_api_integration_factory: The `DatabaseApiIntegration` to use. If none is passed the
9091
default one is used.
92+
enable_attribute_commenter: Flag to enable/disable sqlcomment inclusion in `db.statement` span attribute. Only available if enable_commenter=True.
9193
"""
9294
wrap_connect(
9395
__name__,
@@ -100,6 +102,7 @@ def trace_integration(
100102
capture_parameters=capture_parameters,
101103
enable_commenter=enable_commenter,
102104
db_api_integration_factory=db_api_integration_factory,
105+
enable_attribute_commenter=enable_attribute_commenter,
103106
)
104107

105108

@@ -115,6 +118,7 @@ def wrap_connect(
115118
enable_commenter: bool = False,
116119
db_api_integration_factory=None,
117120
commenter_options: dict = None,
121+
enable_attribute_commenter: bool = False,
118122
):
119123
"""Integrate with DB API library.
120124
https://www.python.org/dev/peps/pep-0249/
@@ -133,6 +137,7 @@ def wrap_connect(
133137
db_api_integration_factory: The `DatabaseApiIntegration` to use. If none is passed the
134138
default one is used.
135139
commenter_options: Configurations for tags to be appended at the sql query.
140+
enable_attribute_commenter: Flag to enable/disable sqlcomment inclusion in `db.statement` span attribute. Only available if enable_commenter=True.
136141
137142
"""
138143
db_api_integration_factory = (
@@ -156,6 +161,7 @@ def wrap_connect_(
156161
enable_commenter=enable_commenter,
157162
commenter_options=commenter_options,
158163
connect_module=connect_module,
164+
enable_attribute_commenter=enable_attribute_commenter,
159165
)
160166
return db_integration.wrapped_connection(wrapped, args, kwargs)
161167

@@ -191,6 +197,7 @@ def instrument_connection(
191197
enable_commenter: bool = False,
192198
commenter_options: dict = None,
193199
connect_module: typing.Callable[..., typing.Any] = None,
200+
enable_attribute_commenter: bool = False,
194201
):
195202
"""Enable instrumentation in a database connection.
196203
@@ -206,6 +213,7 @@ def instrument_connection(
206213
enable_commenter: Flag to enable/disable sqlcommenter.
207214
commenter_options: Configurations for tags to be appended at the sql query.
208215
connect_module: Module name where connect method is available.
216+
enable_attribute_commenter: Flag to enable/disable sqlcomment inclusion in `db.statement` span attribute. Only available if enable_commenter=True.
209217
210218
Returns:
211219
An instrumented connection.
@@ -224,6 +232,7 @@ def instrument_connection(
224232
enable_commenter=enable_commenter,
225233
commenter_options=commenter_options,
226234
connect_module=connect_module,
235+
enable_attribute_commenter=enable_attribute_commenter,
227236
)
228237
db_integration.get_connection_attributes(connection)
229238
return get_traced_connection_proxy(connection, db_integration)
@@ -257,6 +266,7 @@ def __init__(
257266
enable_commenter: bool = False,
258267
commenter_options: dict = None,
259268
connect_module: typing.Callable[..., typing.Any] = None,
269+
enable_attribute_commenter: bool = False,
260270
):
261271
self.connection_attributes = connection_attributes
262272
if self.connection_attributes is None:
@@ -277,6 +287,7 @@ def __init__(
277287
self.capture_parameters = capture_parameters
278288
self.enable_commenter = enable_commenter
279289
self.commenter_options = commenter_options
290+
self.enable_attribute_commenter = enable_attribute_commenter
280291
self.database_system = database_system
281292
self.connection_props = {}
282293
self.span_attributes = {}
@@ -434,9 +445,52 @@ def __init__(self, db_api_integration: DatabaseApiIntegration) -> None:
434445
if self._db_api_integration.commenter_options
435446
else {}
436447
)
448+
self._enable_attribute_commenter = (
449+
self._db_api_integration.enable_attribute_commenter
450+
)
437451
self._connect_module = self._db_api_integration.connect_module
438452
self._leading_comment_remover = re.compile(r"^/\*.*?\*/")
439453

454+
def _capture_mysql_version(self, cursor) -> None:
455+
"""Lazy capture of mysql-connector client version using cursor, if applicable"""
456+
if (
457+
self._db_api_integration.database_system == "mysql"
458+
and self._db_api_integration.connect_module.__name__
459+
== "mysql.connector"
460+
and not self._db_api_integration.commenter_data[
461+
"mysql_client_version"
462+
]
463+
):
464+
self._db_api_integration.commenter_data["mysql_client_version"] = (
465+
cursor._cnx._cmysql.get_client_info()
466+
)
467+
468+
def _get_commenter_data(self) -> dict:
469+
"""Uses DB-API integration to return commenter data for sqlcomment"""
470+
commenter_data = dict(self._db_api_integration.commenter_data)
471+
if self._commenter_options.get("opentelemetry_values", True):
472+
commenter_data.update(**_get_opentelemetry_values())
473+
return {
474+
k: v
475+
for k, v in commenter_data.items()
476+
if self._commenter_options.get(k, True)
477+
}
478+
479+
def _update_args_with_added_sql_comment(self, args, cursor) -> tuple:
480+
"""Updates args with cursor info and adds sqlcomment to query statement"""
481+
try:
482+
args_list = list(args)
483+
self._capture_mysql_version(cursor)
484+
commenter_data = self._get_commenter_data()
485+
statement = _add_sql_comment(args_list[0], **commenter_data)
486+
args_list[0] = statement
487+
args = tuple(args_list)
488+
except Exception as exc: # pylint: disable=broad-except
489+
_logger.exception(
490+
"Exception while generating sql comment: %s", exc
491+
)
492+
return args
493+
440494
def _populate_span(
441495
self,
442496
span: trace_api.Span,
@@ -497,52 +551,22 @@ def traced_execution(
497551
) as span:
498552
if span.is_recording():
499553
if args and self._commenter_enabled:
500-
try:
501-
args_list = list(args)
502-
503-
# lazy capture of mysql-connector client version using cursor
504-
if (
505-
self._db_api_integration.database_system == "mysql"
506-
and self._db_api_integration.connect_module.__name__
507-
== "mysql.connector"
508-
and not self._db_api_integration.commenter_data[
509-
"mysql_client_version"
510-
]
511-
):
512-
self._db_api_integration.commenter_data[
513-
"mysql_client_version"
514-
] = cursor._cnx._cmysql.get_client_info()
515-
516-
commenter_data = dict(
517-
self._db_api_integration.commenter_data
518-
)
519-
if self._commenter_options.get(
520-
"opentelemetry_values", True
521-
):
522-
commenter_data.update(
523-
**_get_opentelemetry_values()
524-
)
525-
526-
# Filter down to just the requested attributes.
527-
commenter_data = {
528-
k: v
529-
for k, v in commenter_data.items()
530-
if self._commenter_options.get(k, True)
531-
}
532-
statement = _add_sql_comment(
533-
args_list[0], **commenter_data
554+
if self._enable_attribute_commenter:
555+
# sqlcomment is added to executed query and db.statement span attribute
556+
args = self._update_args_with_added_sql_comment(
557+
args, cursor
534558
)
535-
536-
args_list[0] = statement
537-
args = tuple(args_list)
538-
539-
except Exception as exc: # pylint: disable=broad-except
540-
_logger.exception(
541-
"Exception while generating sql comment: %s", exc
559+
self._populate_span(span, cursor, *args)
560+
else:
561+
# sqlcomment is only added to executed query
562+
# so db.statement is set before add_sql_comment
563+
self._populate_span(span, cursor, *args)
564+
args = self._update_args_with_added_sql_comment(
565+
args, cursor
542566
)
543-
544-
self._populate_span(span, cursor, *args)
545-
567+
else:
568+
# no sqlcomment anywhere
569+
self._populate_span(span, cursor, *args)
546570
return query_method(*args, **kwargs)
547571

548572

0 commit comments

Comments
 (0)