Structured logs tell you what happened. Sentry tells you what went wrong. These are complementary tools, not alternatives, logs record the flow of normal operations, while error tracking focuses specifically on failures, grouping them, counting them, and alerting you when something new breaks.
The biggest difference: logs are pull-based (you search them when you suspect a problem), while Sentry is push-based (it alerts you when something breaks, even if no user reports it).
What Sentry captures that logs do not
When an exception occurs, Sentry collects far more than a stack traceWhat is stack trace?A list of function calls recorded at the moment an error occurs, showing exactly which functions were active and in what order.. Each error report is a rich document that makes reproduction possible without asking the user "what were you doing?"
| Data | What it shows | Why it matters |
|---|---|---|
| Stack trace | Exact line and call chain | Where the error occurred |
| Breadcrumbs | HTTP requests, DB queries, user actions before the crash | What led to the error |
| User context | User ID, email, IP address | Who was affected |
| Request data | URL, headers, body, method | What triggered it |
| Environment | Python version, OS, server name | Where it happened |
| Release | Git SHA or version tag | Which deployment introduced it |
| Frequency | How many times, how many users | How urgent it is |
Logs give you a single line: ERROR: KeyError 'email'. Sentry gives you the full picture: this error started after deploy abc123, it happens 40 times per hour, it affects 12% of users, and it only occurs when the user signs up via Google OAuthWhat is oauth?An authorization protocol that lets users grant a third-party app limited access to their account on another service without sharing their password. (because the Google profile response does not always include an email field).
Integrating Sentry with FastAPI
Installation
pip install sentry-sdk[fastapi]The [fastapi] extra installs the FastAPI integration automatically. Sentry will hookWhat is hook?A special function in React (starting with "use") that lets you add state, side effects, or other React features to a component without writing a class. into FastAPI's exception handling without any additional configuration.
Initialization
Initialize Sentry as early as possible, before you import your routes, models, or anything else. This ensures Sentry's monkey-patching is in place before any other code runs.
import sentry_sdk
import os
sentry_sdk.init(
dsn=os.environ["SENTRY_DSN"],
environment=os.environ.get("ENVIRONMENT", "development"),
release=os.environ.get("GIT_SHA", "unknown"),
traces_sample_rate=0.1, # Trace 10% of requests for performance
profiles_sample_rate=0.1, # Profile 10% of traced requests
)What happens automatically
Once initialized, the FastAPI integration captures:
- All unhandled exceptions in route handlers (returns 500 to the user, sends the full error to Sentry)
- All HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. requests as breadcrumbs (so you see what APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. calls happened before the crash)
- SQLAlchemy queries as breadcrumbs (if you use SQLAlchemy)
- Logging calls as breadcrumbs (your
logger.warning()calls appear in the Sentry timeline)
You do not need to wrap every route in a try/except. Sentry hooks into FastAPI's exception pipelineWhat is pipeline?A sequence of automated steps (install, lint, test, build, deploy) that code passes through before reaching production. automatically.
Enriching errors with context
Identifying users
Anonymous error reports are hard to act on. Attach user identity so you can see which users are affected and reach out to them:
from fastapi import Request
import sentry_sdk
@app.middleware("http")
async def set_sentry_user(request: Request, call_next):
if request.state.user:
sentry_sdk.set_user({
"id": request.state.user.id,
"email": request.state.user.email,
"username": request.state.user.username,
})
else:
sentry_sdk.set_user(None)
return await call_next(request)With user identity attached, Sentry can tell you "this error affects 3 users, all on the free plan, all using the mobile app." That context changes how you prioritize the fix.
Adding custom context and tags
Tags are indexed and searchable. Context is not indexed but provides rich detail on individual error reports.
# Tags - filterable in the Sentry UI
sentry_sdk.set_tag("plan", user.subscription_plan)
sentry_sdk.set_tag("api_version", "v2")
# Context - rich detail, not searchable
sentry_sdk.set_context("order", {
"order_id": order.id,
"items_count": len(order.items),
"total": float(order.total),
"payment_method": order.payment_method,
})Use tags for things you want to filter by ("show me all errors from v2 APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. users on the enterprise plan"). Use context for details that help reproduce a specific error.
Capturing handled exceptions
Sentry only auto-captures unhandled exceptions. When you catch an error and recover gracefully, Sentry does not see it, unless you tell it:
try:
result = await external_api.fetch_data(resource_id)
except ExternalAPIError as e:
sentry_sdk.capture_exception(e)
# Fall back to cached data
result = await cache.get(resource_id)
logger.warning("External API failed, using cache", extra={
"resource_id": resource_id,
"error": str(e)
})This is important for errors you handle gracefully but still want to track. If the external API fails 500 times a day, you want to know even if your users never see an error.
Breadcrumbs, the timeline before the crash
Breadcrumbs are the most underrated feature of Sentry. They record a timeline of events leading up to the error, HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. requests made, database queries executed, log messages emitted, user interactions.
# Automatic breadcrumbs (enabled by default):
# - HTTP requests via httpx/requests
# - Database queries via SQLAlchemy
# - Log messages at WARNING and above
# - Redis commands
# Custom breadcrumbs for business events
sentry_sdk.add_breadcrumb(
category="checkout",
message="User applied discount code",
level="info",
data={"code": "SAVE20", "discount_percent": 20}
)When you open an error in the Sentry UI, the breadcrumb timeline shows you exactly what the user did in the 30 seconds before the crash. This is often more valuable than the stack traceWhat is stack trace?A list of function calls recorded at the moment an error occurs, showing exactly which functions were active and in what order. itself, the stack trace tells you where the code broke, but the breadcrumbs tell you why.
Alerting rules, signal vs noise
Not every error deserves a notification at 3am. Sentry lets you configure alert rules based on conditions that matter.
What to alert on
| Condition | Alert? | Rationale |
|---|---|---|
| New issue (first occurrence) | Yes | Likely introduced by recent deploy |
| Regression (resolved issue reappears) | Yes | Fix did not hold |
| Error spike (200%+ increase in 1 hour) | Yes | Something changed |
| High user impact (>50 affected users) | Yes | Widespread problem |
| Known low-severity error | No | Suppress with issue rules |
| Client-side network timeout | No | Out of your control |
Filtering noise with before_send
def before_send(event, hint):
exception = hint.get("exc_info")
if exception:
exc_type, exc_value, _ = exception
# Ignore client disconnects - not our problem
if exc_type.__name__ == "ClientDisconnect":
return None
# Ignore 404s - they are not errors
if hasattr(exc_value, "status_code") and exc_value.status_code == 404:
return None
return event
sentry_sdk.init(
dsn=os.environ["SENTRY_DSN"],
before_send=before_send,
)sentry_sdk.capture_exception(). This is redundant, the FastAPI integration already captures unhandled exceptions. The try/except also swallows the error, preventing FastAPI from returning a proper 500 response. Only use capture_exception() for errors you handle and recover from.Quick reference
| Concept | What to do | Why |
|---|---|---|
| DSN | Environment variable, never hardcoded | Different projects per environment |
traces_sample_rate | 0.1 in production | 100% tracing is expensive |
| User identity | Set in middleware after auth | Know who is affected |
| Tags | Plan, API version, region | Filterable dimensions |
before_send | Drop noise (404s, client disconnects) | Keep signal-to-noise ratio high |
| Handled errors | capture_exception() in catch blocks | Track graceful failures |
| Alerts | New issues + regressions + spikes | Wake up for real problems only |