Adding multiple db connections support for django-instrumentation's sqlcommenter (#1187)
This commit is contained in:
		
							parent
							
								
									2eb86e06f9
								
							
						
					
					
						commit
						9e2dbecedc
					
				|  | @ -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" | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue