mirror of
https://github.com/open-telemetry/opentelemetry-python-contrib.git
synced 2025-07-28 20:52:57 +08:00
Integrating sql commenter into otel_django_instrumentation (#896)
* Integrating sql commenter into otel_django_instrumentation * Added test cases for django * - Linting changes - Added Changelog * - Linting changes * - Linting changes * - Linting changes * - Linting changes * - Linting changes * - Linting changes * - Linting changes * PR changes * PR changes * Linting changes * Linting changes * Linting changes * Linting changes * PR changes * PR changes * PR changes * linting changes * PR changes * linting changes * PR changes * PR changes * PR changes * PR changes * PR changes Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com> Co-authored-by: Diego Hurtado <ocelotl@users.noreply.github.com>
This commit is contained in:
@ -39,7 +39,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Add metric instrumentation for WSGI
|
- Add metric instrumentation for WSGI
|
||||||
([#1128](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1128))
|
([#1128](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1128))
|
||||||
- `opentelemetry-instrumentation-requests` Restoring metrics in requests
|
- `opentelemetry-instrumentation-requests` Restoring metrics in requests
|
||||||
([#1110](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1110)
|
([#1110](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1110))
|
||||||
|
- Integrated sqlcommenter plugin into opentelemetry-instrumentation-django
|
||||||
|
([#896](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/896))
|
||||||
|
|
||||||
|
|
||||||
## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17
|
## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17
|
||||||
|
@ -17,6 +17,68 @@ Instrument `django`_ to trace Django applications.
|
|||||||
|
|
||||||
.. _django: https://pypi.org/project/django/
|
.. _django: https://pypi.org/project/django/
|
||||||
|
|
||||||
|
SQLCOMMENTER
|
||||||
|
*****************************************
|
||||||
|
You can optionally configure Django instrumentation to enable sqlcommenter which enriches
|
||||||
|
the query with contextual information.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from opentelemetry.instrumentation.django import DjangoInstrumentor
|
||||||
|
|
||||||
|
DjangoInstrumentor().instrument(is_sql_commentor_enabled=True)
|
||||||
|
|
||||||
|
|
||||||
|
For example,
|
||||||
|
::
|
||||||
|
|
||||||
|
Invoking Users().objects.all() will lead to sql query "select * from auth_users" but when SQLCommenter is enabled
|
||||||
|
the query will get appended with some configurable tags like "select * from auth_users /*metrics=value*/;"
|
||||||
|
|
||||||
|
|
||||||
|
SQLCommenter Configurations
|
||||||
|
***************************
|
||||||
|
We can configure the tags to be appended to the sqlquery log by adding below variables to the settings.py
|
||||||
|
|
||||||
|
SQLCOMMENTER_WITH_FRAMEWORK = True(Default) or False
|
||||||
|
|
||||||
|
For example,
|
||||||
|
::
|
||||||
|
Enabling this flag will add django framework and it's version which is /*framework='django%3A2.2.3*/
|
||||||
|
|
||||||
|
SQLCOMMENTER_WITH_CONTROLLER = True(Default) or False
|
||||||
|
|
||||||
|
For example,
|
||||||
|
::
|
||||||
|
Enabling this flag will add controller name that handles the request /*controller='index'*/
|
||||||
|
|
||||||
|
SQLCOMMENTER_WITH_ROUTE = True(Default) or False
|
||||||
|
|
||||||
|
For example,
|
||||||
|
::
|
||||||
|
Enabling this flag will add url path that handles the request /*route='polls/'*/
|
||||||
|
|
||||||
|
SQLCOMMENTER_WITH_APP_NAME = True(Default) or False
|
||||||
|
|
||||||
|
For example,
|
||||||
|
::
|
||||||
|
Enabling this flag will add app name that handles the request /*app_name='polls'*/
|
||||||
|
|
||||||
|
SQLCOMMENTER_WITH_OPENTELEMETRY = True(Default) or False
|
||||||
|
|
||||||
|
For example,
|
||||||
|
::
|
||||||
|
Enabling this flag will add opentelemetry traceparent /*traceparent='00-fd720cffceba94bbf75940ff3caaf3cc-4fd1a2bdacf56388-01'*/
|
||||||
|
|
||||||
|
SQLCOMMENTER_WITH_DB_DRIVER = True(Default) or False
|
||||||
|
|
||||||
|
For example,
|
||||||
|
::
|
||||||
|
Enabling this flag will add name of the db driver /*db_driver='django.db.backends.postgresql'*/
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
-----
|
-----
|
||||||
|
|
||||||
@ -124,6 +186,7 @@ Example of the added span attribute,
|
|||||||
|
|
||||||
API
|
API
|
||||||
---
|
---
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
@ -136,7 +199,9 @@ from django.conf import settings
|
|||||||
from opentelemetry.instrumentation.django.environment_variables import (
|
from opentelemetry.instrumentation.django.environment_variables import (
|
||||||
OTEL_PYTHON_DJANGO_INSTRUMENT,
|
OTEL_PYTHON_DJANGO_INSTRUMENT,
|
||||||
)
|
)
|
||||||
from opentelemetry.instrumentation.django.middleware import _DjangoMiddleware
|
from opentelemetry.instrumentation.django.middleware.otel_middleware import (
|
||||||
|
_DjangoMiddleware,
|
||||||
|
)
|
||||||
from opentelemetry.instrumentation.django.package import _instruments
|
from opentelemetry.instrumentation.django.package import _instruments
|
||||||
from opentelemetry.instrumentation.django.version import __version__
|
from opentelemetry.instrumentation.django.version import __version__
|
||||||
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
||||||
@ -166,6 +231,8 @@ class DjangoInstrumentor(BaseInstrumentor):
|
|||||||
[_DjangoMiddleware.__module__, _DjangoMiddleware.__qualname__]
|
[_DjangoMiddleware.__module__, _DjangoMiddleware.__qualname__]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_sql_commenter_middleware = "opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware.SqlCommenter"
|
||||||
|
|
||||||
def instrumentation_dependencies(self) -> Collection[str]:
|
def instrumentation_dependencies(self) -> Collection[str]:
|
||||||
return _instruments
|
return _instruments
|
||||||
|
|
||||||
@ -204,7 +271,13 @@ class DjangoInstrumentor(BaseInstrumentor):
|
|||||||
if isinstance(settings_middleware, tuple):
|
if isinstance(settings_middleware, tuple):
|
||||||
settings_middleware = list(settings_middleware)
|
settings_middleware = list(settings_middleware)
|
||||||
|
|
||||||
|
is_sql_commentor_enabled = kwargs.pop("is_sql_commentor_enabled", None)
|
||||||
|
|
||||||
|
if is_sql_commentor_enabled:
|
||||||
|
settings_middleware.insert(0, self._sql_commenter_middleware)
|
||||||
|
|
||||||
settings_middleware.insert(0, self._opentelemetry_middleware)
|
settings_middleware.insert(0, self._opentelemetry_middleware)
|
||||||
|
|
||||||
setattr(settings, _middleware_setting, settings_middleware)
|
setattr(settings, _middleware_setting, settings_middleware)
|
||||||
|
|
||||||
def _uninstrument(self, **kwargs):
|
def _uninstrument(self, **kwargs):
|
||||||
|
@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
#
|
||||||
|
# Copyright The OpenTelemetry Authors
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# 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 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.backends.utils import CursorDebugWrapper
|
||||||
|
|
||||||
|
from opentelemetry.trace.propagation.tracecontext import (
|
||||||
|
TraceContextTextMapPropagator,
|
||||||
|
)
|
||||||
|
|
||||||
|
_propagator = TraceContextTextMapPropagator()
|
||||||
|
|
||||||
|
_django_version = get_version()
|
||||||
|
_logger = getLogger(__name__)
|
||||||
|
|
||||||
|
T = TypeVar("T") # pylint: disable-msg=invalid-name
|
||||||
|
|
||||||
|
|
||||||
|
class SqlCommenter:
|
||||||
|
"""
|
||||||
|
Middleware to append a comment to each database query with details about
|
||||||
|
the framework and the execution context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_response) -> None:
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request) -> Any:
|
||||||
|
with connection.execute_wrapper(_QueryWrapper(request)):
|
||||||
|
return self.get_response(request)
|
||||||
|
|
||||||
|
|
||||||
|
class _QueryWrapper:
|
||||||
|
def __init__(self, request) -> None:
|
||||||
|
self.request = request
|
||||||
|
|
||||||
|
def __call__(self, execute: Type[T], sql, params, many, context) -> T:
|
||||||
|
# pylint: disable-msg=too-many-locals
|
||||||
|
with_framework = getattr(
|
||||||
|
conf.settings, "SQLCOMMENTER_WITH_FRAMEWORK", True
|
||||||
|
)
|
||||||
|
with_controller = getattr(
|
||||||
|
conf.settings, "SQLCOMMENTER_WITH_CONTROLLER", True
|
||||||
|
)
|
||||||
|
with_route = getattr(conf.settings, "SQLCOMMENTER_WITH_ROUTE", True)
|
||||||
|
with_app_name = getattr(
|
||||||
|
conf.settings, "SQLCOMMENTER_WITH_APP_NAME", True
|
||||||
|
)
|
||||||
|
with_opentelemetry = getattr(
|
||||||
|
conf.settings, "SQLCOMMENTER_WITH_OPENTELEMETRY", True
|
||||||
|
)
|
||||||
|
with_db_driver = getattr(
|
||||||
|
conf.settings, "SQLCOMMENTER_WITH_DB_DRIVER", True
|
||||||
|
)
|
||||||
|
|
||||||
|
db_driver = context["connection"].settings_dict.get("ENGINE", "")
|
||||||
|
resolver_match = self.request.resolver_match
|
||||||
|
|
||||||
|
sql_comment = _generate_sql_comment(
|
||||||
|
# Information about the controller.
|
||||||
|
controller=resolver_match.view_name
|
||||||
|
if resolver_match and with_controller
|
||||||
|
else None,
|
||||||
|
# route is the pattern that matched a request with a controller i.e. the regex
|
||||||
|
# See https://docs.djangoproject.com/en/stable/ref/urlresolvers/#django.urls.ResolverMatch.route
|
||||||
|
# getattr() because the attribute doesn't exist in Django < 2.2.
|
||||||
|
route=getattr(resolver_match, "route", None)
|
||||||
|
if resolver_match and with_route
|
||||||
|
else None,
|
||||||
|
# app_name is the application namespace for the URL pattern that matches the URL.
|
||||||
|
# See https://docs.djangoproject.com/en/stable/ref/urlresolvers/#django.urls.ResolverMatch.app_name
|
||||||
|
app_name=(resolver_match.app_name or None)
|
||||||
|
if resolver_match and with_app_name
|
||||||
|
else None,
|
||||||
|
# Framework centric information.
|
||||||
|
framework=f"django:{_django_version}" if with_framework else None,
|
||||||
|
# Information about the database and driver.
|
||||||
|
db_driver=db_driver if with_db_driver else None,
|
||||||
|
**_get_opentelemetry_values() if with_opentelemetry else {},
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: MySQL truncates logs > 1024B so prepend comments
|
||||||
|
# instead of statements, if the engine is MySQL.
|
||||||
|
# See:
|
||||||
|
# * https://github.com/basecamp/marginalia/issues/61
|
||||||
|
# * https://github.com/basecamp/marginalia/pull/80
|
||||||
|
sql += sql_comment
|
||||||
|
|
||||||
|
# Add the query to the query log if debugging.
|
||||||
|
if context["cursor"].__class__ is 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({})
|
@ -102,11 +102,11 @@ class TestMiddleware(WsgiTestBase):
|
|||||||
)
|
)
|
||||||
self.env_patch.start()
|
self.env_patch.start()
|
||||||
self.exclude_patch = patch(
|
self.exclude_patch = patch(
|
||||||
"opentelemetry.instrumentation.django.middleware._DjangoMiddleware._excluded_urls",
|
"opentelemetry.instrumentation.django.middleware.otel_middleware._DjangoMiddleware._excluded_urls",
|
||||||
get_excluded_urls("DJANGO"),
|
get_excluded_urls("DJANGO"),
|
||||||
)
|
)
|
||||||
self.traced_patch = patch(
|
self.traced_patch = patch(
|
||||||
"opentelemetry.instrumentation.django.middleware._DjangoMiddleware._traced_request_attrs",
|
"opentelemetry.instrumentation.django.middleware.otel_middleware._DjangoMiddleware._traced_request_attrs",
|
||||||
get_traced_request_attrs("DJANGO"),
|
get_traced_request_attrs("DJANGO"),
|
||||||
)
|
)
|
||||||
self.exclude_patch.start()
|
self.exclude_patch.start()
|
||||||
|
@ -104,11 +104,11 @@ class TestMiddlewareAsgi(SimpleTestCase, TestBase):
|
|||||||
)
|
)
|
||||||
self.env_patch.start()
|
self.env_patch.start()
|
||||||
self.exclude_patch = patch(
|
self.exclude_patch = patch(
|
||||||
"opentelemetry.instrumentation.django.middleware._DjangoMiddleware._excluded_urls",
|
"opentelemetry.instrumentation.django.middleware.otel_middleware._DjangoMiddleware._excluded_urls",
|
||||||
get_excluded_urls("DJANGO"),
|
get_excluded_urls("DJANGO"),
|
||||||
)
|
)
|
||||||
self.traced_patch = patch(
|
self.traced_patch = patch(
|
||||||
"opentelemetry.instrumentation.django.middleware._DjangoMiddleware._traced_request_attrs",
|
"opentelemetry.instrumentation.django.middleware.otel_middleware._DjangoMiddleware._traced_request_attrs",
|
||||||
get_traced_request_attrs("DJANGO"),
|
get_traced_request_attrs("DJANGO"),
|
||||||
)
|
)
|
||||||
self.exclude_patch.start()
|
self.exclude_patch.start()
|
||||||
|
@ -0,0 +1,100 @@
|
|||||||
|
# Copyright The OpenTelemetry Authors
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# pylint: disable=no-name-in-module
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
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 (
|
||||||
|
_QueryWrapper,
|
||||||
|
)
|
||||||
|
from opentelemetry.test.wsgitestutil import WsgiTestBase
|
||||||
|
|
||||||
|
DJANGO_2_0 = VERSION >= (2, 0)
|
||||||
|
|
||||||
|
_django_instrumentor = DjangoInstrumentor()
|
||||||
|
|
||||||
|
|
||||||
|
class TestMiddleware(WsgiTestBase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
conf.settings.configure(
|
||||||
|
SQLCOMMENTER_WITH_FRAMEWORK=False,
|
||||||
|
SQLCOMMENTER_WITH_DB_DRIVER=False,
|
||||||
|
)
|
||||||
|
super().setUpClass()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
setup_test_environment()
|
||||||
|
_django_instrumentor.instrument(is_sql_commentor_enabled=True)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
teardown_test_environment()
|
||||||
|
_django_instrumentor.uninstrument()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
super().tearDownClass()
|
||||||
|
conf.settings = conf.LazySettings()
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware.SqlCommenter"
|
||||||
|
)
|
||||||
|
def test_middleware_added(self, sqlcommenter_middleware):
|
||||||
|
instance = sqlcommenter_middleware.return_value
|
||||||
|
instance.get_response = HttpResponse()
|
||||||
|
if DJANGO_2_0:
|
||||||
|
middleware = conf.settings.MIDDLEWARE
|
||||||
|
else:
|
||||||
|
middleware = conf.settings.MIDDLEWARE_CLASSES
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
"opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware.SqlCommenter"
|
||||||
|
in middleware
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware._get_opentelemetry_values"
|
||||||
|
)
|
||||||
|
def test_query_wrapper(self, trace_capture):
|
||||||
|
requests_mock = MagicMock()
|
||||||
|
requests_mock.resolver_match.view_name = "view"
|
||||||
|
requests_mock.resolver_match.route = "route"
|
||||||
|
requests_mock.resolver_match.app_name = "app"
|
||||||
|
|
||||||
|
trace_capture.return_value = {
|
||||||
|
"traceparent": "*traceparent='00-000000000000000000000000deadbeef-000000000000beef-00"
|
||||||
|
}
|
||||||
|
qw_instance = _QueryWrapper(requests_mock)
|
||||||
|
execute_mock_obj = MagicMock()
|
||||||
|
qw_instance(
|
||||||
|
execute_mock_obj,
|
||||||
|
"Select 1",
|
||||||
|
MagicMock("test"),
|
||||||
|
MagicMock("test1"),
|
||||||
|
MagicMock(),
|
||||||
|
)
|
||||||
|
output_sql = execute_mock_obj.call_args[0][0]
|
||||||
|
self.assertEqual(
|
||||||
|
output_sql,
|
||||||
|
"Select 1 /*app_name='app',controller='view',route='route',traceparent='%%2Atraceparent%%3D%%2700-0000000"
|
||||||
|
"00000000000000000deadbeef-000000000000beef-00'*/",
|
||||||
|
)
|
Reference in New Issue
Block a user