RunGuard SDK – capture runtime errors and send them to RunGuard
Project description
RunGuard Python SDK
Capture runtime errors and send them to RunGuard. Graceful: never raises, never blocks longer than the configured timeout (default 3 s).
Read-only mirror. This repo is a snapshot of the SDK source published to PyPI as
runguard-sdk. Pull requests cannot be merged here — please open an issue describing the problem or change, and we'll work it back into the upstream source. The SDK is developed in a private monorepo by Robion Systems.
Prerequisites
- A RunGuard account — sign up at runguard.ai.
- Install the RunGuard GitHub App on your repos: github.com/apps/runguard-app.
- Get an API key from the RunGuard dashboard.
Install
pip install runguard-sdk
Requires Python 3.9+. Zero runtime dependencies — uses only the standard library.
Usage
from runguard_sdk import init, capture
init(api_key="your-api-key")
try:
do_work()
except Exception as err:
capture(err)
raise
In asyncio apps (FastAPI, Starlette, aiohttp, Sanic), use acapture so the event loop is not blocked on the HTTP send:
from runguard_sdk import init, acapture
init(api_key="your-api-key")
@app.exception_handler(Exception)
async def on_error(request, exc):
await acapture(exc, metadata={"path": request.url.path})
raise
In hot paths where you want best-effort reporting without paying the HTTP round-trip cost on the request path, use capture_sync:
from runguard_sdk import capture_sync
capture_sync(err) # returns immediately; HTTP send runs in a daemon thread
API
init(*, api_key, base_url=None, timeout=None)— configure the SDK. May be called multiple times; the last call wins. Emptyapi_keyis allowed and short-circuitscapture()without making a network call.timeoutis in seconds.capture(error, *, service=None, env=None, timestamp=None, metadata=None) -> bool— synchronous. ReturnsTrueon HTTP 2xx,Falseon anything else. Never raises.await acapture(error, ...) -> bool— async variant; same arguments. Runs the blocking HTTP send in a worker thread viaasyncio.to_threadso the event loop is never blocked.capture_sync(error, ...) -> None— fire-and-forget. Spawns a daemon thread and returns immediately.
Environment variables
| Name | Purpose |
|---|---|
RUNGUARD_API_KEY |
API key (used if init(api_key=...) not called) |
RUNGUARD_BASE_URL |
Ingest base URL (default https://api.runguard.ai) |
RUNGUARD_TIMEOUT_MS |
HTTP send timeout in milliseconds (default 3000) |
RUNGUARD_SERVICE |
Service name override |
RUNGUARD_ENV |
Environment override; wins over ENVIRONMENT |
Service detection
When service is not passed and RUNGUARD_SERVICE is unset, the SDK tries, in order:
OTEL_SERVICE_NAMEK_SERVICE(Cloud Run)FLY_APP_NAME(fly.io)- Falls back to
"app"and prints a one-time warning to stderr.
If none of these env vars are set in your runtime, pass service explicitly:
capture(err, service="checkout-api")
Env detection
RUNGUARD_ENV → ENVIRONMENT (12-factor convention) → "production".
The pre-1.0 ENV fallback was removed because it collides with too many other tools (notably Heroku-style config and several shell helpers).
Auto-collected metadata
Every payload includes the following keys under metadata in addition to anything you pass:
| Key | Value |
|---|---|
sdk_name |
"runguard-python" |
sdk_version |
Package version |
sdk_runtime |
python-<version> (e.g. python-3.12.7) |
sdk_platform |
platform.platform() (Darwin-23.6.0-arm64, Linux-5.15-x86_64, …) |
sdk_pid |
os.getpid() |
sdk_hostname |
socket.gethostname() when available |
User-supplied metadata keys override SDK auto-keys on collision. Per-payload limit: 20 keys total; values are server-side stringified and truncated to 1000 chars. Client-side the SDK pre-coerces complex objects to strings so json.dumps of the payload never throws; None values are dropped.
Graceful failure
capture() never throws and never blocks longer than timeout. It returns False when:
- the API key is missing,
- the server returns a non-2xx response (401, 5xx, …),
- the network is unreachable, the DNS lookup fails, or the TLS handshake fails,
- the send exceeds
timeout(default 3 s).
No retries — the SDK is at-most-once delivery. Pair it with a structured logger if you need a local fallback for ingest outages.
Framework patterns
Django / Flask (WSGI)
capture() is thread-safe; you can call it from any view handler. With gunicorn --workers N --threads M, all N*M threads share the same module-global config.
# settings.py / app.py — once at boot
from runguard_sdk import init
init(api_key=os.environ["RUNGUARD_API_KEY"])
# views.py / handler — synchronous
from runguard_sdk import capture
def my_view(request):
try:
return do_work(request)
except Exception as err:
capture(err, metadata={"path": request.path})
raise
FastAPI / Starlette / aiohttp / Sanic (ASGI)
Use acapture to avoid blocking the event loop:
from runguard_sdk import init, acapture
init(api_key=...)
@app.middleware("http")
async def report_errors(request, call_next):
try:
return await call_next(request)
except Exception as err:
await acapture(err, metadata={"path": str(request.url.path)})
raise
Celery
For --pool=threads, use capture() directly inside the task body. For --pool=prefork, each worker process must init() itself (workers are forked before the user code imports the SDK in most setups; call init() at module top of celery_app.py).
from celery import Celery
from runguard_sdk import init, capture
init(api_key=os.environ["RUNGUARD_API_KEY"])
celery_app = Celery(...)
@celery_app.task(bind=True)
def send_email(self, to_addr):
try:
smtp.send(to_addr)
except Exception as err:
capture(err, metadata={"task": self.name, "to": to_addr})
raise # let Celery retry
AWS Lambda / Cloud Run
init() once at module top so the warm container reuses it. capture() from inside the handler.
from runguard_sdk import init, capture
init(api_key=os.environ["RUNGUARD_API_KEY"])
def handler(event, context):
try:
return do_work(event)
except Exception as err:
capture(err, metadata={"request_id": context.aws_request_id})
raise
Privacy note
Error tracebacks contain file paths from the throwing process. On developer machines this means paths like /Users/<you>/... are sent to RunGuard. In production these typically resolve to container paths (/app/...). Pass redacted stacks or extra context via metadata= if your build embeds sensitive paths.
Development
cd packages/runguard-python
pip install -e ".[dev]"
pytest
The test suite spins up a real http.server on 127.0.0.1 and points the SDK at it for transport-level tests — no monkey-patching urllib. Framework-compatibility tests simulate Django/Flask/FastAPI/Celery via stdlib primitives (threading.Thread, asyncio, multiprocessing) rather than installing the frameworks.
Publish a new version:
pip install build twine
python -m build
twine upload dist/*
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file runguard_sdk-1.0.0.tar.gz.
File metadata
- Download URL: runguard_sdk-1.0.0.tar.gz
- Upload date:
- Size: 35.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b80c5650853fc80b8aafacb99cb99b54b9fcd6cc63eaa6b6083f58d191b7df7a
|
|
| MD5 |
0546bbc762109bc437602d39da52df06
|
|
| BLAKE2b-256 |
6e3979dc14213c80f9d7c5e75355e2e1111af15a205bfddb91fefcc654eef04a
|
File details
Details for the file runguard_sdk-1.0.0-py3-none-any.whl.
File metadata
- Download URL: runguard_sdk-1.0.0-py3-none-any.whl
- Upload date:
- Size: 15.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e03e9e61c91b28336c5ae7bb105e5b976e7ecdeadabc7a67f42d688a6602b2f3
|
|
| MD5 |
9d6e2dc599473559fcdcec5e02346f8a
|
|
| BLAKE2b-256 |
a0c586565141d5691cf1882cbe2d0f44d640704588b9f1b810b2117268aa8038
|