Foxray
A light error tracker that accepts errors from the official Sentry SDKs. It runs two ways from one codebase:
- inside a Django app, as
foxray.django: errors from the app itself are stored in-process (no DSN, no HTTP), and the Django admin is the UI; - on its own, as one small stdlib-only process (
foxray serve) with a SQLite file or Postgres behind it.
Works with the Sentry SDKs; not affiliated with Sentry.
Install from PyPI: pip install foxray (add [postgres] or [brotli] for
those extras).
Design rule: one process plus one database; no Redis, Celery, Kafka or ClickHouse. Everything other trackers push into side services lives in the database, kept as simple as possible:
| Elsewhere it's in | In Foxray it's |
|---|---|
| Redis rate-limit counters | foxray_rate: one atomic upsert per project per minute |
| Redis/memcached dedupe, cooldowns | a unique index on event ids; last_alerted_at on the issue |
| Celery alert tasks | foxray_alert: an outbox written in the ingest transaction, sent after commit, retried |
| Celery beat (cron checks, cleanup) | foxray_state leases; run after ingest when due, or by foxray tick |
| Kafka/ClickHouse counts | foxray_issue_day, foxray_counter: daily counters |
So several processes (uvicorn workers, a cron tick) always agree, and
nothing else needs running.
Add it to a Django app
pip install sentry-sdk and Foxray, add "foxray.django" to INSTALLED_APPS,
add path("foxray/", include("foxray.django.urls")) to your URLs, and run
python manage.py migrate. That's all: the migration creates a "default"
project, AppConfig.ready() starts the Sentry SDK with an in-process transport
(DjangoIntegration, CeleryIntegration if Celery is installed,
traces_sample_rate=0, no PII), errors appear in the admin under Foxray >
Issues, and alert emails go to settings.ADMINS (or, if that is empty, the
superusers) through Django's email backend. Access is Django's own: superusers
see everything; give staff foxray.view_issue (and foxray.change_issue to
resolve or ignore) through groups. Postgres or SQLite; Django 4.2+; Python 3.10+.
# settings.py
INSTALLED_APPS = [..., "foxray.django"]
# Optional; every key has a default. FOXRAY_<KEY> env vars fill in what isn't set here.
FOXRAY = {
"BASE_URL": "https://askrobots.com", # for links in alerts and DSNs
"ALERT_EMAILS": ["ops@example.com"], # default: ADMINS, else superusers
"ALERT_WEBHOOK": "", # POSTs alert JSON here too
"MAX_DB_MB": 500, # Foxray's own tables
}
# urls.py
urlpatterns += [path("foxray/", include("foxray.django.urls"))]
- If you already call
sentry_sdk.init(...)before apps load, orSENTRY_DSNis set, Foxray leaves the SDK alone. (A left-overSENTRY_DSNwith no SDK started: setFOXRAY = {"AUTO_INIT": True}and Foxray starts anyway.) To send the app's own errors to Foxray explicitly, callfoxray.django.sdk.init(**options)instead ofsentry_sdk.init.FOXRAY = {"AUTO_INIT": False}turns auto-start off. It also stays off undermanage.py testand pytest, so a host app's test errors don't land in its test database. - Storing never raises into the app: if it fails, the error is logged with
logging(loggerfoxray.sdk, which the SDK is told to ignore). - Other apps send over HTTP: copy a DSN from Foxray > Projects (or run
manage.py foxray_create_project webhive). It looks likehttps://<key>@askrobots.com/foxray/<project_id>. The ingest views are CSRF-exempt and don't use sessions or auth; the key is the credential. - Cleanup runs by itself (see below).
manage.py foxray_tick(monitors, cleanup when due, alerts) andmanage.py foxray_prune(cleanup now) are there if you'd rather run them from cron or your scheduler.
The trade-off: an app that reports to itself can't report while it's down, and an error storm adds load to the same app and database (rate limits and per-issue sampling bound it). The standalone server on another box avoids both.
Run it on its own
pip install foxray # SQLite: standard library only
pip install "foxray[postgres]" # optional: Postgres via psycopg
export FOXRAY_DATABASE_URL=/var/lib/foxray/foxray.db # or postgres://user@host/foxray
export FOXRAY_BASE_URL=https://foxray.example.com
foxray create-project shop # prints the DSN
foxray create-user you@example.com # a UI login (asks for a password)
foxray serve --host 127.0.0.1 --port 8765
serve is a threaded HTTP server: SDK ingest at /api/<id>/envelope/ and
/api/<id>/store/, and an HTML UI behind a login (issues, issue pages with the
stack trace, monitors, projects with their filters and limits, alerts,
storage). It runs its own tick every minute. Put it behind a TLS proxy (Caddy,
nginx) and set FOXRAY_TRUST_PROXY=1. Other commands: foxray projects,
foxray tick, foxray prune, foxray storage.
Configuration comes from environment variables or an INI file
(--config foxray.ini, section [foxray], same names in lower case; a .toml
file works on Python 3.11+). Environment variables win.
| Variable | Default | |
|---|---|---|
FOXRAY_DATABASE_URL / DATABASE_URL |
foxray.db |
SQLite path or postgres://... |
FOXRAY_HOST, FOXRAY_PORT |
127.0.0.1, 8765 |
|
FOXRAY_BASE_URL |
http://host:port |
public URL, for DSNs and alert links |
FOXRAY_SMTP_HOST, _PORT, _USER, _PASSWORD, _FROM |
off, 587 | email alerts (STARTTLS; FOXRAY_SMTP_SSL=1 for 465) |
FOXRAY_ALERT_EMAILS |
comma-separated; per project too | |
FOXRAY_ALERT_WEBHOOK |
POST alert JSON; per project too | |
FOXRAY_RATE_LIMIT_PER_MINUTE |
600 | events per project; 0 = off; per project too |
FOXRAY_SAMPLE_AFTER |
1000 | event bodies per issue per day, then counts only |
FOXRAY_MAX_DB_MB |
500 | hard size cap |
FOXRAY_EVENT_RETENTION_DAYS |
30 | also EVENTS_PER_ISSUE (100), ISSUE_RETENTION_DAYS (90) |
FOXRAY_ALERT_COOLDOWN_SECONDS |
3600 | |
FOXRAY_SECRET_KEY |
generated, kept in the DB | signs UI sessions |
Measured on an M-series Mac, Python 3.10, bench/measure.py (1,000 SDK-shaped
4 KB events, gzip, one keep-alive connection, everything stored):
| idle RSS | 1,000 events | |
|---|---|---|
| SQLite | 24.5 MB | 1.9 s (about 520/s) |
| Postgres 14 (local) | 34.9 MB | 5.7 s (about 175/s) |
Point an SDK at it
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.celery import CeleryIntegration
sentry_sdk.init(
dsn="https://<key>@foxray.example.com/<project_id>",
integrations=[DjangoIntegration(), CeleryIntegration()],
traces_sample_rate=0,
send_default_pii=False,
)
- Recent sentry-sdk releases (2.73 at the time of writing) deprecate
send_default_piiin favour ofdata_collection. Both default to off, so you can leave it out. - Cron monitors:
@sentry_sdk.crons.monitor(monitor_slug=..., monitor_config=...)orcapture_checkin(...). Crontab and interval schedules, timezones,checkin_marginandmax_runtimeare honoured. - Brotli: the Python SDK compresses with brotli whenever the
Brotlipackage is installed in the sending app (whitenoise setups often have it). Foxray decodes brotli only whenBrotliis installed where Foxray runs (pip install "foxray[brotli]"). Otherwise it answers 415 and those events are lost, so either install it or set_experiments={"transport_compression_algo": "gzip"}on the senders. gzip and deflate always work.
What it keeps, and what it drops
| Item | |
|---|---|
event (and the legacy /store/ JSON) |
stored: exception chain, frames with source context, in_app, message, level, platform, release, environment, server_name, transaction, tags, contexts, breadcrumbs (last 50), extra |
check_in |
stored; monitors created and scheduled from monitor_config |
transaction, session(s), client_report, attachment, profile, replay*, statsd, anything unknown |
accepted (200), dropped, counted per project per day |
- User data is cut to
idand request data tourl(query string removed) andmethodby default; frame local variables aren't kept unlessKEEP_FRAME_VARSis on. Everything kept is scrubbed before storage: passwords, tokens, cookies, Authorization headers, anything under a secret-looking key, card numbers (Luhn-checked), private keys, JWTs, and Stripe/AWS/GitHub/Slack-style keys. - Grouping: an SDK
fingerprintwins ({{ default }}works). Otherwise each exception's type plus its in-app frames asmodule:function(no line numbers, so edits elsewhere don't split an issue). Messages group with numbers, hex and ids blanked, using the logging template when there is one. - Answers:
200 {"id": ...}; 401/403 for a missing or wrong key; 413 for too large (10 MB sent, 20 MB decompressed, 1 MB per event); 415 for an unknown encoding; 429 withRetry-AfterandX-Sentry-Rate-Limitsover the rate limit. CORS is open, so browser SDKs work.
Filters, alerts, cleanup
-
Inbound filters (per project, in the DB, edited in the UI or the admin) are applied before anything is stored. A dropped event still gets a 200 and is counted by reason per day. You can drop by exception type or message (substring or regex, 200-character limit, nested repeats refused, optionally only for one environment or release), by environment, or by release. Built-in switches drop crawlers by user agent and health-check transactions, and (opt-in) browser-extension frames and localhost URLs on JavaScript events. After N events per issue per day (1,000) only the count is kept. Issues can be ignored for good, until N more events, or until a date.
-
Alerts fire on a new issue or a regression (a resolved issue seen again, or an ignored one past its "until"). Repeats never alert, and regressions respect a per-issue cooldown. Per project they can be limited to new issues and/or regressions, a minimum level, and environments. Monitors alert when they go from healthy to missed, failed or timed out. Delivery is email and/or a webhook, through the outbox.
-
Cleanup runs by itself. At most every 10 minutes, the ingest request that notices it's due runs one small batch on a short thread after answering. A lease row means two never overlap. Defaults:
- event bodies older than 30 days go;
- each issue keeps its newest 100 bodies;
- resolved/ignored issues not seen for 90 days go, with their daily counts;
- daily counts go after 400 days;
- check-ins: 30 days or the newest 500 per monitor;
- sent alerts go after 30 days, failed ones after 7 days of retries;
- rate windows and drop counters go after 2 days.
Issue counts and first/last seen survive all of it.
-
Size cap (
MAX_DB_MB, 500). Over 90%, the oldest bodies go until the store is under 80%. Still over 100%, Foxray counts only: it keeps grouping, counting and alerting but stores no new bodies, shows a banner, and sends one "storage full" alert. Ingest never fails because the store is full. On SQLite the file is created with incremental auto-vacuum, so space goes back to the disk after each prune (plus a WAL checkpoint). Postgres keeps freed space for reuse: Foxray deletes in batches, lets autovacuum work, never runsVACUUM FULL, and only prunes again once the tables grow. The Storage page shows the size, cap, mode, rows per table and the last cleanup.
Compared with GlitchTip and self-hosted Sentry
- Self-hosted Sentry is the full product: performance tracing, replays, profiling, source maps and symbolication, release health, integrations, teams. It is also a couple of dozen services (Kafka, ClickHouse, Redis, Postgres, Snuba, Relay, ...) and its docs ask for about 16 GB of RAM. Its server is under the Functional Source License.
- GlitchTip is the closest relative: open source (MIT), Sentry-SDK compatible, a Django app with a proper multi-user UI, organizations and teams, performance and uptime monitoring. A standard install runs PostgreSQL plus a Redis-compatible store and a worker.
- Foxray is smaller on purpose: one process and one database (even just a SQLite file), about 25 MB of RAM, or no extra process at all inside an existing Django app. It does errors, grouping, alerts, cron monitors, filters and its own cleanup. It does not do tracing, replays, profiles, attachments, source maps (minified JavaScript stack traces stay minified), release health, organizations/teams, or integrations beyond email and a webhook. If you need those, use GlitchTip or Sentry.
The name
Foxray: add up the letters of "sentry" as you go (a=1…z=26, wrapping past z) and you get s x l f x w; in the phonetic alphabet that's Sierra X-ray Lima Foxtrot X-ray Whiskey, and Foxtrot X-ray gives the name. It looks inside a crash.
Development
python3.10 -m venv .venv && .venv/bin/pip install -e ".[dev,postgres,brotli]"
.venv/bin/pytest # SQLite everywhere
FOXRAY_TEST_PG_URL=postgres://you@localhost/postgres .venv/bin/pytest
# + Postgres (creates and drops its own databases)
FOXRAY_TEST_DJANGO_PG=postgres://you@localhost/postgres .venv/bin/pytest tests/test_django.py tests/test_store_contract.py
python bench/measure.py [--db postgres://...] # idle RSS and 1,000-event ingest time
Layout: foxray/core (stdlib only: protocol, normalising, grouping, scrubbing,
filters, cron, alerts, the SQL store, cleanup), foxray/standalone (server,
UI, CLI), foxray/django (models, migrations, admin, ingest views, in-process
transport). The storage contract in tests/test_store_contract.py runs
against SQLite, Postgres and the Django store alike.
MIT licensed; see LICENSE and NOTICE.
Metadata
Release files for foxray 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| foxray-0.1.2.tar.gz | 122.1 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| foxray-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 229.2 kB
Release files / foxray-0.1.2.tar.gz
| Download URL | foxray-0.1.2.tar.gz |
|---|---|
| Size | 122.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
10fb099b34fbd19164cd3ffee13a52accce0387c2b10e32d706a6272a52eaaa6
|
|
BLAKE2b-256 checksum How to use checksums |
cf6eaea251d639e7ef30628aef1046c19a9578c8cac80a0de91314be554add4c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Release files / foxray-0.1.2-py3-none-any.whl
| Download URL | foxray-0.1.2-py3-none-any.whl |
|---|---|
| Size | 107.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5bb020c2e76964b03400db04ee954aafab1ff4dccdbdf963be79ca4c93857a43
|
|
BLAKE2b-256 checksum How to use checksums |
219ef918002bea2946b0a96be6d0c46ac83a0a3f9427b2827184adef3f6c82e2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Security for foxray 0.1.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.