Track Errors in FastAPI for Python with AppSignal
Tags: fastapi, python, errors
Published: 2024-04-17 11:08:53

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:

```shell
$ mkdir fastapi_appsignal
$ cd fastapi_appsignal
$ python3.12 -m venv venv
$ source venv/bin/activate
```

Second, install FastAPI:

```shell
$ pip install "fastapi[all]"
```

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

```python
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
```

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](https://appsignal.com/users/sign_up)._

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

```shell
$ pip install appsignal
$ pip install opentelemetry-instrumentation-fastapi
```

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

```python
import os

from appsignal import Appsignal

appsignal = Appsignal(
    active=True,
    name="fastapi_appsignal",
    push_api_key=os.getenv("APPSIGNAL_PUSH_API_KEY"),
)
```

<Banner lang="python" />

Third, update the `main.py` file:

```python
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
```

We need to start the AppSignal agent alongside our FastAPI application. For more details, refer to the [AppSignal Python installation docs](https://docs.appsignal.com/python/installation) and [FastAPI instrumentation docs](https://docs.appsignal.com/python/instrumentations/fastapi.html).

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](https://docs.appsignal.com/guides/new-application.html) for adding a new app. After you have your push API key, start the server:

```shell
$ export APPSIGNAL_PUSH_API_KEY=<your_appsignal_push_api_key>
$ uvicorn main:app --reload
```

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
```

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](https://blog.appsignal.com/images/blog/2024-03/next_step.png)

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](https://blog.appsignal.com/images/blog/2024-03/issue_list.png)

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](https://docs.appsignal.com/application/markers/deploy-markers.html) and [backtrace links](https://docs.appsignal.com/application/backtrace-links.html).

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](https://docs.github.com/en/migrations/importing-source-code/using-the-command-line-to-import-source-code/adding-locally-hosted-code-to-github#adding-a-local-repository-to-github-using-git)). 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:

```python
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
)
```

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](https://blog.appsignal.com/images/blog/2024-03/install_github_app.png)

Once on GitHub, select your organization.
![Select Organization](https://blog.appsignal.com/images/blog/2024-03/select_github_organization.png)

After that, keep everything as it is and click _Install_.
![Install App](https://blog.appsignal.com/images/blog/2024-03/install_app.png)

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](https://blog.appsignal.com/images/blog/2024-03/select_repository_link.png)

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

```shell
$ export APPSIGNAL_PUSH_API_KEY=<your_appsignal_push_api_key>
$ export APPSIGNAL_REVISION=main
$ uvicorn main:app --reload
```

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
```

### 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](https://blog.appsignal.com/images/blog/2024-03/inspect_latest_example.png)

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](https://blog.appsignal.com/images/blog/2024-03/backtrace_link.png)

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:

```python
# ... 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
```

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.
1. Send a request to the API.
1. 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:

```python
# ... 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
```

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](https://blog.appsignal.com/images/blog/2024-03/error_with_context.png)

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](/python-wizardry)!**