Skip to content

Commit 32d7ff4

Browse files
authored
Flask sqlalchemy psycopg2 integration (#1224)
1 parent 270b73f commit 32d7ff4

File tree

12 files changed

+309
-46
lines changed

12 files changed

+309
-46
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
- Flask sqlalchemy psycopg2 integration
12+
([#1224](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1224))
13+
1014
### Fixed
1115

1216
- `opentelemetry-instrumentation-boto3sqs` Make propagation compatible with other SQS instrumentations, add 'messaging.url' span attribute, and fix missing package dependencies.

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@
4545

4646
from opentelemetry import trace as trace_api
4747
from opentelemetry.instrumentation.dbapi.version import __version__
48+
from opentelemetry.instrumentation.sqlcommenter_utils import _add_sql_comment
4849
from opentelemetry.instrumentation.utils import (
49-
_add_sql_comment,
5050
_get_opentelemetry_values,
5151
unwrap,
5252
)

instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py

+33
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import logging
1717
from unittest import mock
1818

19+
from opentelemetry import context
1920
from opentelemetry import trace as trace_api
2021
from opentelemetry.instrumentation import dbapi
2122
from opentelemetry.sdk import resources
@@ -254,6 +255,38 @@ def test_executemany_comment(self):
254255
r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
255256
)
256257

258+
def test_executemany_flask_integration_comment(self):
259+
260+
connect_module = mock.MagicMock()
261+
connect_module.__version__ = mock.MagicMock()
262+
connect_module.__libpq_version__ = 123
263+
connect_module.apilevel = 123
264+
connect_module.threadsafety = 123
265+
connect_module.paramstyle = "test"
266+
267+
db_integration = dbapi.DatabaseApiIntegration(
268+
"testname",
269+
"testcomponent",
270+
enable_commenter=True,
271+
commenter_options={"db_driver": False, "dbapi_level": False},
272+
connect_module=connect_module,
273+
)
274+
current_context = context.get_current()
275+
sqlcommenter_context = context.set_value(
276+
"SQLCOMMENTER_ORM_TAGS_AND_VALUES", {"flask": 1}, current_context
277+
)
278+
context.attach(sqlcommenter_context)
279+
280+
mock_connection = db_integration.wrapped_connection(
281+
mock_connect, {}, {}
282+
)
283+
cursor = mock_connection.cursor()
284+
cursor.executemany("Select 1;")
285+
self.assertRegex(
286+
cursor.query,
287+
r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',flask=1,libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
288+
)
289+
257290
def test_callproc(self):
258291
db_integration = dbapi.DatabaseApiIntegration(
259292
"testname", "testcomponent"

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

+2-4
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@
2222
from django.db import connections
2323
from django.db.backends.utils import CursorDebugWrapper
2424

25-
from opentelemetry.instrumentation.utils import (
26-
_add_sql_comment,
27-
_get_opentelemetry_values,
28-
)
25+
from opentelemetry.instrumentation.sqlcommenter_utils import _add_sql_comment
26+
from opentelemetry.instrumentation.utils import _get_opentelemetry_values
2927
from opentelemetry.trace.propagation.tracecontext import (
3028
TraceContextTextMapPropagator,
3129
)

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

+88
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,53 @@
2424
* The ``http.route`` Span attribute is set so that one can see which URL rule
2525
matched a request.
2626
27+
SQLCOMMENTER
28+
*****************************************
29+
You can optionally configure Flask instrumentation to enable sqlcommenter which enriches
30+
the query with contextual information.
31+
32+
Usage
33+
-----
34+
35+
.. code:: python
36+
37+
from opentelemetry.instrumentation.flask import FlaskInstrumentor
38+
39+
FlaskInstrumentor().instrument(enable_commenter=True, commenter_options={})
40+
41+
42+
For example,
43+
::
44+
45+
FlaskInstrumentor when used with SQLAlchemyInstrumentor or Psycopg2Instrumentor, invoking cursor.execute("select * from auth_users")
46+
will lead to sql query "select * from auth_users" but when SQLCommenter is enabled
47+
the query will get appended with some configurable tags like "select * from auth_users /*metrics=value*/;"
48+
49+
Inorder for the commenter to append flask related tags to sql queries, the commenter needs to enabled on
50+
the respective SQLAlchemyInstrumentor or Psycopg2Instrumentor framework too.
51+
52+
SQLCommenter Configurations
53+
***************************
54+
We can configure the tags to be appended to the sqlquery log by adding configuration inside commenter_options(default:{}) keyword
55+
56+
framework = True(Default) or False
57+
58+
For example,
59+
::
60+
Enabling this flag will add flask and it's version which is /*flask%%3A2.9.3*/
61+
62+
route = True(Default) or False
63+
64+
For example,
65+
::
66+
Enabling this flag will add route uri /*route='/home'*/
67+
68+
controller = True(Default) or False
69+
70+
For example,
71+
::
72+
Enabling this flag will add controller name /*controller='home_view'*/
73+
2774
Usage
2875
-----
2976
@@ -255,6 +302,8 @@ def _wrapped_before_request(
255302
request_hook=None,
256303
tracer=None,
257304
excluded_urls=None,
305+
enable_commenter=True,
306+
commenter_options=None,
258307
):
259308
def _before_request():
260309
if excluded_urls and excluded_urls.url_disabled(flask.request.url):
@@ -300,6 +349,30 @@ def _before_request():
300349
flask_request_environ[_ENVIRON_SPAN_KEY] = span
301350
flask_request_environ[_ENVIRON_TOKEN] = token
302351

352+
if enable_commenter:
353+
current_context = context.get_current()
354+
flask_info = {}
355+
356+
# https://flask.palletsprojects.com/en/1.1.x/api/#flask.has_request_context
357+
if flask and flask.request:
358+
if commenter_options.get("framework", True):
359+
flask_info["framework"] = f"flask:{flask.__version__}"
360+
if (
361+
commenter_options.get("controller", True)
362+
and flask.request.endpoint
363+
):
364+
flask_info["controller"] = flask.request.endpoint
365+
if (
366+
commenter_options.get("route", True)
367+
and flask.request.url_rule
368+
and flask.request.url_rule.rule
369+
):
370+
flask_info["route"] = flask.request.url_rule.rule
371+
sqlcommenter_context = context.set_value(
372+
"SQLCOMMENTER_ORM_TAGS_AND_VALUES", flask_info, current_context
373+
)
374+
context.attach(sqlcommenter_context)
375+
303376
return _before_request
304377

305378

@@ -336,6 +409,8 @@ class _InstrumentedFlask(flask.Flask):
336409
_tracer_provider = None
337410
_request_hook = None
338411
_response_hook = None
412+
_enable_commenter = True
413+
_commenter_options = None
339414
_meter_provider = None
340415

341416
def __init__(self, *args, **kwargs):
@@ -374,6 +449,8 @@ def __init__(self, *args, **kwargs):
374449
_InstrumentedFlask._request_hook,
375450
tracer,
376451
excluded_urls=_InstrumentedFlask._excluded_urls,
452+
enable_commenter=_InstrumentedFlask._enable_commenter,
453+
commenter_options=_InstrumentedFlask._commenter_options,
377454
)
378455
self._before_request = _before_request
379456
self.before_request(_before_request)
@@ -410,6 +487,11 @@ def _instrument(self, **kwargs):
410487
if excluded_urls is None
411488
else parse_excluded_urls(excluded_urls)
412489
)
490+
enable_commenter = kwargs.get("enable_commenter", True)
491+
_InstrumentedFlask._enable_commenter = enable_commenter
492+
493+
commenter_options = kwargs.get("commenter_options", {})
494+
_InstrumentedFlask._commenter_options = commenter_options
413495
meter_provider = kwargs.get("meter_provider")
414496
_InstrumentedFlask._meter_provider = meter_provider
415497
flask.Flask = _InstrumentedFlask
@@ -424,6 +506,8 @@ def instrument_app(
424506
response_hook=None,
425507
tracer_provider=None,
426508
excluded_urls=None,
509+
enable_commenter=True,
510+
commenter_options=None,
427511
meter_provider=None,
428512
):
429513
if not hasattr(app, "_is_instrumented_by_opentelemetry"):
@@ -462,6 +546,10 @@ def instrument_app(
462546
request_hook,
463547
tracer,
464548
excluded_urls=excluded_urls,
549+
enable_commenter=enable_commenter,
550+
commenter_options=commenter_options
551+
if commenter_options
552+
else {},
465553
)
466554
app._before_request = _before_request
467555
app.before_request(_before_request)

instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py

+11
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from werkzeug.test import Client
1717
from werkzeug.wrappers import Response
1818

19+
from opentelemetry import context
20+
1921

2022
class InstrumentationTest:
2123
@staticmethod
@@ -24,6 +26,14 @@ def _hello_endpoint(helloid):
2426
raise ValueError(":-(")
2527
return "Hello: " + str(helloid)
2628

29+
@staticmethod
30+
def _sqlcommenter_endpoint():
31+
current_context = context.get_current()
32+
sqlcommenter_flask_values = current_context.get(
33+
"SQLCOMMENTER_ORM_TAGS_AND_VALUES", {}
34+
)
35+
return sqlcommenter_flask_values
36+
2737
@staticmethod
2838
def _custom_response_headers():
2939
resp = flask.Response("test response")
@@ -43,6 +53,7 @@ def excluded2_endpoint():
4353

4454
# pylint: disable=no-member
4555
self.app.route("/hello/<int:helloid>")(self._hello_endpoint)
56+
self.app.route("/sqlcommenter")(self._sqlcommenter_endpoint)
4657
self.app.route("/excluded/<int:helloid>")(self._hello_endpoint)
4758
self.app.route("/excluded")(excluded_endpoint)
4859
self.app.route("/excluded2")(excluded2_endpoint)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import flask
16+
from werkzeug.test import Client
17+
from werkzeug.wrappers import Response
18+
19+
from opentelemetry.instrumentation.flask import FlaskInstrumentor
20+
from opentelemetry.test.wsgitestutil import WsgiTestBase
21+
22+
# pylint: disable=import-error
23+
from .base_test import InstrumentationTest
24+
25+
26+
class TestSQLCommenter(InstrumentationTest, WsgiTestBase):
27+
def setUp(self):
28+
super().setUp()
29+
FlaskInstrumentor().instrument()
30+
self.app = flask.Flask(__name__)
31+
self._common_initialization()
32+
33+
def tearDown(self):
34+
super().tearDown()
35+
with self.disable_logging():
36+
FlaskInstrumentor().uninstrument()
37+
38+
def test_sqlcommenter_enabled_default(self):
39+
40+
self.app = flask.Flask(__name__)
41+
self.app.route("/sqlcommenter")(self._sqlcommenter_endpoint)
42+
client = Client(self.app, Response)
43+
44+
resp = client.get("/sqlcommenter")
45+
self.assertEqual(200, resp.status_code)
46+
self.assertRegex(
47+
list(resp.response)[0].strip(),
48+
b'{"controller":"_sqlcommenter_endpoint","framework":"flask:(.*)","route":"/sqlcommenter"}',
49+
)
50+
51+
def test_sqlcommenter_enabled_with_configurations(self):
52+
FlaskInstrumentor().uninstrument()
53+
FlaskInstrumentor().instrument(
54+
enable_commenter=True, commenter_options={"route": False}
55+
)
56+
57+
self.app = flask.Flask(__name__)
58+
self.app.route("/sqlcommenter")(self._sqlcommenter_endpoint)
59+
client = Client(self.app, Response)
60+
61+
resp = client.get("/sqlcommenter")
62+
self.assertEqual(200, resp.status_code)
63+
self.assertRegex(
64+
list(resp.response)[0].strip(),
65+
b'{"controller":"_sqlcommenter_endpoint","framework":"flask:(.*)"}',
66+
)
67+
68+
def test_sqlcommenter_disabled(self):
69+
FlaskInstrumentor().uninstrument()
70+
FlaskInstrumentor().instrument(enable_commenter=False)
71+
72+
self.app = flask.Flask(__name__)
73+
self.app.route("/sqlcommenter")(self._sqlcommenter_endpoint)
74+
client = Client(self.app, Response)
75+
76+
resp = client.get("/sqlcommenter")
77+
self.assertEqual(200, resp.status_code)
78+
self.assertEqual(list(resp.response)[0].strip(), b"{}")

instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@
2020
_instrumenting_module_name,
2121
)
2222
from opentelemetry.instrumentation.sqlalchemy.version import __version__
23-
from opentelemetry.instrumentation.utils import (
24-
_add_sql_comment,
25-
_get_opentelemetry_values,
26-
)
23+
from opentelemetry.instrumentation.sqlcommenter_utils import _add_sql_comment
24+
from opentelemetry.instrumentation.utils import _get_opentelemetry_values
2725
from opentelemetry.semconv.trace import NetTransportValues, SpanAttributes
2826
from opentelemetry.trace.status import Status, StatusCode
2927

instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlcommenter.py

+23
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import pytest
1717
from sqlalchemy import create_engine
1818

19+
from opentelemetry import context
1920
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
2021
from opentelemetry.test.test_base import TestBase
2122

@@ -54,3 +55,25 @@ def test_sqlcommenter_enabled(self):
5455
self.caplog.records[-2].getMessage(),
5556
r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
5657
)
58+
59+
def test_sqlcommenter_flask_integration(self):
60+
engine = create_engine("sqlite:///:memory:")
61+
SQLAlchemyInstrumentor().instrument(
62+
engine=engine,
63+
tracer_provider=self.tracer_provider,
64+
enable_commenter=True,
65+
commenter_options={"db_framework": False},
66+
)
67+
cnx = engine.connect()
68+
69+
current_context = context.get_current()
70+
sqlcommenter_context = context.set_value(
71+
"SQLCOMMENTER_ORM_TAGS_AND_VALUES", {"flask": 1}, current_context
72+
)
73+
context.attach(sqlcommenter_context)
74+
75+
cnx.execute("SELECT 1;").fetchall()
76+
self.assertRegex(
77+
self.caplog.records[-2].getMessage(),
78+
r"SELECT 1 /\*db_driver='(.*)',flask=1,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
79+
)

0 commit comments

Comments
 (0)