Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flask sqlalchemy psycopg2 integration #1224

Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c45dec7
Added flask support in dbapi
Thiyagu55 Aug 1, 2022
6c543a9
Added test cases for flask
Thiyagu55 Aug 3, 2022
21de491
Test case for dbapi
Thiyagu55 Aug 4, 2022
108e618
linting changes
Thiyagu55 Aug 4, 2022
f38e22a
Merge branch 'open-telemetry:main' into flask-sqlalchemy-psycopg2-int…
Thiyagu55 Aug 5, 2022
125cba0
Integrating in sqlalchemy
Thiyagu55 Aug 9, 2022
ee913bf
Merging main
Thiyagu55 Aug 9, 2022
63ed9d9
Added PR in CHANGELOG.md
Thiyagu55 Aug 9, 2022
9e7058d
Linting changes
Thiyagu55 Aug 9, 2022
18a82f7
Linting changes
Thiyagu55 Aug 9, 2022
1c4f841
Linting changes
Thiyagu55 Aug 9, 2022
dbaea4b
Merge branch 'main' into flask-sqlalchemy-psycopg2-integration
srikanthccv Aug 10, 2022
7be8ac1
Merge branch 'main' into flask-sqlalchemy-psycopg2-integration
ocelotl Aug 24, 2022
8e88ac3
Merge branch 'open-telemetry:main' into flask-sqlalchemy-psycopg2-int…
Thiyagu55 Aug 26, 2022
4ebf055
PR changes
Thiyagu55 Aug 26, 2022
7c17444
Linting changes
Thiyagu55 Aug 26, 2022
867ab5b
PR changes
Thiyagu55 Aug 26, 2022
7ceb8c0
PR changes
Thiyagu55 Aug 26, 2022
c78c3c6
PR changes
Thiyagu55 Aug 26, 2022
2b79664
PR changes
Thiyagu55 Aug 26, 2022
8671dd6
Added license header to sqlcomenter_utils.py
Thiyagu55 Aug 26, 2022
b75c0ac
Merge branch 'main' into flask-sqlalchemy-psycopg2-integration
srikanthccv Sep 1, 2022
85b6302
PR changes
Thiyagu55 Sep 1, 2022
595a702
Merge remote-tracking branch 'origin/flask-sqlalchemy-psycopg2-integr…
Thiyagu55 Sep 1, 2022
37574dd
Merge branch 'main' into flask-sqlalchemy-psycopg2-integration
srikanthccv Sep 6, 2022
5538c1a
Merge branch 'main' into flask-sqlalchemy-psycopg2-integration
srikanthccv Sep 8, 2022
d90dfac
Update CHANGELOG.md
srikanthccv Sep 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#1206](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1206))
- Add psycopg2 native tags to sqlcommenter
([#1203](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1203))
- Flask sqlalchemy psycopg2 integration
([#1224](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1224))
Thiyagu55 marked this conversation as resolved.
Show resolved Hide resolved

### Added
- `opentelemetry-instrumentation-redis` add support to instrument RedisCluster clients
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@

import wrapt

from opentelemetry import context as opentelemetry_context
from opentelemetry import trace as trace_api
from opentelemetry.instrumentation.dbapi.version import __version__
from opentelemetry.instrumentation.utils import (
Expand Down Expand Up @@ -430,6 +431,18 @@ def traced_execution(
):
commenter_data.update(**_get_opentelemetry_values())

# Add flask related tags
sqlcommenter_flask_values = (
opentelemetry_context.get_value(
"SQLCOMMENTER_FLASK_VALUES"
Thiyagu55 marked this conversation as resolved.
Show resolved Hide resolved
)
if opentelemetry_context.get_value(
"SQLCOMMENTER_FLASK_VALUES"
)
else {}
)
commenter_data.update(**sqlcommenter_flask_values)

# Filter down to just the requested attributes.
commenter_data = {
k: v
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import logging
from unittest import mock

from opentelemetry import context
from opentelemetry import trace as trace_api
from opentelemetry.instrumentation import dbapi
from opentelemetry.sdk import resources
Expand Down Expand Up @@ -254,6 +255,38 @@ def test_executemany_comment(self):
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}'\*/;",
)

def test_executemany_flask_integration_comment(self):

connect_module = mock.MagicMock()
connect_module.__version__ = mock.MagicMock()
connect_module.__libpq_version__ = 123
connect_module.apilevel = 123
connect_module.threadsafety = 123
connect_module.paramstyle = "test"

db_integration = dbapi.DatabaseApiIntegration(
"testname",
"testcomponent",
enable_commenter=True,
commenter_options={"db_driver": False, "dbapi_level": False},
connect_module=connect_module,
)
current_context = context.get_current()
sqlcommenter_context = context.set_value(
"SQLCOMMENTER_FLASK_VALUES", {"flask": 1}, current_context
)
context.attach(sqlcommenter_context)

mock_connection = db_integration.wrapped_connection(
mock_connect, {}, {}
)
cursor = mock_connection.cursor()
cursor.executemany("Select 1;")
self.assertRegex(
cursor.query,
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}'\*/;",
)

def test_callproc(self):
db_integration = dbapi.DatabaseApiIntegration(
"testname", "testcomponent"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,53 @@
* The ``http.route`` Span attribute is set so that one can see which URL rule
matched a request.

SQLCOMMENTER
*****************************************
You can optionally configure Flask instrumentation to enable sqlcommenter which enriches
the query with contextual information.

Usage
-----

.. code:: python

from opentelemetry.instrumentation.flask import FlaskInstrumentor

FlaskInstrumentor().instrument(enable_commenter=True, commenter_options={})


For example,
::

FlaskInstrumentor when used with SQLAlchemyInstrumentor or Psycopg2Instrumentor, invoking cursor.execute("select * from auth_users")
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*/;"

Inorder for the commenter to append flask related tags to sql queries, the commenter needs to enabled on
the respective SQLAlchemyInstrumentor or Psycopg2Instrumentor framework too.

SQLCommenter Configurations
***************************
We can configure the tags to be appended to the sqlquery log by adding configuration inside commenter_options(default:{}) keyword

framework = True(Default) or False

For example,
::
Enabling this flag will add flask and it's version which is /*flask%%3A2.9.3*/

route = True(Default) or False

For example,
::
Enabling this flag will add route uri /*route='/home'*/

controller = True(Default) or False

For example,
::
Enabling this flag will add controller name /*controller='home_view'*/

Usage
-----

Expand Down Expand Up @@ -255,6 +302,8 @@ def _wrapped_before_request(
request_hook=None,
tracer=None,
excluded_urls=None,
enable_commenter=True,
commenter_options=None,
):
def _before_request():
if excluded_urls and excluded_urls.url_disabled(flask.request.url):
Expand Down Expand Up @@ -300,6 +349,30 @@ def _before_request():
flask_request_environ[_ENVIRON_SPAN_KEY] = span
flask_request_environ[_ENVIRON_TOKEN] = token

if enable_commenter:
current_context = context.get_current()
flask_info = {}

# https://flask.palletsprojects.com/en/1.1.x/api/#flask.has_request_context
if flask and flask.request:
if commenter_options.get("framework", True):
flask_info["framework"] = f"flask:{flask.__version__}"
if (
commenter_options.get("controller", True)
and flask.request.endpoint
):
flask_info["controller"] = flask.request.endpoint
if (
commenter_options.get("route", True)
and flask.request.url_rule
and flask.request.url_rule.rule
):
flask_info["route"] = flask.request.url_rule.rule
sqlcommenter_context = context.set_value(
"SQLCOMMENTER_FLASK_VALUES", flask_info, current_context
)
context.attach(sqlcommenter_context)

return _before_request


Expand Down Expand Up @@ -336,6 +409,8 @@ class _InstrumentedFlask(flask.Flask):
_tracer_provider = None
_request_hook = None
_response_hook = None
_enable_commenter = True
_commenter_options = None
_meter_provider = None

def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -374,6 +449,8 @@ def __init__(self, *args, **kwargs):
_InstrumentedFlask._request_hook,
tracer,
excluded_urls=_InstrumentedFlask._excluded_urls,
enable_commenter=_InstrumentedFlask._enable_commenter,
commenter_options=_InstrumentedFlask._commenter_options,
)
self._before_request = _before_request
self.before_request(_before_request)
Expand Down Expand Up @@ -410,6 +487,11 @@ def _instrument(self, **kwargs):
if excluded_urls is None
else parse_excluded_urls(excluded_urls)
)
enable_commenter = kwargs.get("enable_commenter", True)
_InstrumentedFlask._enable_commenter = enable_commenter

commenter_options = kwargs.get("commenter_options", {})
_InstrumentedFlask._commenter_options = commenter_options
meter_provider = kwargs.get("meter_provider")
_InstrumentedFlask._meter_provider = meter_provider
flask.Flask = _InstrumentedFlask
Expand All @@ -424,6 +506,8 @@ def instrument_app(
response_hook=None,
tracer_provider=None,
excluded_urls=None,
enable_commenter=True,
commenter_options=None,
meter_provider=None,
):
if not hasattr(app, "_is_instrumented_by_opentelemetry"):
Expand Down Expand Up @@ -462,6 +546,10 @@ def instrument_app(
request_hook,
tracer,
excluded_urls=excluded_urls,
enable_commenter=enable_commenter,
commenter_options=commenter_options
if commenter_options
else {},
)
app._before_request = _before_request
app.before_request(_before_request)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from werkzeug.test import Client
from werkzeug.wrappers import Response

from opentelemetry import context


class InstrumentationTest:
@staticmethod
Expand All @@ -24,6 +26,14 @@ def _hello_endpoint(helloid):
raise ValueError(":-(")
return "Hello: " + str(helloid)

@staticmethod
def _sqlcommenter_endpoint():
current_context = context.get_current()
sqlcommenter_flask_values = current_context.get(
"SQLCOMMENTER_FLASK_VALUES", {}
)
return sqlcommenter_flask_values

@staticmethod
def _custom_response_headers():
resp = flask.Response("test response")
Expand All @@ -43,6 +53,7 @@ def excluded2_endpoint():

# pylint: disable=no-member
self.app.route("/hello/<int:helloid>")(self._hello_endpoint)
self.app.route("/sqlcommenter")(self._sqlcommenter_endpoint)
self.app.route("/excluded/<int:helloid>")(self._hello_endpoint)
self.app.route("/excluded")(excluded_endpoint)
self.app.route("/excluded2")(excluded2_endpoint)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# 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.

import flask
from werkzeug.test import Client
from werkzeug.wrappers import Response

from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.test.wsgitestutil import WsgiTestBase

# pylint: disable=import-error
from .base_test import InstrumentationTest


class TestSQLCommenter(InstrumentationTest, WsgiTestBase):
def setUp(self):
super().setUp()
FlaskInstrumentor().instrument()
self.app = flask.Flask(__name__)
self._common_initialization()

def tearDown(self):
super().tearDown()
with self.disable_logging():
FlaskInstrumentor().uninstrument()

def test_sqlcommenter_enabled_default(self):

self.app = flask.Flask(__name__)
self.app.route("/sqlcommenter")(self._sqlcommenter_endpoint)
client = Client(self.app, Response)

resp = client.get("/sqlcommenter")
self.assertEqual(200, resp.status_code)
self.assertRegex(
list(resp.response)[0].strip(),
b'{"controller":"_sqlcommenter_endpoint","framework":"flask:(.*)","route":"/sqlcommenter"}',
)

def test_sqlcommenter_enabled_with_configurations(self):
FlaskInstrumentor().uninstrument()
FlaskInstrumentor().instrument(
enable_commenter=True, commenter_options={"route": False}
)

self.app = flask.Flask(__name__)
self.app.route("/sqlcommenter")(self._sqlcommenter_endpoint)
client = Client(self.app, Response)

resp = client.get("/sqlcommenter")
self.assertEqual(200, resp.status_code)
self.assertRegex(
list(resp.response)[0].strip(),
b'{"controller":"_sqlcommenter_endpoint","framework":"flask:(.*)"}',
)

def test_sqlcommenter_disabled(self):
FlaskInstrumentor().uninstrument()
FlaskInstrumentor().instrument(enable_commenter=False)

self.app = flask.Flask(__name__)
self.app.route("/sqlcommenter")(self._sqlcommenter_endpoint)
client = Client(self.app, Response)

resp = client.get("/sqlcommenter")
self.assertEqual(200, resp.status_code)
self.assertEqual(list(resp.response)[0].strip(), b"{}")
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from sqlalchemy.event import listen # pylint: disable=no-name-in-module

from opentelemetry import context as opentelemetry_context
from opentelemetry import trace
from opentelemetry.instrumentation.sqlalchemy.package import (
_instrumenting_module_name,
Expand Down Expand Up @@ -160,6 +161,18 @@ def _before_cur_exec(
if self.commenter_options.get(k, True)
}

# Add flask related tags
sqlcommenter_flask_values = (
opentelemetry_context.get_value(
"SQLCOMMENTER_FLASK_VALUES"
)
if opentelemetry_context.get_value(
"SQLCOMMENTER_FLASK_VALUES"
)
else {}
)
commenter_data.update(**sqlcommenter_flask_values)

statement = _add_sql_comment(statement, **commenter_data)

context._otel_span = span
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import pytest
from sqlalchemy import create_engine

from opentelemetry import context
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.test.test_base import TestBase

Expand Down Expand Up @@ -54,3 +55,25 @@ def test_sqlcommenter_enabled(self):
self.caplog.records[-2].getMessage(),
r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
)

def test_sqlcommenter_flask_integration(self):
engine = create_engine("sqlite:///:memory:")
SQLAlchemyInstrumentor().instrument(
engine=engine,
tracer_provider=self.tracer_provider,
enable_commenter=True,
commenter_options={"db_framework": False},
)
cnx = engine.connect()

current_context = context.get_current()
sqlcommenter_context = context.set_value(
"SQLCOMMENTER_FLASK_VALUES", {"flask": 1}, current_context
)
context.attach(sqlcommenter_context)

cnx.execute("SELECT 1;").fetchall()
self.assertRegex(
self.caplog.records[-2].getMessage(),
r"SELECT 1 /\*db_driver='(.*)',flask=1,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
)