mirror of
https://github.com/open-telemetry/opentelemetry-python-contrib.git
synced 2025-07-29 13:12:39 +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).
|
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
|
||||||
|
@ -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({})
|
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
Reference in New Issue
Block a user