-
Notifications
You must be signed in to change notification settings - Fork 1
Add example of Fast API with postgres database #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| FROM python:3.12-slim | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| # Install uv | ||
| COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ | ||
|
|
||
| # Copy project files | ||
| COPY pyproject.toml . | ||
| COPY main.py . | ||
|
|
||
| # Dependencies are installed at startup via entrypoint because | ||
| # sentry-python is mounted as a volume (not available at build time). | ||
| COPY entrypoint.sh . | ||
| RUN chmod +x entrypoint.sh | ||
|
|
||
| CMD ["./entrypoint.sh"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # test-fastapi-with-database | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - Docker and Docker Compose | ||
| - A Sentry DSN | ||
|
|
||
| ## Running | ||
|
|
||
| ```bash | ||
| SENTRY_DSN=<your-dsn> ./run.sh | ||
| ``` | ||
|
|
||
| This starts two containers: | ||
| - **db**: PostgreSQL 16 with a `test_multiline` database | ||
| - **app**: FastAPI server on port 5000, using a local editable install of `sentry-python` (mounted from `../../sentry-python`) | ||
|
|
||
| On first startup, the app creates `users` and `posts` tables and seeds sample data. | ||
|
|
||
| ## Reproducing the issue | ||
|
|
||
| 1. Hit the query endpoint: | ||
|
|
||
| ```bash | ||
| curl http://localhost:5000/query | ||
| ``` | ||
|
|
||
| 2. Watch the app container logs for the `[before_send_transaction]` output. The callback attempts to match a multiline SQL string against span descriptions but fails to find a match. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| services: | ||
| db: | ||
| image: postgres:16 | ||
| environment: | ||
| POSTGRES_USER: postgres | ||
| POSTGRES_PASSWORD: postgres | ||
| POSTGRES_DB: test_multiline | ||
| ports: | ||
| - "5432:5432" | ||
| healthcheck: | ||
| test: ["CMD-SHELL", "pg_isready -U postgres"] | ||
| interval: 2s | ||
| timeout: 5s | ||
| retries: 5 | ||
|
|
||
| app: | ||
| build: . | ||
| ports: | ||
| - "5000:5000" | ||
| environment: | ||
| DATABASE_HOST: db | ||
| DATABASE_USER: postgres | ||
| DATABASE_PASSWORD: postgres | ||
| DATABASE_NAME: test_multiline | ||
| SENTRY_DSN: ${SENTRY_DSN:-} | ||
| ENV: local | ||
| volumes: | ||
| - ~/sentry-python:/sentry-python | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The Suggested FixUpdate the volume mount in Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. |
||
| depends_on: | ||
| db: | ||
| condition: service_healthy | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| #!/usr/bin/env bash | ||
| set -e | ||
|
|
||
| # Install dependencies (sentry-python is mounted as a volume at /sentry-python) | ||
| uv sync --no-dev | ||
|
|
||
| # Start the app | ||
| uv run uvicorn main:app --host 0.0.0.0 --port 5000 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| import os | ||
|
|
||
| import asyncpg | ||
| import sentry_sdk | ||
| from fastapi import FastAPI | ||
| from sentry_sdk.integrations.asyncpg import AsyncPGIntegration | ||
| from sentry_sdk.integrations.fastapi import FastApiIntegration | ||
| from sentry_sdk.integrations.starlette import StarletteIntegration | ||
| import logging | ||
|
|
||
| logging.basicConfig(level=logging.INFO) | ||
| logger = logging.getLogger(__name__) | ||
|
|
||
| DATABASE_USER = os.environ.get("DATABASE_USER", "postgres") | ||
| DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD", "postgres") | ||
| DATABASE_HOST = os.environ.get("DATABASE_HOST", "localhost") | ||
| DATABASE_PORT = os.environ.get("DATABASE_PORT", "5432") | ||
| DATABASE_NAME = os.environ.get("DATABASE_NAME", "test_multiline") | ||
|
|
||
| DATABASE_URL = f"postgresql://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}" | ||
|
|
||
|
|
||
| # This multiline string simulates what a user would copy from the Sentry UI | ||
| # "Traces" page and paste into their before_send_transaction callback. | ||
| # The Sentry UI displays the span description (the SQL query) with its | ||
| # original newlines/whitespace intact. | ||
| QUERY_TO_FILTER = "SELECT u.id, u.name, u.email, p.title AS post_title, p.created_at AS post_date FROM users u JOIN posts p ON u.id = p.user_id WHERE u.active = true ORDER BY p.created_at DESC" | ||
|
|
||
| def before_send_transaction(event, hint): | ||
| """ | ||
| User's before_send_transaction callback that attempts to filter out | ||
| transactions containing a specific multiline SQL query. | ||
|
|
||
| The user copied the query string from the Sentry UI Traces page | ||
| and wants to drop transactions that contain this query as a span. | ||
| """ | ||
| for span in event.get("spans", []): | ||
| description = span.get("description", "") | ||
| if description and QUERY_TO_FILTER in description: | ||
| logger.info(f"[before_send_transaction] MATCH FOUND - dropping transaction") | ||
| return None | ||
|
|
||
| logger.info(f"[before_send_transaction] No match found - keeping transaction") | ||
| return event | ||
|
|
||
|
|
||
| sentry_sdk.init( | ||
| dsn=os.environ.get("SENTRY_DSN"), | ||
| environment=os.environ.get("ENV", "local"), | ||
| traces_sample_rate=1.0, | ||
| debug=True, | ||
| integrations=[ | ||
| StarletteIntegration(), | ||
| FastApiIntegration(), | ||
| AsyncPGIntegration(), | ||
| ], | ||
| before_send_transaction=before_send_transaction, | ||
| ) | ||
|
|
||
| app = FastAPI() | ||
|
|
||
|
|
||
| @app.on_event("startup") | ||
| async def startup(): | ||
| app.state.pool = await asyncpg.create_pool(DATABASE_URL) | ||
|
|
||
| # Create tables if they don't exist | ||
| async with app.state.pool.acquire() as conn: | ||
| await conn.execute(""" | ||
| CREATE TABLE IF NOT EXISTS users ( | ||
| id SERIAL PRIMARY KEY, | ||
| name TEXT NOT NULL, | ||
| email TEXT NOT NULL, | ||
| active BOOLEAN DEFAULT true | ||
| ) | ||
| """) | ||
| await conn.execute(""" | ||
| CREATE TABLE IF NOT EXISTS posts ( | ||
| id SERIAL PRIMARY KEY, | ||
| user_id INTEGER REFERENCES users(id), | ||
| title TEXT NOT NULL, | ||
| created_at TIMESTAMP DEFAULT NOW() | ||
| ) | ||
| """) | ||
|
|
||
| # Seed some data if tables are empty | ||
| count = await conn.fetchval("SELECT COUNT(*) FROM users") | ||
| if count == 0: | ||
| await conn.execute(""" | ||
| INSERT INTO users (name, email, active) VALUES | ||
| ('Alice', 'alice@example.com', true), | ||
| ('Bob', 'bob@example.com', true), | ||
| ('Charlie', 'charlie@example.com', false) | ||
| """) | ||
| await conn.execute(""" | ||
| INSERT INTO posts (user_id, title) VALUES | ||
| (1, 'First Post'), | ||
| (1, 'Second Post'), | ||
| (2, 'Hello World') | ||
| """) | ||
|
|
||
|
|
||
| @app.on_event("shutdown") | ||
| async def shutdown(): | ||
| await app.state.pool.close() | ||
|
|
||
|
|
||
| @app.get("/") | ||
| async def root(): | ||
| return { | ||
| "endpoints": { | ||
| "multiline_query": "http://localhost:5000/query", | ||
| } | ||
| } | ||
|
|
||
|
|
||
| @app.get("/query") | ||
| async def multiline_query(): | ||
| """ | ||
| Executes a multiline SQL query against the database. | ||
| The span description captured by Sentry will contain the query | ||
| with its original newlines. | ||
| """ | ||
| async with app.state.pool.acquire() as conn: | ||
| rows = await conn.fetch("""SELECT | ||
| u.id, | ||
| u.name, | ||
| u.email, | ||
| p.title AS post_title, | ||
| p.created_at AS post_date | ||
| FROM | ||
| users u | ||
| JOIN | ||
| posts p ON u.id = p.user_id | ||
| WHERE | ||
| u.active = true | ||
| ORDER BY | ||
| p.created_at DESC""") | ||
|
|
||
| return { | ||
| "count": len(rows), | ||
| "results": [dict(r) for r in rows], | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| [project] | ||
| name = "test-fastapi-with-database" | ||
| version = "0" | ||
| requires-python = ">=3.12" | ||
|
|
||
| dependencies = [ | ||
| "fastapi>=0.115.11", | ||
| "asyncpg>=0.30.0", | ||
| "sentry-sdk[fastapi,asyncpg]", | ||
| "uvicorn>=0.34.0", | ||
| ] | ||
|
|
||
| [tool.uv.sources] | ||
| sentry-sdk = { path = "/sentry-python", editable = true } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| docker compose up --build |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Volume mount path inconsistent with repo convention
Medium Severity
The volume mount in
docker-compose.ymluses~/sentry-python:/sentry-python, but the README states it's "mounted from../../sentry-python". Every other test app in this repository references sentry-python at../../sentry-python(a relative path two directories up). The~/sentry-pythonpath assumes the repo is cloned directly in the user's home directory, which won't be the case for most developers. This will cause the container to start with an empty or missing/sentry-pythonvolume, anduv syncwill fail.Additional Locations (1)
test-fastapi-with-database/README.md#L15-L16