Self-hosted error monitoring for FastAPI, Starlette, and any async-Python app — a Sentry-compatible-ish SDK that ships errors to your own ErrorWatch instance.
- Async-first: fire-and-forget on
httpx.AsyncClient, never blocks the request hot path. - Sync fallback: usable from CLIs, workers, or atexit hooks.
- ASGI middleware: drop-in for FastAPI/Starlette.
contextvarsscope: tags, user, breadcrumbs propagate naturally acrossawaitboundaries.- stdlib
loggingbridge: log records become breadcrumbs and (optionally) events. - Zero crash guarantee: the SDK never propagates an exception to your app.
pip install errorwatch-sdk[fastapi]Or, from a sibling clone (monorepo / local dev):
pip install -e /Users/kev/Documents/lab/sandbox/errorwatch/packages/sdk-pythonRequires Python 3.9+ and httpx.
# app/main.py
from fastapi import FastAPI
import errorwatch
from errorwatch.integrations.fastapi import install, lifespan_shutdown_hook
errorwatch.init(
api_key="ew_xxx", # or env var ERRORWATCH_API_KEY
endpoint="https://errorwatch.example", # your self-hosted instance
environment="production",
release="api@1.4.2",
project_root="/srv/app", # for in_app frame detection
)
app = FastAPI()
install(app) # ASGI middleware: captures unhandled exceptions
lifespan_shutdown_hook(app) # flushes pending sends on graceful shutdownThat's it — every unhandled exception in any route is reported with full stack, source context, request URL, method, headers (sensitive ones filtered), and the current scope (user/tags/breadcrumbs).
import errorwatch
try:
risky_call()
except Exception:
errorwatch.capture_exception() # uses sys.exc_info()
errorwatch.capture_message("payment retry exhausted", level="warning")import errorwatch
errorwatch.set_user({"id": "u_42", "email": "alice@example.com"})
errorwatch.set_tag("tenant", "kweli")
errorwatch.set_extra("invoice_id", "inv_8821")
errorwatch.add_breadcrumb(
category="db",
message="SELECT * FROM orders WHERE …",
level="info",
data={"rows": 312},
)Each request gets its own isolated scope thanks to contextvars, so concurrent FastAPI requests never see each other's user/tags.
For ad-hoc isolation (e.g. inside a background task):
from errorwatch import push_scope
with push_scope() as s:
s.set_tag("job", "nightly-export")
do_thing() # tag visible only inside this blockimport logging
from errorwatch.integrations.logging import BreadcrumbHandler, EventHandler
root = logging.getLogger()
root.addHandler(BreadcrumbHandler(level=logging.INFO)) # every log → breadcrumb
root.addHandler(EventHandler(level=logging.ERROR)) # errors → events| Option | Env var | Default | Description |
|---|---|---|---|
api_key |
ERRORWATCH_API_KEY |
— | Project API key. SDK is inert if empty. |
endpoint |
ERRORWATCH_ENDPOINT |
https://api.errorwatch.io |
Base URL — SDK appends /api/v1/envelope. |
environment |
ERRORWATCH_ENVIRONMENT |
production |
Free-form env label. |
release |
ERRORWATCH_RELEASE |
— | Release identifier (commit SHA, semver…). |
project_root |
ERRORWATCH_PROJECT_ROOT |
— | Used to mark frames as in_app. |
sample_rate |
— | 1.0 |
Client-side sampling (0.0 – 1.0). |
max_breadcrumbs |
— | 50 |
Ring buffer size per scope. |
max_context_lines |
— | 5 |
Source lines before/after each frame. |
send_default_pii |
— | False |
If False, sensitive headers (auth, cookie…) are filtered. |
timeout |
— | 5.0 |
HTTP request timeout (s). |
before_send |
— | None |
Callable[[payload], payload | None] — drop by returning None. |
debug |
— | False |
Logs transport diagnostics to the errorwatch logger. |
The SDK posts to POST {endpoint}/api/v1/envelope with header X-API-Key: <key>. Payload is the v2 enriched event shape — same as the official PHP SDK:
┌──────────────────────── your FastAPI app ─────────────────────────┐
│ │
│ request ──▶ ErrorWatchMiddleware ──▶ route handler ──▶ response │
│ │ │
│ │ on exception │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ Client.capture_*() │ │
│ └──────────┬───────────┘ │
│ │ │
│ ┌──────────▼───────────┐ │
│ │ Scope.apply_to_event │ user / tags / breadcrumbs │
│ └──────────┬───────────┘ │
│ │ │
│ ┌──────────▼───────────┐ fire-and-forget │
│ │ AsyncTransport.send │ ──────────────┐ │
│ └──────────────────────┘ │ │
└────────────────────────────────────────────────────┼──────────────┘
│ httpx POST
▼
https://errorwatch.example/api/v1/envelope
cd packages/sdk-python
pip install -e ".[dev,fastapi]"
pytest -q
ruff check .
mypy errorwatchMIT — see LICENSE.
{ "event_id": "4f7d…", "timestamp": "2026-05-12T11:23:45.123Z", "platform": "python", "level": "error", "sdk": { "name": "errorwatch-python", "version": "0.1.0" }, "contexts": { "runtime": { "name": "python", "version": "3.12.3" }, … }, "exception": { "type": "ValueError", "value": "boom" }, "frames": [ { "filename": "/srv/app/services/billing.py", "function": "charge_card", "lineno": 88, "in_app": true, "context_line": " raise ValueError('boom')", "pre_context": ["def charge_card(amount):", " if amount < 0:"], "post_context": ["", "def refund(amount):"] } ], "environment": "production", "release": "api@1.4.2", "server_name": "ip-10-0-3-12", "tags": { "tenant": "kweli" }, "user": { "id": "u_42" }, "request": { "url": "https://api.example/orders", "method": "POST", "headers": { "host": "api.example", "authorization": "[Filtered]" }, "query_string": "page=2" }, "breadcrumbs": [ … ] }