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
Second, install FastAPI:
$ pip install "fastapi[all]"
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
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
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"), )
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
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
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.
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.
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 )
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.
Once on GitHub, select your organization.
After that, keep everything as it is and click Install.
You'll be redirected back to AppSignal's dashboard. Select the repository you want to link to your app and click Save repository selection.
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
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.
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.
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
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:
- Start the server.
- Send a request to the API.
- 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
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).
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!