Adding multiple db connections support for django-instrumentation's sqlcommenter (#1187)

This commit is contained in:
Thiyagu55
2022-07-14 15:59:34 +05:30
committed by GitHub
parent 2eb86e06f9
commit 9e2dbecedc
5 changed files with 62 additions and 55 deletions

View File

@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc2-0.32b0...HEAD) ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc2-0.32b0...HEAD)
- Adding multiple db connections support for django-instrumentation's sqlcommenter
([#1187](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1187))
### Added ### Added
- `opentelemetry-instrumentation-redis` add support to instrument RedisCluster clients - `opentelemetry-instrumentation-redis` add support to instrument RedisCluster clients

View File

@ -13,15 +13,19 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from contextlib import ExitStack
from logging import getLogger from logging import getLogger
from typing import Any, Type, TypeVar from typing import Any, Type, TypeVar
from urllib.parse import quote as urllib_quote
# pylint: disable=no-name-in-module # pylint: disable=no-name-in-module
from django import conf, get_version from django import conf, get_version
from django.db import connection from django.db import connections
from django.db.backends.utils import CursorDebugWrapper from django.db.backends.utils import CursorDebugWrapper
from opentelemetry.instrumentation.utils import (
_generate_sql_comment,
_get_opentelemetry_values,
)
from opentelemetry.trace.propagation.tracecontext import ( from opentelemetry.trace.propagation.tracecontext import (
TraceContextTextMapPropagator, TraceContextTextMapPropagator,
) )
@ -44,7 +48,13 @@ class SqlCommenter:
self.get_response = get_response self.get_response = get_response
def __call__(self, request) -> Any: def __call__(self, request) -> Any:
with connection.execute_wrapper(_QueryWrapper(request)): with ExitStack() as stack:
for db_alias in connections:
stack.enter_context(
connections[db_alias].execute_wrapper(
_QueryWrapper(request)
)
)
return self.get_response(request) return self.get_response(request)
@ -105,49 +115,7 @@ class _QueryWrapper:
sql += sql_comment sql += sql_comment
# Add the query to the query log if debugging. # Add the query to the query log if debugging.
if context["cursor"].__class__ is CursorDebugWrapper: if isinstance(context["cursor"], CursorDebugWrapper):
context["connection"].queries_log.append(sql) context["connection"].queries_log.append(sql)
return execute(sql, params, many, context) return execute(sql, params, many, context)
def _generate_sql_comment(**meta) -> str:
"""
Return a SQL comment with comma delimited key=value pairs created from
**meta kwargs.
"""
key_value_delimiter = ","
if not meta: # No entries added.
return ""
# Sort the keywords to ensure that caching works and that testing is
# deterministic. It eases visual inspection as well.
return (
" /*"
+ key_value_delimiter.join(
f"{_url_quote(key)}={_url_quote(value)!r}"
for key, value in sorted(meta.items())
if value is not None
)
+ "*/"
)
def _url_quote(value) -> str:
if not isinstance(value, (str, bytes)):
return value
_quoted = urllib_quote(value)
# Since SQL uses '%' as a keyword, '%' is a by-product of url quoting
# e.g. foo,bar --> foo%2Cbar
# thus in our quoting, we need to escape it too to finally give
# foo,bar --> foo%%2Cbar
return _quoted.replace("%", "%%")
def _get_opentelemetry_values() -> dict or None:
"""
Return the OpenTelemetry Trace and Span IDs if Span ID is set in the
OpenTelemetry execution context.
"""
return _propagator.inject({})

View File

@ -86,7 +86,13 @@ _django_instrumentor = DjangoInstrumentor()
class TestMiddleware(WsgiTestBase): class TestMiddleware(WsgiTestBase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
conf.settings.configure(ROOT_URLCONF=modules[__name__]) conf.settings.configure(
ROOT_URLCONF=modules[__name__],
DATABASES={
"default": {},
"other": {},
}, # db.connections gets populated only at first test execution
)
super().setUpClass() super().setUpClass()
def setUp(self): def setUp(self):

View File

@ -13,15 +13,16 @@
# limitations under the License. # limitations under the License.
# pylint: disable=no-name-in-module # pylint: disable=no-name-in-module
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest
from django import VERSION, conf from django import VERSION, conf
from django.http import HttpResponse from django.http import HttpResponse
from django.test.utils import setup_test_environment, teardown_test_environment from django.test.utils import setup_test_environment, teardown_test_environment
from opentelemetry.instrumentation.django import DjangoInstrumentor from opentelemetry.instrumentation.django import DjangoInstrumentor
from opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware import ( from opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware import (
SqlCommenter,
_QueryWrapper, _QueryWrapper,
) )
from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.test.wsgitestutil import WsgiTestBase
@ -98,3 +99,19 @@ class TestMiddleware(WsgiTestBase):
"Select 1 /*app_name='app',controller='view',route='route',traceparent='%%2Atraceparent%%3D%%2700-0000000" "Select 1 /*app_name='app',controller='view',route='route',traceparent='%%2Atraceparent%%3D%%2700-0000000"
"00000000000000000deadbeef-000000000000beef-00'*/", "00000000000000000deadbeef-000000000000beef-00'*/",
) )
@patch(
"opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware._QueryWrapper"
)
def test_multiple_connection_support(self, query_wrapper):
if not DJANGO_2_0:
pytest.skip()
requests_mock = MagicMock()
get_response = MagicMock()
sql_instance = SqlCommenter(get_response)
sql_instance(requests_mock)
# check if query_wrapper is added to the context for 2 databases
self.assertEqual(query_wrapper.call_count, 2)

View File

@ -25,6 +25,11 @@ from opentelemetry import context, trace
from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY # noqa: F401 from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY # noqa: F401
from opentelemetry.propagate import extract from opentelemetry.propagate import extract
from opentelemetry.trace import Span, StatusCode from opentelemetry.trace import Span, StatusCode
from opentelemetry.trace.propagation.tracecontext import (
TraceContextTextMapPropagator,
)
propagator = TraceContextTextMapPropagator()
def extract_attributes_from_object( def extract_attributes_from_object(
@ -119,24 +124,22 @@ def _start_internal_or_server_span(
return span, token return span, token
_KEY_VALUE_DELIMITER = "," def _generate_sql_comment(**meta) -> str:
def _generate_sql_comment(**meta):
""" """
Return a SQL comment with comma delimited key=value pairs created from Return a SQL comment with comma delimited key=value pairs created from
**meta kwargs. **meta kwargs.
""" """
key_value_delimiter = ","
if not meta: # No entries added. if not meta: # No entries added.
return "" return ""
# Sort the keywords to ensure that caching works and that testing is # Sort the keywords to ensure that caching works and that testing is
# deterministic. It eases visual inspection as well. # deterministic. It eases visual inspection as well.
# pylint: disable=consider-using-f-string
return ( return (
" /*" " /*"
+ _KEY_VALUE_DELIMITER.join( + key_value_delimiter.join(
"{}={!r}".format(_url_quote(key), _url_quote(value)) f"{_url_quote(key)}={_url_quote(value)!r}"
for key, value in sorted(meta.items()) for key, value in sorted(meta.items())
if value is not None if value is not None
) )
@ -155,6 +158,17 @@ def _url_quote(s): # pylint: disable=invalid-name
return quoted.replace("%", "%%") return quoted.replace("%", "%%")
def _get_opentelemetry_values():
"""
Return the OpenTelemetry Trace and Span IDs if Span ID is set in the
OpenTelemetry execution context.
"""
# Insert the W3C TraceContext generated
_headers = {}
propagator.inject(_headers)
return _headers
def _generate_opentelemetry_traceparent(span: Span) -> str: def _generate_opentelemetry_traceparent(span: Span) -> str:
meta = {} meta = {}
_version = "00" _version = "00"