Skip to content

Commit cfd65a1

Browse files
committed
Add example of Fast API with postgres database
1 parent 9de6615 commit cfd65a1

File tree

7 files changed

+245
-0
lines changed

7 files changed

+245
-0
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
FROM python:3.12-slim
2+
3+
WORKDIR /app
4+
5+
# Install uv
6+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
7+
8+
# Copy project files
9+
COPY pyproject.toml .
10+
COPY main.py .
11+
12+
# Dependencies are installed at startup via entrypoint because
13+
# sentry-python is mounted as a volume (not available at build time).
14+
COPY entrypoint.sh .
15+
RUN chmod +x entrypoint.sh
16+
17+
CMD ["./entrypoint.sh"]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# test-fastapi-with-database
2+
3+
## Prerequisites
4+
5+
- Docker and Docker Compose
6+
- A Sentry DSN
7+
8+
## Running
9+
10+
```bash
11+
SENTRY_DSN=<your-dsn> ./run.sh
12+
```
13+
14+
This starts two containers:
15+
- **db**: PostgreSQL 16 with a `test_multiline` database
16+
- **app**: FastAPI server on port 5000, using a local editable install of `sentry-python` (mounted from `../../sentry-python`)
17+
18+
On first startup, the app creates `users` and `posts` tables and seeds sample data.
19+
20+
## Reproducing the issue
21+
22+
1. Hit the query endpoint:
23+
24+
```bash
25+
curl http://localhost:5000/query
26+
```
27+
28+
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.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
services:
2+
db:
3+
image: postgres:16
4+
environment:
5+
POSTGRES_USER: postgres
6+
POSTGRES_PASSWORD: postgres
7+
POSTGRES_DB: test_multiline
8+
ports:
9+
- "5432:5432"
10+
healthcheck:
11+
test: ["CMD-SHELL", "pg_isready -U postgres"]
12+
interval: 2s
13+
timeout: 5s
14+
retries: 5
15+
16+
app:
17+
build: .
18+
ports:
19+
- "5000:5000"
20+
environment:
21+
DATABASE_HOST: db
22+
DATABASE_USER: postgres
23+
DATABASE_PASSWORD: postgres
24+
DATABASE_NAME: test_multiline
25+
SENTRY_DSN: ${SENTRY_DSN:-}
26+
ENV: local
27+
volumes:
28+
- ~/sentry-python:/sentry-python
29+
depends_on:
30+
db:
31+
condition: service_healthy
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env bash
2+
set -e
3+
4+
# Install dependencies (sentry-python is mounted as a volume at /sentry-python)
5+
uv sync --no-dev
6+
7+
# Start the app
8+
uv run uvicorn main:app --host 0.0.0.0 --port 5000

