Track Errors in Your Python Flask Application with AppSignal

duplxey

Nik Tomazic

Posted on June 12, 2024

Track Errors in Your Python Flask Application with AppSignal

In this article, we'll look at how to track errors in a Flask application using AppSignal.

We'll first bootstrap a Flask project, and install and configure AppSignal. Then, we'll introduce some faulty code and demonstrate how to track and resolve errors using AppSignal's Errors dashboard.

Let's get started!

Prerequisites

Before diving into the article, ensure you have:

Project Setup

To demonstrate how AppSignal error tracking works, we'll create a simple TODO app. The app will provide a RESTful API that supports CRUD operations. Initially, it will contain some faulty code, which we'll address later.

I recommend you first follow along with this exact project since the article is tailored to it. After the article, you'll, of course, be able to integrate AppSignal into your own Flask projects.

Start by bootstrapping a Flask project:

  1. Create and activate a virtual environment
  2. Use pip to install the latest version of Flask
  3. Start the development server

If you get stuck, refer to the Flask Installation guide.

Note: The source code for this project can be found in the appsignal-flask-error-tracking GitHub repo.

Install AppSignal for Flask

To add AppSignal to your Flask project, follow the AppSignal documentation:

  1. AppSignal Python Installation
  2. AppSignal Flask Instrumentation

Ensure everything works by starting the development server:

(venv)$ flask run
Enter fullscreen mode Exit fullscreen mode

Your app should automatically send a demo error to AppSignal. From now on, all your app errors will be forwarded to AppSignal.

If you get an error saying Failed to find Flask application, you most likely imported Flask before starting the AppSignal client. As mentioned in the docs, AppSignal has to be imported and started at the top of app.py.

Flask for Python App Logic

Moving along, let's implement the web app logic.

Flask-SQLAlchemy for the Database

We'll use the Flask-SQLAlchemy package to manage the database. This package provides SQLAlchemy support to Flask projects. That includes the Python SQL toolkit and the ORM.

First, install it via pip:

(venv)$ pip install Flask-SQLAlchemy
Enter fullscreen mode Exit fullscreen mode

Then initialize the database and Flask:

# app.py

db = SQLAlchemy()
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///default.db"
db.init_app(app)
Enter fullscreen mode Exit fullscreen mode

Don't forget about the import:

from flask_sqlalchemy import SQLAlchemy
Enter fullscreen mode Exit fullscreen mode

Next, create the Task database model:

# app.py

class Task(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(128), nullable=False)
    description = db.Column(db.Text(512), nullable=True)

    created_at = db.Column(db.DateTime, default=db.func.now())
    updated_at = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now())

    is_done = db.Column(db.Boolean, default=False)

    def as_dict(self):
        return {c.name: getattr(self, c.name) for c in self.__table__.columns}

    def __repr__(self):
        return f"<Task {self.id}>"
Enter fullscreen mode Exit fullscreen mode

Each Task will have a name, an optional description, an is_done field, and some administrative data. To serialize the Task, we'll use its as_dict() method.

Since Flask-SQLAlchemy doesn't automatically create the database and its structure, we must do it ourselves. To handle that, we'll create a simple Python script.

Create an init_db.py file in the project root with the following content:

# init_db.py

from app import Task
from app import db, app

with app.app_context():
    db.create_all()

    if Task.query.count() == 0:
        tasks = [
            Task(
                name="Deploy App",
                description="Deploy the Flask app to the cloud.",
                is_done=False,
            ),
            Task(
                name="Optimize DB",
                description="Optimize the database access layer.",
                is_done=False,
            ),
            Task(
                name="Install AppSignal",
                description="Install AppSignal to track errors.",
                is_done=False,
            ),
        ]

        for task in tasks:
            db.session.add(task)

        db.session.commit()
Enter fullscreen mode Exit fullscreen mode

What's Happening Here?

This script performs the following:

  1. Fetches Flask's app instance.
  2. Creates the database and its structure via db.create_all().
  3. Populates the database with three sample tasks.
  4. Commits all the changes to the database via db.session.commit().

Defining Views

Define the views in app.py like so:

# app.py

@app.route("/")
def list_view():
    tasks = Task.query.all()
    return jsonify([task.as_dict() for task in tasks])


@app.route("/<int:task_id>", methods=["GET"])
def detail_view(task_id):
    task = db.get_or_404(Task, task_id)
    return jsonify(task.as_dict())


@app.route("/create", methods=["POST"])
def create_view():
    name = request.form.get("name", type=str)
    description = request.form.get("description", type=str)

    task = Task(name=name, description=description)
    db.session.add(task)
    db.session.commit()

    return jsonify(task.as_dict()), 201


@app.route("/toggle-done/<int:task_id>", methods=["PATCH"])
def toggle_done_view(task_id):
    task = db.get_or_404(Task, task_id)

    task.is_done = not task.is_done
    db.session.commit()

    return jsonify(task.as_dict())


@app.route("/delete/<int:task_id>", methods=["DELETE"])
def delete_view(task_id):
    task = db.get_or_404(Task, task_id)

    db.session.delete(task)
    db.session.commit()

    return jsonify({}), 204


@app.route("/statistics", methods=["GET"])
def statistics_view():
    done_tasks_count = Task.query.filter_by(is_done=True).count()
    undone_tasks_count = Task.query.filter_by(is_done=False).count()
    done_percentage = done_tasks_count / (done_tasks_count + undone_tasks_count) * 100

    return jsonify({
        "done_tasks_count": done_tasks_count,
        "undone_tasks_count": undone_tasks_count,
        "done_percentage": done_percentage,
    })
