Track Errors in FastAPI for Python with AppSignal

jangia

Jan Giacomelli

Posted on April 17, 2024

Track Errors in FastAPI for Python with AppSignal

When you first try a new library or framework, you are excited about it. However, as soon as you run something on production, things are less than ideal — an error here, an exception there - bugs everywhere! You start reading your logs, but you often lack context, like how often an error happens, in what line, etc.

Fortunately, tools such as AppSignal can help. AppSignal helps you track your errors and gives you a lot of valuable insights. For example, you can very quickly see how often an error happens, in what line, and for which deployment.

That's what we'll do now: leverage AppSignal to track errors in a FastAPI application.

Setting Up Our FastAPI for Python Project

Let's prepare a project we'll use as an example. First, create a new directory and virtual environment:

$ mkdir fastapi_appsignal
$ cd fastapi_appsignal
$ python3.12 -m venv venv
$ source venv/bin/activate
Enter fullscreen mode Exit fullscreen mode

Second, install FastAPI:

$ pip install "fastapi[all]"
Enter fullscreen mode Exit fullscreen mode

Third, add a new file called main.py with the following contents:

import uuid
from uuid import UUID

from fastapi import FastAPI
from pydantic import BaseModel


class TodoData(BaseModel):
    title: str
    done: bool = False


class Todo(BaseModel):
    id: UUID
    title: str
    done: bool = False


TODOS = []
app = FastAPI()


@app.get("/todos")
def todo_list() -> list[Todo]:
    return TODOS


@app.post("/todos")
def todo_create(data: TodoData) -> Todo:
    todo = Todo(id=uuid.uuid4(), title=data.title, done=data.done)
    TODOS.append(todo)
    return todo


@app.post("/todos/{todo_id}")
def todo_edit(todo_id: UUID, data: TodoData) -> Todo:
    todo = next((t for t in TODOS if t.id == todo_id))
    todo.title = data.title
    todo.done = data.done
    return todo
Enter fullscreen mode Exit fullscreen mode

What we have here is a very simple API example for managing TODOs. We'll use this API to demonstrate how to track errors with AppSignal.

Configure AppSignal with FastAPI

If you don't have an AppSignal account, create one - AppSignal offers a 30 day free trial.

First, we need to install appsignal and opentelemetry-instrumentation-fastapi:

$ pip install appsignal
$ pip install opentelemetry-instrumentation-fastapi
Enter fullscreen mode Exit fullscreen mode

Second, let's configure AppSignal for FastAPI. Create a new file called __appsignal__.py with the following contents:

import os

from appsignal import Appsignal

appsignal = Appsignal(
    active=True,
    name="fastapi_appsignal",
    push_api_key=os.getenv("APPSIGNAL_PUSH_API_KEY"),
)
Enter fullscreen mode Exit fullscreen mode

Third, update the main.py file:

import uuid
from uuid import UUID

from fastapi import FastAPI
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor  # new
from pydantic import BaseModel

from __appsignal__ import appsignal  # new

appsignal.start()  # new


class TodoData(BaseModel):
    title: str
    done: bool = False


class Todo(BaseModel):
    id: UUID
    title: str
    done: bool = False


TODOS = []
app = FastAPI()


@app.get("/todos")
def todo_list() -> list[Todo]:
    return TODOS


@app.post("/todos")
def todo_create(data: TodoData) -> Todo:
    todo = Todo(id=uuid.uuid4(), title=data.title, done=data.done)
    TODOS.append(todo)
    return todo


@app.post("/todos/{todo_id}")
def todo_edit(todo_id: UUID, data: TodoData) -> Todo:
    todo = next((t for t in TODOS if t.id == todo_id))
    todo.title = data.title
    todo.done = data.done
    return todo


FastAPIInstrumentor().instrument_app(app)  # new
Enter fullscreen mode Exit fullscreen mode

We need to start the AppSignal agent alongside our FastAPI application. For more details, refer to the AppSignal Python installation docs and FastAPI instrumentation docs.

At this point, our project is ready to track our first error with AppSignal. To send errors to AppSignal, you need to get your push API key. You can get one by following the official AppSignal guide for adding a new app. After you have your push API key, start the server:

$ export APPSIGNAL_PUSH_API_KEY=<your_appsignal_push_api_key>
$ uvicorn main:app --reload
Enter fullscreen mode Exit fullscreen mode

Now your server is up and running, open a new terminal and send a request to the API:

$ curl -X POST -H "Content-Type: application/json" -d '{"title": "Buy milk", "done": true}' http://localhost:8000/todos/dde18df8-7e11-4f84-bcbf-2eee1a5d157e
Enter fullscreen mode Exit fullscreen mode

This request will raise the StopIteration exception inside the todo_edit function. That's because we use the next function to get the TODO with the given ID. The exception is raised since we're using the UUID of a non-existing task.

To confirm that AppSignal is working, click on Next Step at the bottom of the page from which you've copied the push API key.
Next Step

Once you've clicked on Next Step, wait for AppSignal to confirm that it has received data from your app. After this, you can open the Errors -> Issue list tab and see the error we've just raised.
Issue List

Congrats! You've just tracked your first error in FastAPI with AppSignal. But we can do better than that. Let's see how we can get more insights into our errors.

Get More Error Insights

At this point, we're able to track errors with AppSignal. On the dashboard, you can see:

  • How often an error happens
  • Error messages
  • Error backtraces

You can also add notes to errors, assign errors, and post messages to Slack when errors occur.

To get even more insights, you can set up deploy markers and backtrace links.

Using them together lets you jump directly to the source code on your GitHub repository from the error's backtrace. For this to work, you need to push your code to GitHub. Do that before going further (you can follow the official guide). Once this is done, we can continue setting up deploy markers and backtrace links.

