Skip to content

Commit 7c75b38

Browse files
authored
SQLCommenter semicolon bug fix (#1200)
1 parent d75194a commit 7c75b38

File tree

11 files changed

+92
-87
lines changed

11 files changed

+92
-87
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc2-0.32b0...HEAD)
99
- Adding multiple db connections support for django-instrumentation's sqlcommenter
1010
([#1187](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1187))
11+
- SQLCommenter semicolon bug fix
12+
([#1200](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1200/files))
1113

1214
### Added
1315
- `opentelemetry-instrumentation-redis` add support to instrument RedisCluster clients

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

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@
4646
from opentelemetry import trace as trace_api
4747
from opentelemetry.instrumentation.dbapi.version import __version__
4848
from opentelemetry.instrumentation.utils import (
49-
_generate_opentelemetry_traceparent,
50-
_generate_sql_comment,
49+
_add_sql_comment,
50+
_get_opentelemetry_values,
5151
unwrap,
5252
)
5353
from opentelemetry.semconv.trace import SpanAttributes
54-
from opentelemetry.trace import Span, SpanKind, TracerProvider, get_tracer
54+
from opentelemetry.trace import SpanKind, TracerProvider, get_tracer
5555

5656
_logger = logging.getLogger(__name__)
5757

@@ -375,15 +375,6 @@ def get_statement(self, cursor, args): # pylint: disable=no-self-use
375375
return statement.decode("utf8", "replace")
376376
return statement
377377

378-
@staticmethod
379-
def _generate_comment(span: Span) -> str:
380-
span_context = span.get_span_context()
381-
meta = {}
382-
if span_context.is_valid:
383-
meta.update(_generate_opentelemetry_traceparent(span))
384-
# TODO(schekuri): revisit to enrich with info such as route, db_driver etc...
385-
return _generate_sql_comment(**meta)
386-
387378
def traced_execution(
388379
self,
389380
cursor,
@@ -405,11 +396,14 @@ def traced_execution(
405396
self._populate_span(span, cursor, *args)
406397
if args and self._commenter_enabled:
407398
try:
408-
comment = self._generate_comment(span)
409-
if isinstance(args[0], bytes):
410-
comment = comment.encode("utf8")
411399
args_list = list(args)
412-
args_list[0] += comment
400+
commenter_data = {}
401+
commenter_data.update(_get_opentelemetry_values())
402+
statement = _add_sql_comment(
403+
args_list[0], **commenter_data
404+
)
405+
406+
args_list[0] = statement
413407
args = tuple(args_list)
414408
except Exception as exc: # pylint: disable=broad-except
415409
_logger.exception(

instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,11 @@ def test_executemany_comment(self):
236236
mock_connect, {}, {}
237237
)
238238
cursor = mock_connection.cursor()
239-
cursor.executemany("Test query")
240-
spans_list = self.memory_exporter.get_finished_spans()
241-
self.assertEqual(len(spans_list), 1)
242-
span = spans_list[0]
243-
comment = dbapi.CursorTracer._generate_comment(span)
244-
self.assertIn(comment, cursor.query)
239+
cursor.executemany("Select 1;")
240+
self.assertRegex(
241+
cursor.query,
242+
r"Select 1 /\*traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
243+
)
245244

246245
def test_callproc(self):
247246
db_integration = dbapi.DatabaseApiIntegration(

instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/sqlcommenter_middleware.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from django.db.backends.utils import CursorDebugWrapper
2424

2525
from opentelemetry.instrumentation.utils import (
26-
_generate_sql_comment,
26+
_add_sql_comment,
2727
_get_opentelemetry_values,
2828
)
2929
from opentelemetry.trace.propagation.tracecontext import (
@@ -84,7 +84,8 @@ def __call__(self, execute: Type[T], sql, params, many, context) -> T:
8484
db_driver = context["connection"].settings_dict.get("ENGINE", "")
8585
resolver_match = self.request.resolver_match
8686

87-
sql_comment = _generate_sql_comment(
87+
sql = _add_sql_comment(
88+
sql,
8889
# Information about the controller.
8990
controller=resolver_match.view_name
9091
if resolver_match and with_controller
@@ -112,7 +113,6 @@ def __call__(self, execute: Type[T], sql, params, many, context) -> T:
112113
# See:
113114
# * https://github.com/basecamp/marginalia/issues/61
114115
# * https://github.com/basecamp/marginalia/pull/80
115-
sql += sql_comment
116116

117117
# Add the query to the query log if debugging.
118118
if isinstance(context["cursor"], CursorDebugWrapper):

instrumentation/opentelemetry-instrumentation-django/tests/test_sqlcommenter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def test_query_wrapper(self, trace_capture):
8888
execute_mock_obj = MagicMock()
8989
qw_instance(
9090
execute_mock_obj,
91-
"Select 1",
91+
"Select 1;",
9292
MagicMock("test"),
9393
MagicMock("test1"),
9494
MagicMock(),
@@ -97,7 +97,7 @@ def test_query_wrapper(self, trace_capture):
9797
self.assertEqual(
9898
output_sql,
9999
"Select 1 /*app_name='app',controller='view',route='route',traceparent='%%2Atraceparent%%3D%%2700-0000000"
100-
"00000000000000000deadbeef-000000000000beef-00'*/",
100+
"00000000000000000deadbeef-000000000000beef-00'*/;",
101101
)
102102

103103
@patch(

instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,10 @@
2121
)
2222
from opentelemetry.instrumentation.sqlalchemy.version import __version__
2323
from opentelemetry.instrumentation.utils import (
24-
_generate_opentelemetry_traceparent,
25-
_generate_sql_comment,
24+
_add_sql_comment,
25+
_get_opentelemetry_values,
2626
)
2727
from opentelemetry.semconv.trace import NetTransportValues, SpanAttributes
28-
from opentelemetry.trace import Span
2928
from opentelemetry.trace.status import Status, StatusCode
3029

3130

@@ -141,21 +140,15 @@ def _before_cur_exec(
141140
span.set_attribute(SpanAttributes.DB_SYSTEM, self.vendor)
142141
for key, value in attrs.items():
143142
span.set_attribute(key, value)
143+
if self.enable_commenter:
144+
commenter_data = {}
145+
commenter_data.update(_get_opentelemetry_values())
146+
statement = _add_sql_comment(statement, **commenter_data)
144147

145148
context._otel_span = span
146-
if self.enable_commenter:
147-
statement = statement + EngineTracer._generate_comment(span=span)
148149

149150
return statement, params
150151

151-
@staticmethod
152-
def _generate_comment(span: Span) -> str:
153-
span_context = span.get_span_context()
154-
meta = {}
155-
if span_context.is_valid:
156-
meta.update(_generate_opentelemetry_traceparent(span))
157-
return _generate_sql_comment(**meta)
158-
159152

160153
# pylint: disable=unused-argument
161154
def _after_cur_exec(conn, cursor, statement, params, context, executemany):

instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
import asyncio
15-
import logging
1615
from unittest import mock
1716

1817
import pytest
@@ -21,7 +20,6 @@
2120

2221
from opentelemetry import trace
2322
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
24-
from opentelemetry.instrumentation.sqlalchemy.engine import EngineTracer
2523
from opentelemetry.sdk.resources import Resource
2624
from opentelemetry.sdk.trace import TracerProvider, export
2725
from opentelemetry.test.test_base import TestBase
@@ -217,22 +215,3 @@ async def run():
217215
)
218216

219217
asyncio.get_event_loop().run_until_complete(run())
220-
221-
def test_generate_commenter(self):
222-
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
223-
engine = create_engine("sqlite:///:memory:")
224-
SQLAlchemyInstrumentor().instrument(
225-
engine=engine,
226-
tracer_provider=self.tracer_provider,
227-
enable_commenter=True,
228-
)
229-
230-
cnx = engine.connect()
231-
cnx.execute("SELECT 1 + 1;").fetchall()
232-
spans = self.memory_exporter.get_finished_spans()
233-
self.assertEqual(len(spans), 2)
234-
span = spans[1]
235-
self.assertIn(
236-
EngineTracer._generate_comment(span),
237-
self.caplog.records[-2].getMessage(),
238-
)

instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlcommenter.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
import logging
1415

1516
import pytest
1617
from sqlalchemy import create_engine
@@ -28,6 +29,17 @@ def tearDown(self):
2829
super().tearDown()
2930
SQLAlchemyInstrumentor().uninstrument()
3031

32+
def test_sqlcommenter_disabled(self):
33+
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
34+
engine = create_engine("sqlite:///:memory:", echo=True)
35+
SQLAlchemyInstrumentor().instrument(
36+
engine=engine, tracer_provider=self.tracer_provider
37+
)
38+
cnx = engine.connect()
39+
cnx.execute("SELECT 1;").fetchall()
40+
41+
self.assertEqual(self.caplog.records[-2].getMessage(), "SELECT 1;")
42+
3143
def test_sqlcommenter_enabled(self):
3244
engine = create_engine("sqlite:///:memory:")
3345
SQLAlchemyInstrumentor().instrument(
@@ -39,15 +51,5 @@ def test_sqlcommenter_enabled(self):
3951
cnx.execute("SELECT 1;").fetchall()
4052
self.assertRegex(
4153
self.caplog.records[-2].getMessage(),
42-
r"SELECT 1; /\*traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/",
54+
r"SELECT 1 /\*traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
4355
)
44-
45-
def test_sqlcommenter_disabled(self):
46-
engine = create_engine("sqlite:///:memory:", echo=True)
47-
SQLAlchemyInstrumentor().instrument(
48-
engine=engine, tracer_provider=self.tracer_provider
49-
)
50-
cnx = engine.connect()
51-
cnx.execute("SELECT 1;").fetchall()
52-
53-
self.assertEqual(self.caplog.records[-2].getMessage(), "SELECT 1;")

opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
# pylint: disable=E0611
2525
from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY # noqa: F401
2626
from opentelemetry.propagate import extract
27-
from opentelemetry.trace import Span, StatusCode
27+
from opentelemetry.trace import StatusCode
2828
from opentelemetry.trace.propagation.tracecontext import (
2929
TraceContextTextMapPropagator,
3030
)
@@ -147,7 +147,7 @@ def _generate_sql_comment(**meta) -> str:
147147
)
148148

149149

150-
def _url_quote(s): # pylint: disable=invalid-name
150+
def _url_quote(s) -> str: # pylint: disable=invalid-name
151151
if not isinstance(s, (str, bytes)):
152152
return s
153153
quoted = urllib.parse.quote(s)
@@ -158,7 +158,7 @@ def _url_quote(s): # pylint: disable=invalid-name
158158
return quoted.replace("%", "%%")
159159

160160

161-
def _get_opentelemetry_values():
161+
def _get_opentelemetry_values() -> dict:
162162
"""
163163
Return the OpenTelemetry Trace and Span IDs if Span ID is set in the
164164
OpenTelemetry execution context.
@@ -169,20 +169,22 @@ def _get_opentelemetry_values():
169169
return _headers
170170

171171

172-
def _generate_opentelemetry_traceparent(span: Span) -> str:
173-
meta = {}
174-
_version = "00"
175-
_span_id = trace.format_span_id(span.context.span_id)
176-
_trace_id = trace.format_trace_id(span.context.trace_id)
177-
_flags = str(trace.TraceFlags.SAMPLED)
178-
_traceparent = _version + "-" + _trace_id + "-" + _span_id + "-" + _flags
179-
meta.update({"traceparent": _traceparent})
180-
return meta
181-
182-
183172
def _python_path_without_directory(python_path, directory, path_separator):
184173
return sub(
185174
rf"{escape(directory)}{path_separator}(?!$)",
186175
"",
187176
python_path,
188177
)
178+
179+
180+
def _add_sql_comment(sql, **meta) -> str:
181+
"""
182+
Appends comments to the sql statement and returns it
183+
"""
184+
comment = _generate_sql_comment(**meta)
185+
sql = sql.rstrip()
186+
if sql[-1] == ";":
187+
sql = sql[:-1] + comment + ";"
188+
else:
189+
sql = sql + comment
190+
return sql

opentelemetry-instrumentation/tests/test_utils.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from http import HTTPStatus
1717

1818
from opentelemetry.instrumentation.utils import (
19+
_add_sql_comment,
1920
_python_path_without_directory,
2021
http_status_to_status_code,
2122
)
@@ -152,3 +153,36 @@ def test_remove_current_directory_from_python_path_linux_only_path(self):
152153
python_path, directory, path_separator
153154
)
154155
self.assertEqual(actual_python_path, python_path)
156+
157+
def test_add_sql_comments_with_semicolon(self):
158+
sql_query_without_semicolon = "Select 1;"
159+
comments = {"comment_1": "value 1", "comment 2": "value 3"}
160+
commented_sql_without_semicolon = _add_sql_comment(
161+
sql_query_without_semicolon, **comments
162+
)
163+
164+
self.assertEqual(
165+
commented_sql_without_semicolon,
166+
"Select 1 /*comment%%202='value%%203',comment_1='value%%201'*/;",
167+
)
168+
169+
def test_add_sql_comments_without_semicolon(self):
170+
sql_query_without_semicolon = "Select 1"
171+
comments = {"comment_1": "value 1", "comment 2": "value 3"}
172+
commented_sql_without_semicolon = _add_sql_comment(
173+
sql_query_without_semicolon, **comments
174+
)
175+
176+
self.assertEqual(
177+
commented_sql_without_semicolon,
178+
"Select 1 /*comment%%202='value%%203',comment_1='value%%201'*/",
179+
)
180+
181+
def test_add_sql_comments_without_comments(self):
182+
sql_query_without_semicolon = "Select 1"
183+
comments = {}
184+
commented_sql_without_semicolon = _add_sql_comment(
185+
sql_query_without_semicolon, **comments
186+
)
187+
188+
self.assertEqual(commented_sql_without_semicolon, "Select 1")

0 commit comments

Comments
 (0)