Skip to content

Commit 9e2dbec

Browse files
authored
Adding multiple db connections support for django-instrumentation's sqlcommenter (#1187)
1 parent 2eb86e0 commit 9e2dbec

File tree

5 files changed

+62
-55
lines changed

5 files changed

+62
-55
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc2-0.32b0...HEAD)
9+
- Adding multiple db connections support for django-instrumentation's sqlcommenter
10+
([#1187](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1187))
911

1012
### Added
1113
- `opentelemetry-instrumentation-redis` add support to instrument RedisCluster clients

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

+14-46
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,19 @@
1313
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
16+
from contextlib import ExitStack
1617
from logging import getLogger
1718
from typing import Any, Type, TypeVar
18-
from urllib.parse import quote as urllib_quote
1919

2020
# pylint: disable=no-name-in-module
2121
from django import conf, get_version
22-
from django.db import connection
22+
from django.db import connections
2323
from django.db.backends.utils import CursorDebugWrapper
2424

25+
from opentelemetry.instrumentation.utils import (
26+
_generate_sql_comment,
27+
_get_opentelemetry_values,
28+
)
2529
from opentelemetry.trace.propagation.tracecontext import (
2630
TraceContextTextMapPropagator,
2731
)
@@ -44,7 +48,13 @@ def __init__(self, get_response) -> None:
4448
self.get_response = get_response
4549

4650
def __call__(self, request) -> Any:
47-
with connection.execute_wrapper(_QueryWrapper(request)):
51+
with ExitStack() as stack:
52+
for db_alias in connections:
53+
stack.enter_context(
54+
connections[db_alias].execute_wrapper(
55+
_QueryWrapper(request)
56+
)
57+
)
4858
return self.get_response(request)
4959

5060

@@ -105,49 +115,7 @@ def __call__(self, execute: Type[T], sql, params, many, context) -> T:
105115
sql += sql_comment
106116

107117
# Add the query to the query log if debugging.
108-
if context["cursor"].__class__ is CursorDebugWrapper:
118+
if isinstance(context["cursor"], CursorDebugWrapper):
109119
context["connection"].queries_log.append(sql)
110120

111121
return execute(sql, params, many, context)
112-
113-
114-
def _generate_sql_comment(**meta) -> str:
115-
"""
116-
Return a SQL comment with comma delimited key=value pairs created from
117-
**meta kwargs.
118-
"""
119-
key_value_delimiter = ","
120-
121-
if not meta: # No entries added.
122-
return ""
123-
124-
# Sort the keywords to ensure that caching works and that testing is
125-
# deterministic. It eases visual inspection as well.
126-
return (
127-
" /*"
128-
+ key_value_delimiter.join(
129-
f"{_url_quote(key)}={_url_quote(value)!r}"
130-
for key, value in sorted(meta.items())
131-
if value is not None
132-
)
133-
+ "*/"
134-
)
135-
136-
137-
def _url_quote(value) -> str:
138-
if not isinstance(value, (str, bytes)):
139-
return value
140-
_quoted = urllib_quote(value)
141-
# Since SQL uses '%' as a keyword, '%' is a by-product of url quoting
142-
# e.g. foo,bar --> foo%2Cbar
143-
# thus in our quoting, we need to escape it too to finally give
144-
# foo,bar --> foo%%2Cbar
145-
return _quoted.replace("%", "%%")
146-
147-
148-
def _get_opentelemetry_values() -> dict or None:
149-
"""
150-
Return the OpenTelemetry Trace and Span IDs if Span ID is set in the
151-
OpenTelemetry execution context.
152-
"""
153-
return _propagator.inject({})

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,13 @@
8686
class TestMiddleware(WsgiTestBase):
8787
@classmethod
8888
def setUpClass(cls):
89-
conf.settings.configure(ROOT_URLCONF=modules[__name__])
89+
conf.settings.configure(
90+
ROOT_URLCONF=modules[__name__],
91+
DATABASES={
92+
"default": {},
93+
"other": {},
94+
}, # db.connections gets populated only at first test execution
95+
)
9096
super().setUpClass()
9197

9298
def setUp(self):

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

+18-1
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@
1313
# limitations under the License.
1414

1515
# pylint: disable=no-name-in-module
16-
1716
from unittest.mock import MagicMock, patch
1817

18+
import pytest
1919
from django import VERSION, conf
2020
from django.http import HttpResponse
2121
from django.test.utils import setup_test_environment, teardown_test_environment
2222

2323
from opentelemetry.instrumentation.django import DjangoInstrumentor
2424
from opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware import (
25+
SqlCommenter,
2526
_QueryWrapper,
2627
)
2728
from opentelemetry.test.wsgitestutil import WsgiTestBase
@@ -98,3 +99,19 @@ def test_query_wrapper(self, trace_capture):
9899
"Select 1 /*app_name='app',controller='view',route='route',traceparent='%%2Atraceparent%%3D%%2700-0000000"
99100
"00000000000000000deadbeef-000000000000beef-00'*/",
100101
)
102+
103+
@patch(
104+
"opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware._QueryWrapper"
105+
)
106+
def test_multiple_connection_support(self, query_wrapper):
107+
if not DJANGO_2_0:
108+
pytest.skip()
109+
110+
requests_mock = MagicMock()
111+
get_response = MagicMock()
112+
113+
sql_instance = SqlCommenter(get_response)
114+
sql_instance(requests_mock)
115+
116+
# check if query_wrapper is added to the context for 2 databases
117+
self.assertEqual(query_wrapper.call_count, 2)

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

+21-7
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY # noqa: F401
2626
from opentelemetry.propagate import extract
2727
from opentelemetry.trace import Span, StatusCode
28+
from opentelemetry.trace.propagation.tracecontext import (
29+
TraceContextTextMapPropagator,
30+
)
31+
32+
propagator = TraceContextTextMapPropagator()
2833

2934

3035
def extract_attributes_from_object(
@@ -119,24 +124,22 @@ def _start_internal_or_server_span(
119124
return span, token
120125

121126

122-
_KEY_VALUE_DELIMITER = ","
123-
124-
125-
def _generate_sql_comment(**meta):
127+
def _generate_sql_comment(**meta) -> str:
126128
"""
127129
Return a SQL comment with comma delimited key=value pairs created from
128130
**meta kwargs.
129131
"""
132+
key_value_delimiter = ","
133+
130134
if not meta: # No entries added.
131135
return ""
132136

133137
# Sort the keywords to ensure that caching works and that testing is
134138
# deterministic. It eases visual inspection as well.
135-
# pylint: disable=consider-using-f-string
136139
return (
137140
" /*"
138-
+ _KEY_VALUE_DELIMITER.join(
139-
"{}={!r}".format(_url_quote(key), _url_quote(value))
141+
+ key_value_delimiter.join(
142+
f"{_url_quote(key)}={_url_quote(value)!r}"
140143
for key, value in sorted(meta.items())
141144
if value is not None
142145
)
@@ -155,6 +158,17 @@ def _url_quote(s): # pylint: disable=invalid-name
155158
return quoted.replace("%", "%%")
156159

157160

161+
def _get_opentelemetry_values():
162+
"""
163+
Return the OpenTelemetry Trace and Span IDs if Span ID is set in the
164+
OpenTelemetry execution context.
165+
"""
166+
# Insert the W3C TraceContext generated
167+
_headers = {}
168+
propagator.inject(_headers)
169+
return _headers
170+
171+
158172
def _generate_opentelemetry_traceparent(span: Span) -> str:
159173
meta = {}
160174
_version = "00"

0 commit comments

Comments
 (0)