Deploy Markers

First, we need to set up deploy markers. To do that, update the __appsignal__.py file:

import os

from appsignal import Appsignal

appsignal = Appsignal(
    active=True,
    name="fastapi_appsignal",
    push_api_key=os.getenv("APPSIGNAL_PUSH_API_KEY"),
    revision=os.getenv("APPSIGNAL_REVISION"),  # new
)
Enter fullscreen mode Exit fullscreen mode

We enable deploy markers by setting the revision value to a non-null value. Setting up deploy markers will allow us to see which deployment causes an error. We'll set APPSIGNAL_REVISION to main because that's the name of our branch. In the production setup, you should set this to the commit hash of the deployed version or tag name.

Second, we need to enable backtrace links. We already enabled deploy markers, so we're halfway there. What's left is to link our GitHub repository. To do that, go to your organization's settings and click on Install GitHub App.
Install GitHub App

Once on GitHub, select your organization.
Select Organization

After that, keep everything as it is and click Install.
Install App

You'll be redirected back to AppSignal's dashboard. Select the repository you want to link to your app and click Save repository selection.

Select Repository

Let's raise another error to test that everything is working as expected. To do that, set environment variables and start your server again:

$ export APPSIGNAL_PUSH_API_KEY=<your_appsignal_push_api_key>
$ export APPSIGNAL_REVISION=main
$ uvicorn main:app --reload
Enter fullscreen mode Exit fullscreen mode

After that, open a new terminal and send another request to the API:

$ curl -X POST -H "Content-Type: application/json" -d '{"title": "Buy milk", "done": true}' http://localhost:8000/todos/dde18df8-7e11-4f84-bcbf-2eee1a5d157e
Enter fullscreen mode Exit fullscreen mode

Backtrace Links

Once the latest error is tracked, you can try backtrace links. Go to the Errors -> Issue list tab and click on the error we've just raised. Once on the error's detail page, click _Inspect latest example.

Inspect Latest Example

You'll end up on the details of the latest error sample, where you'll see the Backtrace section. On the right side of the backtrace, you'll see a link to the source code on GitHub - git.

Backtrace link

When you click on git, you'll be redirected to the source code on GitHub. You'll see the exact line where the error happened. Now, you can easily pinpoint the problematic code and fix the issue. This is way less work than parsing log files and doing things manually.

Note: If you're not pointed to the correct line of code on GitHub, make sure you've committed and pushed the latest version of your code to GitHub.

Reporting Python FastAPI Errors Explicitly

So far, we've seen how to track errors raised by our FastAPI application. But we might want to report errors explicitly in some cases. For example, we might use an AI API to generate emails for our clients.

As you may know, these APIs are often unreliable, and seeing internal server errors is quite common. In such cases, we want to handle the error - for example, by falling back to a default template.

We want to report an error to AppSignal to see how often it happens and what the error message is.

Using set_error

In some cases, we can make usage more robust (e.g., by implementing retries) or contact the API provider's support.
To handle such scenarios, AppSignal provides the set_error function. Let's see how to use it.

Update your main.py file:

# ... existing imports
from appsignal import set_error  # new
from fastapi import FastAPI, HTTPException  # new
# ... existing imports


# ... existing code

@app.post("/todos/{todo_id}")
def todo_edit(todo_id: UUID, data: TodoData) -> Todo:
    try:  # new
        todo = next((t for t in TODOS if t.id == todo_id))
    except StopIteration:
        set_error(Exception("Todo not found"))
        raise HTTPException(status_code=404, detail="Todo not found")
    todo.title = data.title
    todo.done = data.done
    return todo


# ... existing code
Enter fullscreen mode Exit fullscreen mode

In our case, users won't see the Internal server error anymore. Instead, we'll return the Todo not found error.
That's better for our users and our API. Without set_error, nothing would be reported to AppSignal. With set_error, the error is reported and tracked as before. Hence, we'll see how often this error happens and the error message. Backtrace links still apply, as in the previous example.

To test that everything is working as expected, follow the same steps as above:

  1. Start the server.
  2. Send a request to the API.
  3. Go to the dashboard Errors -> Issue list in AppSignal and check the new error.

That's all great, but sometimes, we need more context when manually reporting an error to AppSignal.
In such cases, we can use the send_error_with_context function. This function allows us to manually add needed context to the error - e.g., the ID of a non-existing task.
Once again, update the main.py file:

# ... existing imports
from appsignal import send_error_with_context, set_params  # new
from fastapi import FastAPI, HTTPException  # new
# ... existing imports


# ... existing code

@app.post("/todos/{todo_id}")
def todo_edit(todo_id: UUID, data: TodoData) -> Todo:
    try:
        todo = next((t for t in TODOS if t.id == todo_id))
    except StopIteration:
        with send_error_with_context(Exception("Todo not found")):
            set_params({"todo_id": todo_id})
        raise HTTPException(status_code=404, detail="Todo not found")
    todo.title = data.title
    todo.done = data.done
    return todo



# ... existing code
Enter fullscreen mode Exit fullscreen mode

To test that everything works as expected, follow the same steps as above.

In this case, you'll also see the Parameters section that contains the parameters you've explicitly set (here, that's the ID of the non-existing task).
Error with context

And that's it!

Wrapping Up

In this blog post, we've seen how to track errors in FastAPI with AppSignal. We set up AppSignal for FastAPI, tracked errors, and gained some further insights.

Besides that, we also learned how to explicitly track errors using the AppSignal functions set_error and send_error_with_context. This should give you a good starting point to track your errors better and fix them faster from now on.

Happy engineering!

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!

💖 💪 🙅 🚩
jangia
Jan Giacomelli

Posted on April 17, 2024

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

Sign up to receive the latest update from our blog.

Related