Exercise: Logging and Migrations
You'll:
- fill in the
Taskschema variants inmodels.py - generate and apply an alembic migration that creates the
taskstable - run the server and confirm the
log_requestsmiddleware is emitting the timing line
The logging setup (the LOG_FORMAT constant, the configure_logging helper, the log_requests middleware, and the IntegrityError warning) is already there in the branch for this chapter.
Before You Start
Set up the starting state for this exercise:
git fetch
git checkout -b my-ch08 --no-track origin/ch08
uv sync
Files that changed in this chapter:
src/release_tracker/config.py(modified, addsdebug,LOG_FORMAT,configure_logging)src/release_tracker/main.py(modified, callsconfigure_logging, addslog_requestsmiddleware, adds anIntegrityErrorwarning log)src/release_tracker/models.py(modified, addsTaskStatus,TaskPriority,TaskTitle, theTasktable model, and theProject.tasksrelationship; you'll fill in theTaskBasefields and the schema variants)
Postgres running?
Confirm Postgres is up before you start:
docker compose ps
The db service should show as healthy. If it isn't, start it with docker compose up -d.
Step 1: Fill In the Task Models
Open src/release_tracker/models.py. Three classes have TODO comments where you fill in the field declarations: TaskBase, TaskUpdate, and TaskRead. The imports, the enums, the TaskTitle annotation, and the Task table model are all provided.
class TaskBase(SQLModel):
# TODO: Define the shared fields for a task.
#
# A task has a title, optional details, a status, a priority,
# and an optional due date.
#
# Use TaskTitle for the title (same idea as ProjectName above).
# Use the TaskStatus and TaskPriority enums for status and priority.
# Pick sensible defaults: a new task starts as "planned" with "medium"
# priority.
#
# Look at ProjectBase for how required fields, optional fields, and
# fields with defaults are declared.
pass
class Task(TaskBase, table=True):
__tablename__ = "tasks"
id: int | None = Field(default=None, primary_key=True)
project_id: int = Field(foreign_key="projects.id", index=True)
project: Project = Relationship(back_populates="tasks")
class TaskCreate(TaskBase):
pass
class TaskUpdate(SQLModel):
# TODO: The update schema for PATCH requests.
#
# PATCH: a client sends only the fields it wants to change.
# Fields not included in the request body stay untouched.
#
# What should the defaults be?
# Compare with ProjectUpdate above.
pass
class TaskRead(TaskBase):
# TODO: The read schema adds server-generated fields that don't exist
# at creation time but need to appear in API responses.
#
# Which fields are in Task table model have not in TaskBase?
# Compare with ProjectRead above.
pass
Step 2: Generate the Migration
Run autogenerate from the project root. Substitute your own message if you'd like:
uv run alembic revision --autogenerate -m "add tasks table"
A new file appears in alembic/versions/. The filename is <revision-id>_add_tasks_table.py. Open it. The upgrade function should:
- create the
taskstable withtitle,details,status,priority,due_date,id, andproject_idcolumns - declare a foreign key constraint from
project_idtoprojects.id - create an index on
project_id
If anything is missing, double-check that models.py is saved and that TaskBase has all five fields.
Step 3: Apply the Migration
uv run alembic upgrade head
Confirm the database is at the new revision:
uv run alembic current
The output should print the revision ID from your generated file.
Step 4: Trigger a Log Line
Start the server in dev mode:
uv run fastapi dev
In a browser, open http://localhost:8000/docs and execute GET /projects/.
In the terminal where the server is running, you should see lines that look like this:
2026-05-06 09:12:34,567 INFO release_tracker.main GET /projects/ completed in 12.34ms with status code 200
If DEBUG=true is set in your .env, the level on the first column will read DEBUG and additional DEBUG-level lines from the framework will appear. With no .env (or DEBUG=false), INFO is the floor.
Success
If the migration applied cleanly and the timing line shows up in the terminal, the Task model is wired into the database and the request middleware is doing its job.
Troubleshooting Tips
-
alembic revision --autogenerateproduces an empty migration:alembic/env.pymay not be importingrelease_tracker.models. Open it and confirm thefrom release_tracker import models # noqa: F401line is present. -
No log output at all when hitting an endpoint:
Confirm
configure_logging(debug=get_settings().debug)runs at the top ofmain.py. If it's missing, the root logger has no handlers andlogger.info(...)calls go nowhere. -
The
taskstable didn't get created:Run
uv run alembic currentto see what revision the database thinks it's on. If it's still on the initial revision, the upgrade didn't run; re-runuv run alembic upgrade headand watch for errors.
Compare with the Solution
git diff solutions/ch08 -- src/