Enter fullscreen mode Exit fullscreen mode

Don't forget about the import:

from flask import jsonify, request
Enter fullscreen mode Exit fullscreen mode

What's Happening Here?

  1. We define six API endpoints.
  2. The list_view() fetches all the tasks, serializes and returns them.
  3. The detail_view() fetches a specific task, serializes and returns it.
  4. The create_view() creates a new task from the provided data.
  5. toggle_done_view() toggles the task's is_done property.
  6. The delete_view() deletes a specific task.
  7. The statistics_view() calculates general app statistics.

Great, we've successfully created a simple TODO web app!

Test Your Python Flask App's Errors with AppSignal

During the development of our web app, we intentionally left in some faulty code. We'll now trigger these bugs to see what happens when an error occurs.

Before proceeding, ensure your Flask development server is running:

(venv)$ flask run --debug
Enter fullscreen mode Exit fullscreen mode

Your API should be accessible at http://localhost:5000/.

AppSignal should, of course, be employed when your application is in production rather than during development, as shown in this article.

Error 1: OperationalError

To trigger the first error, request the task list:

$ curl --location 'localhost:5000/'
Enter fullscreen mode Exit fullscreen mode

This will return an Internal Server Error. Let's use AppSignal to figure out what went wrong.

Open your favorite web browser and navigate to your AppSignal dashboard. Select your organization and then your application. Lastly, choose "Errors > Issue list" on the sidebar:

AppSignal Errors Issue List

You'll see that an OperationalError was reported. Click on it to inspect it:

AppSignal Errors Issue Details

The error detail page will display the error message, backtrace, state, trends, and so on.

We can figure out what went wrong just by looking at the error message. no such table: task tells us that we forgot to initialize the database.

To fix that, run the previously created script:

(venv)$ python init_db.py
Enter fullscreen mode Exit fullscreen mode

Retest the app and mark the issue as "Closed" once you've verified everything works.

AppSignal Errors Issue Tag Closed

Error 2: IntegrityError

Let's trigger the next error by trying to create a task without a name:

$ curl --location 'localhost:5000/create' \
       --form 'description="Test the web application."'
Enter fullscreen mode Exit fullscreen mode

Open the AppSignal dashboard and navigate to the IntegrityError's details.

Now instead of just checking the error message, select "Samples" in the navigation:

AppSignal Errors Samples

A sample refers to a recorded instance of a specific error. Select the first sample.

AppSignal Errors Sample Details

By checking the backtrace, we can see exactly what line caused the error. As you can see, the error happened in app.py on line 53 when we tried saving the task to the database.

To fix it, provide a default when assigning the name variable:

# app.py

@app.route("/create", methods=["POST"])
def create_view():
    name = request.form.get("name", type=str, default="Unnamed Task")  # new
    description = request.form.get("description", type=str, default="")

    task = Task(name=name, description=description)
    db.session.add(task)
    db.session.commit()

    return jsonify(task.as_dict()), 201
Enter fullscreen mode Exit fullscreen mode

Error 3: ZeroDivisionError

Now we'll delete all tasks and then calculate the statistics by running the following commands:

$ curl --location --request DELETE 'localhost:5000/delete/1'
$ curl --location --request DELETE 'localhost:5000/delete/2'
$ curl --location --request DELETE 'localhost:5000/delete/3'
$ curl --location --request DELETE 'localhost:5000/delete/4'
$ curl --location --request DELETE 'localhost:5000/delete/5'
$ curl --location 'localhost:5000/statistics'
Enter fullscreen mode Exit fullscreen mode

As expected, a ZeroDivisonError is raised. To track the error, follow the same approach as described in the previous section.

AppSignal ZeroDivisionError Sample

To fix it, add a zero check to the statistics_view() endpoint like so:

# app.py

@app.route("/statistics", methods=["GET"])
def statistics_view():
    done_tasks_count = Task.query.filter_by(is_done=True).count()
    undone_tasks_count = Task.query.filter_by(is_done=False).count()

    # new
    if done_tasks_count + undone_tasks_count == 0:
        done_percentage = 0
    else:
        done_percentage = done_tasks_count / \
                          (done_tasks_count + undone_tasks_count) * 100

    return jsonify({
        "done_tasks_count": done_tasks_count,
        "undone_tasks_count": undone_tasks_count,
        "done_percentage": done_percentage,
    })
Enter fullscreen mode Exit fullscreen mode

Retest the endpoint and mark it as "Closed" once you've verified the issue has been resolved.

Manual Tracking

By default, errors are only reported to AppSignal when exceptions are left unhandled. However, in some cases, you may want handled exceptions to be reported.

To accomplish this, you can utilize AppSignal's helper methods:

  1. set_error()
  2. send_error() and send_error_with_context()

Wrapping Up

In this article, we've covered how to monitor errors in a Flask app using AppSignal.

We explored two error reporting methods: automatic tracking and manual tracking (using helper methods). With this knowledge, you can easily incorporate AppSignal into your Flask projects.

Happy coding!

P.S. If you'd like to read Python posts as soon as they get off the press, subscribe to our Python Wizardry newsletter and never miss a single post!

๐Ÿ’– ๐Ÿ’ช ๐Ÿ™… ๐Ÿšฉ
duplxey
Nik Tomazic

Posted on June 12, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

ยฉ TheLazy.dev

About