Skip to content

Commit ac84e99

Browse files
Thiyagu55srikanthccvocelotl
authored
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 <[email protected]> Co-authored-by: Diego Hurtado <[email protected]>
1 parent e267ebc commit ac84e99

File tree

8 files changed

+334
-6
lines changed

8 files changed

+334
-6
lines changed

CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3939
- Add metric instrumentation for WSGI
4040
([#1128](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1128))
4141
- `opentelemetry-instrumentation-requests` Restoring metrics in requests
42-
([#1110](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1110)
42+
([#1110](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1110))
43+
- Integrated sqlcommenter plugin into opentelemetry-instrumentation-django
44+
([#896](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/896))
4345

4446

4547
## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17

instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py

+74-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,68 @@
1717
1818
.. _django: https://pypi.org/project/django/
1919
20+
SQLCOMMENTER
21+
*****************************************
22+
You can optionally configure Django instrumentation to enable sqlcommenter which enriches
23+
the query with contextual information.
24+
25+
Usage
26+
-----
27+
28+
.. code:: python
29+
30+
from opentelemetry.instrumentation.django import DjangoInstrumentor
31+
32+
DjangoInstrumentor().instrument(is_sql_commentor_enabled=True)
33+
34+
35+
For example,
36+
::
37+
38+
Invoking Users().objects.all() will lead to sql query "select * from auth_users" but when SQLCommenter is enabled
39+
the query will get appended with some configurable tags like "select * from auth_users /*metrics=value*/;"
40+
41+
42+
SQLCommenter Configurations
43+
***************************
44+
We can configure the tags to be appended to the sqlquery log by adding below variables to the settings.py
45+
46+
SQLCOMMENTER_WITH_FRAMEWORK = True(Default) or False
47+
48+
For example,
49+
::
50+
Enabling this flag will add django framework and it's version which is /*framework='django%3A2.2.3*/
51+
52+
SQLCOMMENTER_WITH_CONTROLLER = True(Default) or False
53+
54+
For example,
55+
::
56+
Enabling this flag will add controller name that handles the request /*controller='index'*/
57+
58+
SQLCOMMENTER_WITH_ROUTE = True(Default) or False
59+
60+
For example,
61+
::
62+
Enabling this flag will add url path that handles the request /*route='polls/'*/
63+
64+
SQLCOMMENTER_WITH_APP_NAME = True(Default) or False
65+
66+
For example,
67+
::
68+
Enabling this flag will add app name that handles the request /*app_name='polls'*/
69+
70+
SQLCOMMENTER_WITH_OPENTELEMETRY = True(Default) or False
71+
72+
For example,
73+
::
74+
Enabling this flag will add opentelemetry traceparent /*traceparent='00-fd720cffceba94bbf75940ff3caaf3cc-4fd1a2bdacf56388-01'*/
75+
76+
SQLCOMMENTER_WITH_DB_DRIVER = True(Default) or False
77+
78+
For example,
79+
::
80+
Enabling this flag will add name of the db driver /*db_driver='django.db.backends.postgresql'*/
81+
2082
Usage
2183
-----
2284
@@ -124,6 +186,7 @@ def response_hook(span, request, response):
124186
125187
API
126188
---
189+
127190
"""
128191

129192
from logging import getLogger
@@ -136,7 +199,9 @@ def response_hook(span, request, response):
136199
from opentelemetry.instrumentation.django.environment_variables import (
137200
OTEL_PYTHON_DJANGO_INSTRUMENT,
138201
)
139-
from opentelemetry.instrumentation.django.middleware import _DjangoMiddleware
202+
from opentelemetry.instrumentation.django.middleware.otel_middleware import (
203+
_DjangoMiddleware,
204+
)
140205
from opentelemetry.instrumentation.django.package import _instruments
141206
from opentelemetry.instrumentation.django.version import __version__
142207
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
@@ -166,6 +231,8 @@ class DjangoInstrumentor(BaseInstrumentor):
166231
[_DjangoMiddleware.__module__, _DjangoMiddleware.__qualname__]
167232
)
168233

234+
_sql_commenter_middleware = "opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware.SqlCommenter"
235+
169236
def instrumentation_dependencies(self) -> Collection[str]:
170237
return _instruments
171238

@@ -204,7 +271,13 @@ def _instrument(self, **kwargs):
204271
if isinstance(settings_middleware, tuple):
205272
settings_middleware = list(settings_middleware)
206273

274+
is_sql_commentor_enabled = kwargs.pop("is_sql_commentor_enabled", None)
275+
276+
if is_sql_commentor_enabled:
277+
settings_middleware.insert(0, self._sql_commenter_middleware)
278+
207279
settings_middleware.insert(0, self._opentelemetry_middleware)
280+
208281
setattr(settings, _middleware_setting, settings_middleware)
209282

210283
def _uninstrument(self, **kwargs):

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

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#!/usr/bin/python
2+
#
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
from logging import getLogger
17+
from typing import Any, Type, TypeVar
18+
from urllib.parse import quote as urllib_quote
19+
20+
# pylint: disable=no-name-in-module
21+
from django import conf, get_version
22+
from django.db import connection
23+
from django.db.backends.utils import CursorDebugWrapper
24+
25+
from opentelemetry.trace.propagation.tracecontext import (
26+
TraceContextTextMapPropagator,
27+
)
28+
29+
_propagator = TraceContextTextMapPropagator()
30+
31+
_django_version = get_version()
32+
_logger = getLogger(__name__)
33+
34+
T = TypeVar("T") # pylint: disable-msg=invalid-name
35+
36+
37+
class SqlCommenter:
38+
"""
39+
Middleware to append a comment to each database query with details about
40+
the framework and the execution context.
41+
"""
42+
43+
def __init__(self, get_response) -> None:
44+
self.get_response = get_response
45+
46+
def __call__(self, request) -> Any:
47+
with connection.execute_wrapper(_QueryWrapper(request)):
48+
return self.get_response(request)
49+
50+
51+
class _QueryWrapper:
52+
def __init__(self, request) -> None:
53+
self.request = request
54+
55+
def __call__(self, execute: Type[T], sql, params, many, context) -> T:
56+
# pylint: disable-msg=too-many-locals
57+
with_framework = getattr(
58+
conf.settings, "SQLCOMMENTER_WITH_FRAMEWORK", True
59+
)
60+
with_controller = getattr(
61+
conf.settings, "SQLCOMMENTER_WITH_CONTROLLER", True
62+
)
63+
with_route = getattr(conf.settings, "SQLCOMMENTER_WITH_ROUTE", True)
64+
with_app_name = getattr(
65+
conf.settings, "SQLCOMMENTER_WITH_APP_NAME", True
66+
)
67+
with_opentelemetry = getattr(
68+
conf.settings, "SQLCOMMENTER_WITH_OPENTELEMETRY", True
69+
)
70+
with_db_driver = getattr(
71+
conf.settings, "SQLCOMMENTER_WITH_DB_DRIVER", True
72+
)
73+
74+
db_driver = context["connection"].settings_dict.get("ENGINE", "")
75+
resolver_match = self.request.resolver_match
76+
77+
sql_comment = _generate_sql_comment(
78+
# Information about the controller.
79+
controller=resolver_match.view_name
80+
if resolver_match and with_controller
81+
else None,
82+
# route is the pattern that matched a request with a controller i.e. the regex
83+
# See https://docs.djangoproject.com/en/stable/ref/urlresolvers/#django.urls.ResolverMatch.route
84+
# getattr() because the attribute doesn't exist in Django < 2.2.
85+
route=getattr(resolver_match, "route", None)
86+
if resolver_match and with_route
87+
else None,
88+
# app_name is the application namespace for the URL pattern that matches the URL.
89+
# See https://docs.djangoproject.com/en/stable/ref/urlresolvers/#django.urls.ResolverMatch.app_name
90+
app_name=(resolver_match.app_name or None)
91+
if resolver_match and with_app_name
92+
else None,
93+
# Framework centric information.
94+
framework=f"django:{_django_version}" if with_framework else None,
95+
# Information about the database and driver.
96+
db_driver=db_driver if with_db_driver else None,
97+
**_get_opentelemetry_values() if with_opentelemetry else {},
98+
)
99+
100+
# TODO: MySQL truncates logs > 1024B so prepend comments
101+
# instead of statements, if the engine is MySQL.
102+
# See:
103+
# * https://github.com/basecamp/marginalia/issues/61
104+
# * https://github.com/basecamp/marginalia/pull/80
105+
sql += sql_comment
106+
107+
# Add the query to the query log if debugging.
108+
if context["cursor"].__class__ is CursorDebugWrapper:
109+
context["connection"].queries_log.append(sql)
110+
111+
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

+2-2
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,11 @@ def setUp(self):
102102
)
103103
self.env_patch.start()
104104
self.exclude_patch = patch(
105-
"opentelemetry.instrumentation.django.middleware._DjangoMiddleware._excluded_urls",
105+
"opentelemetry.instrumentation.django.middleware.otel_middleware._DjangoMiddleware._excluded_urls",
106106
get_excluded_urls("DJANGO"),
107107
)
108108
self.traced_patch = patch(
109-
"opentelemetry.instrumentation.django.middleware._DjangoMiddleware._traced_request_attrs",
109+
"opentelemetry.instrumentation.django.middleware.otel_middleware._DjangoMiddleware._traced_request_attrs",
110110
get_traced_request_attrs("DJANGO"),
111111
)
112112
self.exclude_patch.start()

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,11 @@ def setUp(self):
104104
)
105105
self.env_patch.start()
106106
self.exclude_patch = patch(
107-
"opentelemetry.instrumentation.django.middleware._DjangoMiddleware._excluded_urls",
107+
"opentelemetry.instrumentation.django.middleware.otel_middleware._DjangoMiddleware._excluded_urls",
108108
get_excluded_urls("DJANGO"),
109109
)
110110
self.traced_patch = patch(
111-
"opentelemetry.instrumentation.django.middleware._DjangoMiddleware._traced_request_attrs",
111+
"opentelemetry.instrumentation.django.middleware.otel_middleware._DjangoMiddleware._traced_request_attrs",
112112
get_traced_request_attrs("DJANGO"),
113113
)
114114
self.exclude_patch.start()

0 commit comments

Comments
 (0)