test-fastapi-with-database/main.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import os
2+
3+
import asyncpg
4+
import sentry_sdk
5+
from fastapi import FastAPI
6+
from sentry_sdk.integrations.asyncpg import AsyncPGIntegration
7+
from sentry_sdk.integrations.fastapi import FastApiIntegration
8+
from sentry_sdk.integrations.starlette import StarletteIntegration
9+
import logging
10+
11+
logging.basicConfig(level=logging.INFO)
12+
logger = logging.getLogger(__name__)
13+
14+
DATABASE_USER = os.environ.get("DATABASE_USER", "postgres")
15+
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD", "postgres")
16+
DATABASE_HOST = os.environ.get("DATABASE_HOST", "localhost")
17+
DATABASE_PORT = os.environ.get("DATABASE_PORT", "5432")
18+
DATABASE_NAME = os.environ.get("DATABASE_NAME", "test_multiline")
19+
20+
DATABASE_URL = f"postgresql://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}"
21+
22+
23+
# This multiline string simulates what a user would copy from the Sentry UI
24+
# "Traces" page and paste into their before_send_transaction callback.
25+
# The Sentry UI displays the span description (the SQL query) with its
26+
# original newlines/whitespace intact.
27+
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"
28+
29+
def before_send_transaction(event, hint):
30+
"""
31+
User's before_send_transaction callback that attempts to filter out
32+
transactions containing a specific multiline SQL query.
33+
34+
The user copied the query string from the Sentry UI Traces page
35+
and wants to drop transactions that contain this query as a span.
36+
"""
37+
for span in event.get("spans", []):
38+
description = span.get("description", "")
39+
if description and QUERY_TO_FILTER in description:
40+
logger.info(f"[before_send_transaction] MATCH FOUND - dropping transaction")
41+
return None
42+
43+
logger.info(f"[before_send_transaction] No match found - keeping transaction")
44+
return event
45+
46+
47+
sentry_sdk.init(
48+
dsn=os.environ.get("SENTRY_DSN"),
49+
environment=os.environ.get("ENV", "local"),
50+
traces_sample_rate=1.0,
51+
debug=True,
52+
integrations=[
53+
StarletteIntegration(),
54+
FastApiIntegration(),
55+
AsyncPGIntegration(),
56+
],
57+
before_send_transaction=before_send_transaction,
58+
)
59+
60+
app = FastAPI()
61+
62+
63+
@app.on_event("startup")
64+
async def startup():
65+
app.state.pool = await asyncpg.create_pool(DATABASE_URL)
66+
67+
# Create tables if they don't exist
68+
async with app.state.pool.acquire() as conn:
69+
await conn.execute("""
70+
CREATE TABLE IF NOT EXISTS users (
71+
id SERIAL PRIMARY KEY,
72+
name TEXT NOT NULL,
73+
email TEXT NOT NULL,
74+
active BOOLEAN DEFAULT true
75+
)
76+
""")
77+
await conn.execute("""
78+
CREATE TABLE IF NOT EXISTS posts (
79+
id SERIAL PRIMARY KEY,
80+
user_id INTEGER REFERENCES users(id),
81+
title TEXT NOT NULL,
82+
created_at TIMESTAMP DEFAULT NOW()
83+
)
84+
""")
85+
86+
# Seed some data if tables are empty
87+
count = await conn.fetchval("SELECT COUNT(*) FROM users")
88+
if count == 0:
89+
await conn.execute("""
90+
INSERT INTO users (name, email, active) VALUES
91+
('Alice', 'alice@example.com', true),
92+
('Bob', 'bob@example.com', true),
93+
('Charlie', 'charlie@example.com', false)
94+
""")
95+
await conn.execute("""
96+
INSERT INTO posts (user_id, title) VALUES
97+
(1, 'First Post'),
98+
(1, 'Second Post'),
99+
(2, 'Hello World')
100+
""")
101+
102+
103+
@app.on_event("shutdown")
104+
async def shutdown():
105+
await app.state.pool.close()
106+
107+
108+
@app.get("/")
109+
async def root():
110+
return {
111+
"endpoints": {
112+
"multiline_query": "http://localhost:5000/query",
113+
}
114+
}
115+
116+
117+
@app.get("/query")
118+
async def multiline_query():
119+
"""
120+
Executes a multiline SQL query against the database.
121+
The span description captured by Sentry will contain the query
122+
with its original newlines.
123+
"""
124+
async with app.state.pool.acquire() as conn:
125+
rows = await conn.fetch("""SELECT
126+
u.id,
127+
u.name,
128+
u.email,
129+
p.title AS post_title,
130+
p.created_at AS post_date
131+
FROM
132+
users u
133+
JOIN
134+
posts p ON u.id = p.user_id
135+
WHERE
136+
u.active = true
137+
ORDER BY
138+
p.created_at DESC""")
139+
140+
return {
141+
"count": len(rows),
142+
"results": [dict(r) for r in rows],
143+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[project]
2+
name = "test-fastapi-with-database"
3+
version = "0"
4+
requires-python = ">=3.12"
5+
6+
dependencies = [
7+
"fastapi>=0.115.11",
8+
"asyncpg>=0.30.0",
9+
"sentry-sdk[fastapi,asyncpg]",
10+
"uvicorn>=0.34.0",
11+
]
12+
13+
[tool.uv.sources]
14+
sentry-sdk = { path = "/sentry-python", editable = true }

test-fastapi-with-database/run.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
docker compose up --build

0 commit comments

Comments
 (0)