mirror of
https://github.com/open-telemetry/opentelemetry-python-contrib.git
synced 2025-07-28 20:52:57 +08:00
Adding multiple db connections support for django-instrumentation's sqlcommenter (#1187)
This commit is contained in:
@ -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).
|
||||
|
||||
## [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
|
||||
- `opentelemetry-instrumentation-redis` add support to instrument RedisCluster clients
|
||||
|
@ -13,15 +13,19 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from contextlib import ExitStack
|
||||
from logging import getLogger
|
||||
from typing import Any, Type, TypeVar
|
||||
from urllib.parse import quote as urllib_quote
|
||||
|
||||
# pylint: disable=no-name-in-module
|
||||
from django import conf, get_version
|
||||
from django.db import connection
|
||||
from django.db import connections
|
||||
from django.db.backends.utils import CursorDebugWrapper
|
||||
|
||||
from opentelemetry.instrumentation.utils import (
|
||||
_generate_sql_comment,
|
||||
_get_opentelemetry_values,
|
||||
)
|
||||
from opentelemetry.trace.propagation.tracecontext import (
|
||||
TraceContextTextMapPropagator,
|
||||
)
|
||||
@ -44,7 +48,13 @@ class SqlCommenter:
|
||||
self.get_response = get_response
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@ -105,49 +115,7 @@ class _QueryWrapper:
|
||||
sql += sql_comment
|
||||
|
||||
# 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)
|
||||
|
||||
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({})
|
||||
|
@ -86,7 +86,13 @@ _django_instrumentor = DjangoInstrumentor()
|
||||
class TestMiddleware(WsgiTestBase):
|
||||
@classmethod
|
||||
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()
|
||||
|
||||
def setUp(self):
|
||||
|
@ -13,15 +13,16 @@
|
||||
# limitations under the License.
|
||||
|
||||
# pylint: disable=no-name-in-module
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from django import VERSION, conf
|
||||
from django.http import HttpResponse
|
||||
from django.test.utils import setup_test_environment, teardown_test_environment
|
||||
|
||||
from opentelemetry.instrumentation.django import DjangoInstrumentor
|
||||
from opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware import (
|
||||
SqlCommenter,
|
||||
_QueryWrapper,
|
||||
)
|
||||
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"
|
||||
"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)
|
||||
|
@ -25,6 +25,11 @@ from opentelemetry import context, trace
|
||||
from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY # noqa: F401
|
||||
from opentelemetry.propagate import extract
|
||||
from opentelemetry.trace import Span, StatusCode
|
||||
from opentelemetry.trace.propagation.tracecontext import (
|
||||
TraceContextTextMapPropagator,
|
||||
)
|
||||
|
||||
propagator = TraceContextTextMapPropagator()
|
||||
|
||||
|
||||
def extract_attributes_from_object(
|
||||
@ -119,24 +124,22 @@ def _start_internal_or_server_span(
|
||||
return span, token
|
||||
|
||||
|
||||
_KEY_VALUE_DELIMITER = ","
|
||||
|
||||
|
||||
def _generate_sql_comment(**meta):
|
||||
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.
|
||||
# pylint: disable=consider-using-f-string
|
||||
return (
|
||||
" /*"
|
||||
+ _KEY_VALUE_DELIMITER.join(
|
||||
"{}={!r}".format(_url_quote(key), _url_quote(value))
|
||||
+ key_value_delimiter.join(
|
||||
f"{_url_quote(key)}={_url_quote(value)!r}"
|
||||
for key, value in sorted(meta.items())
|
||||
if value is not None
|
||||
)
|
||||
@ -155,6 +158,17 @@ def _url_quote(s): # pylint: disable=invalid-name
|
||||
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:
|
||||
meta = {}
|
||||
_version = "00"
|
||||
|
Reference in New Issue
Block a user