Skip to content

Exercise: Logging and Migrations

You'll:

  • fill in the Task schema variants in models.py
  • generate and apply an alembic migration that creates the tasks table
  • run the server and confirm the log_requests middleware 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:

shell
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, adds debug, LOG_FORMAT, configure_logging)
  • src/release_tracker/main.py (modified, calls configure_logging, adds log_requests middleware, adds an IntegrityError warning log)
  • src/release_tracker/models.py (modified, adds TaskStatus, TaskPriority, TaskTitle, the Task table model, and the Project.tasks relationship; you'll fill in the TaskBase fields and the schema variants)

Postgres running?

Confirm Postgres is up before you start:

shell
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.

src/release_tracker/models.py
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:

shell
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 tasks table with title, details, status, priority, due_date, id, and project_id columns
  • declare a foreign key constraint from project_id to projects.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

shell
uv run alembic upgrade head

Confirm the database is at the new revision:

shell
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:

shell
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:

Text
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 --autogenerate produces an empty migration:

    alembic/env.py may not be importing release_tracker.models. Open it and confirm the from release_tracker import models # noqa: F401 line is present.

  • No log output at all when hitting an endpoint:

    Confirm configure_logging(debug=get_settings().debug) runs at the top of main.py. If it's missing, the root logger has no handlers and logger.info(...) calls go nowhere.

  • The tasks table didn't get created:

    Run uv run alembic current to see what revision the database thinks it's on. If it's still on the initial revision, the upgrade didn't run; re-run uv run alembic upgrade head and watch for errors.

Compare with the Solution

shell
git diff solutions/ch08 -- src/