From cfd65a1102f727f4e761298839c15cbef8f213c2 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Tue, 24 Mar 2026 17:51:48 +0100 Subject: [PATCH] Add example of Fast API with postgres database --- test-fastapi-with-database/Dockerfile | 17 +++ test-fastapi-with-database/README.md | 28 ++++ test-fastapi-with-database/docker-compose.yml | 31 ++++ test-fastapi-with-database/entrypoint.sh | 8 + test-fastapi-with-database/main.py | 143 ++++++++++++++++++ test-fastapi-with-database/pyproject.toml | 14 ++ test-fastapi-with-database/run.sh | 4 + 7 files changed, 245 insertions(+) create mode 100644 test-fastapi-with-database/Dockerfile create mode 100644 test-fastapi-with-database/README.md create mode 100644 test-fastapi-with-database/docker-compose.yml create mode 100644 test-fastapi-with-database/entrypoint.sh create mode 100644 test-fastapi-with-database/main.py create mode 100644 test-fastapi-with-database/pyproject.toml create mode 100755 test-fastapi-with-database/run.sh diff --git a/test-fastapi-with-database/Dockerfile b/test-fastapi-with-database/Dockerfile new file mode 100644 index 0000000..8cd74ee --- /dev/null +++ b/test-fastapi-with-database/Dockerfile @@ -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"] diff --git a/test-fastapi-with-database/README.md b/test-fastapi-with-database/README.md new file mode 100644 index 0000000..b63b65e --- /dev/null +++ b/test-fastapi-with-database/README.md @@ -0,0 +1,28 @@ +# test-fastapi-with-database + +## Prerequisites + +- Docker and Docker Compose +- A Sentry DSN + +## Running + +```bash +SENTRY_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. diff --git a/test-fastapi-with-database/docker-compose.yml b/test-fastapi-with-database/docker-compose.yml new file mode 100644 index 0000000..60f34ef --- /dev/null +++ b/test-fastapi-with-database/docker-compose.yml @@ -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 + depends_on: + db: + condition: service_healthy diff --git a/test-fastapi-with-database/entrypoint.sh b/test-fastapi-with-database/entrypoint.sh new file mode 100644 index 0000000..4405aa3 --- /dev/null +++ b/test-fastapi-with-database/entrypoint.sh @@ -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 diff --git a/test-fastapi-with-database/main.py b/test-fastapi-with-database/main.py new file mode 100644 index 0000000..a3f96e5 --- /dev/null +++ b/test-fastapi-with-database/main.py @@ -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], + } diff --git a/test-fastapi-with-database/pyproject.toml b/test-fastapi-with-database/pyproject.toml new file mode 100644 index 0000000..02b85ca --- /dev/null +++ b/test-fastapi-with-database/pyproject.toml @@ -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 } diff --git a/test-fastapi-with-database/run.sh b/test-fastapi-with-database/run.sh new file mode 100755 index 0000000..f9d794c --- /dev/null +++ b/test-fastapi-with-database/run.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +docker compose up --build