pallets-eco / flask-debugtoolbar

A toolbar overlay for debugging Flask applications
https://flask-debugtoolbar.readthedocs.io
BSD 3-Clause "New" or "Revised" License
944 stars 143 forks source link

No Context shown in SQLAlchemy tab #96

Open rhaamo opened 8 years ago

rhaamo commented 8 years ago

The context column only shown <unknown> on all of my pages. I'm using Blueprints, can this cause issues ? Versions used (not only but related to debugtoolbar):

Flask==0.10.1
Flask-SQLAlchemy==2.1
Flask-DebugToolbar==0.10.0
SQLAlchemy==1.0.12

Config extract:

DEBUG = True
TESTING = True
SQLALCHEMY_ECHO = True
SQLALCHEMY_RECORD_QUERIES = True

Edit: making queries from the "main" app file shows context, anything outside seems <unknown>

sarimak commented 8 years ago

I am using blueprints too and I had the same issue, the context is "unknown". I am able to get non-empty Context if my WSGI app is wrapped by ReverseProxied middleware (http://flask.pocoo.org/snippets/35/) -- but the Context is then always pointing to the middleware and not the actual code.

Flask (0.11.1) Flask-DebugToolbar (0.10.0) Flask-SQLAlchemy (2.1) SQLAlchemy (1.0.12)

See below an app for reproducing the issue. There are several "tunables" which all are True except "classic" in my production system: reverse_proxied, classic, profiled, blueprinted. Only reverse_proxied seems to have impact on the Context in the debug toolbar.

If I would put all modules into one (mock.py), the Context column would display the correct function and line. So the culprit is perhaps hidden in the module imports -- but I am not able to find it.

mock.py:

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from mock_app import app, db, classic
import mock_view
from mock_model import BaseModel, User

if __name__ == "__main__":
    if classic:
        session = db.session
        meta = db
        args = []
    else:
        engine = create_engine("sqlite:///mock.db")
        session = sessionmaker(bind=engine)()
        meta = BaseModel.metadata
        args = [engine]

    meta.drop_all(*args)
    meta.create_all(*args)
    session.add(User(id=1, name="John"))
    session.commit()

    app.run()

mock_model.py:

from sqlalchemy.ext.declarative import declarative_base

from mock_app import db, classic

if classic:
    BaseModel = db.Model
else:
    BaseModel = declarative_base()

if classic:
    Integer = db.Integer
    Column = db.Column
    String = db.String
else:
    from sqlalchemy import Integer, Column, String

class User(BaseModel):
    __tablename__ = "users"  # Not needed by classic

    id = Column(Integer, primary_key=True)
    name = Column(String)

mock_app.py:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from werkzeug.contrib.profiler import ProfilerMiddleware

reverse_proxied = True  # Context is empty if False and ./mock_app.py:27 (__call__) if True
classic = True  # The Flask-SQLAlchemy way
profiled = False

class ReverseProxied:
    def __init__(self, wsgi_app):
        self.app = wsgi_app

    def __call__(self, environ, start_response):
        script_name = environ.get("HTTP_X_SCRIPT_NAME", "")
        scheme = environ.get("HTTP_X_SCHEME", "")

        if script_name:
            environ["SCRIPT_NAME"] = script_name
            path_info = environ["PATH_INFO"]

            if path_info.startswith(script_name):
                environ["PATH_INFO"] = path_info[len(script_name):]

        if scheme:
            environ["wsgi.url_scheme"] = scheme

        return self.app(environ, start_response)

app = Flask(__name__)
if reverse_proxied:
    app.wsgi_app = ReverseProxied(app.wsgi_app)

if profiled:
    app.config["PROFILE"] = True
    app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[5])

app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///mock.db"
if classic:
    db = SQLAlchemy(app)
else:
    db = SQLAlchemy(session_options={"autocommit": True})
    db.init_app(app)

from flask_sqlalchemy import _EngineDebuggingSignalEvents  # noqa
_EngineDebuggingSignalEvents(db.get_engine(app), app.import_name).register()  # Display SQL queries issued by SQLAlchemy

from flask_debugtoolbar import DebugToolbarExtension

app.debug = True
app.config["SECRET_KEY"] = "123"
app.config["DEBUG_TB_PROFILER_ENABLED"] = True
app.config["SQLALCHEMY_RECORD_QUERIES"] = True
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True
debug_toolbar = DebugToolbarExtension(app)
app.extensions["debugtoolbar"] = debug_toolbar

mock_view.py:

from flask import Blueprint
from mock_app import app, db
from mock_model import User

blueprinted = False

if blueprinted:
    blueprint = Blueprint("blueprint", __name__, template_folder="templates", url_prefix="")
else:
    blueprint = app

@blueprint.route("/")
def hello():
    username = db.session.query(User.name).filter_by(id=1).scalar()
    return "<html><head/><body>Hello {}!</body></html>".format(username)

if blueprinted:
    app.register_blueprint(blueprint)
blurrcat commented 7 years ago

Here's how Flask-SQLAlchemy generates the context(_calling_context): From the most inner frame which issues a db query, it goes upwards to find the frame that matches the "app_path", and set that as the context.

app_path is what you pass to Flask when you create the application. Often times you'd use something like app = Flask(__name__) in your app.py. And this is where things go south: your app_path is now something like project.app, but the frame that issues a db query is probably project.views. _calling_context matches the names by checking name.startwith(app_path + '.'). project.views never passes that check.

To fix this, simply explicitly pass the name of your app to Flask, like app = Flask('project'). Now app_path is project and it can correctly match frames in your code.

sarimak commented 7 years ago

Wow, thank you!

My Flask application resides in a module app.py in a package called the same as my application. The SQLAlchemy queries are located in several modules (using the "gateway" pattern - DB calls/persistence layer are decoupled both from the presentation/view and the business logic) located in sub-packages (=blueprints ="micro-services") of the main application package.

Changing the name of the Flask app from__name__ to "name of application package" did the trick. So hard-coding the top-level package name works regardless of the inner structure of the Flask application and regardless of the WSGI middleware and blueprints.

Could you please update the documentation (especially https://flask-debugtoolbar.readthedocs.io/en/latest/#usage ) with the hard-coded value + could you please add a warning that such approach may be necessary and why? (or even better: could you change the discovery code to be compatible with the default __name__ approach? the __name__ is used even on Flask's homepage, see http://flask.pocoo.org/)