Local-first API observability for Python backends — live RPS / latency / errors / CPU / memory dashboard, ORM query tracking, outgoing-call dependency graph, optional LLM-powered diagnostics. Zero external services.
Project description
backtrack
Local-first API observability for Python backends.
Drop-in middleware → live dashboard at localhost:9876 → zero external services.
┌──────────────────┐ HTTP POST ┌────────────────────────┐ poll ┌───────────────┐
│ Your app │ ─── /ingest ──► │ backtrack collector │ ◄────────► │ Dashboard │
│ + middleware │ batch │ :9876 │ /api/* │ (browser) │
│ (FastAPI/Django)│ │ ┌─ SQLite store │ │ Plotly + │
└─────────┬────────┘ │ ├─ FastAPI server │ │ Cytoscape │
│ on boot: │ ├─ static dashboard │ └───────┬───────┘
│ scan source code │ └─ source scanner │ │
└──────────────────────────► │ direct call (key
└────────────────────────┘ in localStorage)
▼
┌──────────────────────┐
│ api.anthropic.com │
│ api.openai.com │
└──────────────────────┘
Table of contents
- Why backtrack
- What you get
- Install
- Quickstart: FastAPI
- Quickstart: Django
- The dashboard, tour by tour
- AI assist (bring your own key)
- Distributed traces with correlation IDs
- Configuration reference
- How it works
- FAQ & troubleshooting
- Roadmap
- License
Why backtrack
You want to see what your Python backend is doing — RPS, latency, errors, slow SQL, who you're calling out to — without spinning up Datadog, signing up for Sentry, or shipping any data off your machine.
backtrack is one pip install, one middleware line, and one backtrack start away from a live dashboard at http://127.0.0.1:9876. It runs entirely on your laptop. Nothing leaves the box unless you click analyze and explicitly send a metric snapshot to your own LLM key.
Local-first means local-first. Your API keys (if you use the AI assist) live in your browser's
localStorage. The collector never sees them. No telemetry, no phone-home, no signup. The whole tool is one Python package and one SQLite file under~/.backtrack/.
What you get
Live metrics
|
Code-aware
|
ORM tracking
|
Error grouping
|
Outgoing HTTP
|
AI assist (optional)
|
Install
pip install backtrack_route101
That's it. The package ships with everything: collector, dashboard, FastAPI + Django middleware. The dashboard is plain HTML/JS/CSS — no Node build step required.
Python ≥ 3.10 required. Tested on 3.10, 3.11, 3.12.
Quickstart: FastAPI
Three lines of code, one terminal command
1. Add the middleware to your app
# main.py
from fastapi import FastAPI
from backtrack.sdk.fastapi import BacktrackMiddleware
app = FastAPI()
app.add_middleware(BacktrackMiddleware, service="my-api") # <─ this line
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"id": user_id}
2. Start your app as usual
uvicorn main:app --reload
3. Start the collector in another terminal
backtrack start --scan . --scan-service my-api
4. Open the dashboard
http://127.0.0.1:9876
Hit a few endpoints in your app, watch the tiles light up.
Add SQLAlchemy query tracking (one line)
from sqlalchemy import create_engine
from backtrack.sdk import instrument_sqlalchemy
engine = create_engine("postgresql://…")
instrument_sqlalchemy(engine) # <─ call once per engine
Now the routes table shows DB q (avg queries per request) and DB ms (avg time spent in the DB) per route. Routes that fire ≥10 queries per request are flagged in red — a strong N+1 signal.
Quickstart: Django
One settings file edit
In settings.py:
MIDDLEWARE = [
"backtrack.sdk.django.BacktrackMiddleware", # <─ add to the top
"django.middleware.security.SecurityMiddleware",
# …existing middleware…
]
BACKTRACK_SERVICE = "my-django-app"
# Optional:
# BACKTRACK_ENDPOINT = "http://127.0.0.1:9876/ingest" # default
# BACKTRACK_INSTRUMENT_OUTGOING = True # default
Then in another terminal:
backtrack start --scan /path/to/your/project --scan-service my-django-app
python manage.py runserver
That's all. Django's ORM is auto-instrumented — every Model.objects.… shows up in the per-route DB columns. Class-based views, function-based views, path() and re_path() URLconfs are all picked up by the source scanner.
The dashboard, tour by tour
Open http://127.0.0.1:9876 after backtrack start. From top to bottom:
Service & window selector
- service — which app to look at. Pick from the dropdown (populated by services that have sent heartbeats or been
--scaned). - window — 1m / 5m / 15m / 1h. All charts and aggregates respect this.
- settings — AI assist key configuration (see below).
The seven tiles
| Tile | Meaning |
|---|---|
| RPS | Requests per second across the window |
| error rate | % of responses with status ≥ 500 or an unhandled exception |
| avg latency | Average response time |
| max latency | Slowest response in the window |
| in-flight | Currently open requests (from service heartbeat) |
| CPU | App process CPU %, averaged across pids of the same service |
| memory | App process RSS, summed across pids |
RPS / latency charts
Two Plotly charts updated every 2 seconds. The left one overlays RPS (blue, left axis) and error rate (red dotted, right axis). The right one is average latency over time.
Project section — tree & graph tabs
tree — folder hierarchy of your project. Each file shows the routes defined in it; each route shows its live RPS / error / avg-latency if it's been hit. Hover a folder → click the "filter" chip to scope the routes table and AI assist to that folder only.
graph — bipartite routes → external hosts view. Your routes on the left (sized by outbound volume), the external services they call on the right. Edges show traffic and error rate. Tells you "what does this service depend on, and where does it break?" — the question a routes table can't show.
Routes table
| Column | What it means |
|---|---|
| method / route | HTTP method + route template (with placeholders) |
| RPS | Requests per second |
| err % | Status ≥ 500 or exception |
| avg ms / max ms | Response latency |
| DB q | Average SQL queries per request (red if ≥ 10) |
| DB ms | Average DB time per request |
| out | Average outgoing HTTP calls per request |
| total | Total request count in window |
| analyze | Ask the LLM to diagnose this route |
Errors (grouped)
Sentry-style error grouping. Identical tracebacks from the same code path collapse into one row. Click a row to expand the sample traceback. Group counts, first/last seen, affected routes all shown.
Outgoing HTTP
Flat table of every external host your app called, with counts, error rate, and latency. Populates as soon as your app makes a request via httpx or requests.
AI assist (bring your own key)
Click settings in the top-right, pick a provider (Anthropic or OpenAI), paste your key, optionally name a model, click save.
Your key is stored in this browser's localStorage only.
The backtrack collector never sees it — analyze requests
go directly from this page to the provider you choose.
Then anywhere you see an analyze button:
- Each route row in the table
- Folder click → "analyze folder" in the banner
The browser fetches a JSON snapshot (latency, errors, DB stats, outgoing-call breakdown) from your local collector, builds a prompt that asks for root causes + mitigations + robustness improvements, and POSTs directly to api.anthropic.com / api.openai.com with your key. The collector logs zero of this. If you tcpdump localhost:9876, you'll see ingest traffic and dashboard polls — never AI calls.
Works with any model in either family — including o1/o3/gpt-5 (reasoning models that require max_completion_tokens) and any Claude 3.x or 4.x.
Distributed traces with correlation IDs
If your service calls another service that's also instrumented, you can stitch the chain together:
import httpx
from backtrack.sdk import current_headers
async with httpx.AsyncClient() as client:
# Merge backtrack's trace headers into your outgoing call:
r = await client.get(other_url, headers={**current_headers(), **other_headers})
That's the only line you need to write. The downstream service will:
- See this hop as its
parent_id - Stay on the same
correlation_id - Show up linked in the data store
Backtrack uses three headers (simplified W3C trace context):
X-Request-ID— this hop's id (generated if absent)X-Correlation-ID— chain id, shared across all hopsX-Parent-Request-ID— the request_id of the caller
You can query the chain via GET /api/route_chains?service=…&w=….
Configuration reference
FastAPI middleware
app.add_middleware(
BacktrackMiddleware,
service="my-api", # required: name shown in dashboard
endpoint="http://127.0.0.1:9876/ingest", # default, override for non-standard ports
instrument_outgoing=True, # auto-patch httpx + requests
)
Django settings
MIDDLEWARE = ["backtrack.sdk.django.BacktrackMiddleware", …]
BACKTRACK_SERVICE = "my-django-app"
BACKTRACK_ENDPOINT = "http://127.0.0.1:9876/ingest" # optional
BACKTRACK_INSTRUMENT_OUTGOING = True # optional (default True)
Environment variables (SDK side)
| Variable | Default | What it does |
|---|---|---|
BACKTRACK_ENDPOINT |
http://127.0.0.1:9876/ingest |
Where the SDK pushes metric batches |
Collector CLI
backtrack start \
--host 127.0.0.1 \ # bind host (KEEP localhost unless you really know)
--port 9876 \ # bind port
--db ~/.backtrack/backtrack.db \ # SQLite database location
--retention 3600 \ # seconds of raw events to keep
--scan ./my-project \ # source path(s) to scan at startup (repeatable)
--scan-service my-api # service name for the corresponding --scan (positional)
Multiple --scan flags can be paired with multiple --scan-service flags positionally.
SQLAlchemy
from backtrack.sdk import instrument_sqlalchemy
instrument_sqlalchemy(engine) # call once per engine, idempotent
How it works
Per-request overhead
| Step | Cost |
|---|---|
| Middleware on hot path | ~100 µs (lock + list append + status capture) |
| SDK background flush | 1 s interval, ~5–20 ms per batch, async to your app |
| Dashboard poll | 2 s interval, 5 parallel REST calls, ~10–50 ms each |
The middleware never blocks on the network. The background reporter buffers events in memory (5,000-event cap) and POSTs them to the collector. If the collector is down, batches are dropped. Your app is never slowed down or blocked by backtrack.
Storage
One SQLite file at ~/.backtrack/backtrack.db. WAL mode, three tables:
events— one row per request (retained for--retentionseconds, default 1 h)outgoing_calls— one row per outbound HTTPservice_state— last heartbeat per(service, host, pid)discovered_routes— output of the source scanner
All aggregation runs on read — no precomputed rollups. Plenty fast for the volume a local-only tool sees.
Source scanning
The collector walks Python source files (skipping .venv, node_modules, __pycache__, etc.) and uses regex pickers for:
- FastAPI:
@app.get/post/…,@router.X,@app.api_route(methods=[…]) - Django:
urls.pyfiles,path()/re_path()patterns, recursiveinclude()resolution with prefix stitching, class-based view detection
Output goes into discovered_routes and powers the tree view + AI folder analysis.
Database query tracking
- Django: Auto-installed on first request — adds a wrapper to every
connection.execute_wrapperslist (existing connections plus any future ones via theconnection_createdsignal). - SQLAlchemy: Listens on the engine's
before_cursor_execute/after_cursor_executeevents. You callinstrument_sqlalchemy(engine)once per engine at startup.
SQL strings are normalized (literal values, IN-lists, numbers → ?) so identical query templates collapse — N+1 patterns become visible by their template-repeat count.
Error fingerprinting
When an exception bubbles out of the handler, we capture the full traceback and compute a blake2b digest of filename + qualname + lineno for every frame. Same exception from the same code path = same fingerprint = one group. Paths are normalized to be portable across machines (everything after the last site-packages/, src/, or Lib/ segment).
Outgoing HTTP instrumentation
The middleware monkey-patches httpx.Client.send, httpx.AsyncClient.send, and requests.Session.send at construction time. Each call is timed, the host extracted, and an OutgoingCall record sent to the collector's separate outgoing_calls table. A per-request aggregate via contextvars also adds outgoing_count + outgoing_total_ms to the originating Event.
Caveats: only httpx and requests are instrumented today. urllib, aiohttp, raw sockets won't show up.
FAQ & troubleshooting
The dashboard shows nothing for my service
- Is the middleware actually loaded? Hit any endpoint, then check
http://127.0.0.1:9876/api/services— your service name should appear. - Is the collector running?
backtrack startprinted its bind address. - Is
BACKTRACK_ENDPOINT(env var) pointing at the wrong port? - Firewall? localhost-only by default, but worth checking.
"no outgoing HTTP calls recorded yet"
Either your app hasn't made an outbound call yet, or it uses a library backtrack doesn't instrument (urllib, aiohttp, etc.). The fastest way to verify: add one httpx.get("https://httpbin.org/uuid") call somewhere in a handler, hit that endpoint, refresh.
The graph view stays empty
The deps graph only draws edges from runtime outgoing-call data — same caveat as above. If your outgoing HTTP table at the bottom is populated but the graph is empty, the service dropdown is set to "all" instead of a specific service.
Dashboard shows old code after I updated
Hard-reload (Ctrl+F5 / Ctrl+Shift+R). Static files have no cache headers, but the browser's session cache can hold onto the old JS until you force a refresh.
Schema error after upgrade ("no such column: …")
Run once to migrate, or nuke and start fresh:
# Migrate (automatic on next start, just re-run)
backtrack start
# Or wipe and recreate
rm ~/.backtrack/backtrack.db*
backtrack start
Windows: "Exception in callback _ProactorBasePipeTransport"
A benign asyncio quirk on Windows that backtrack already suppresses. If you're seeing it, you're on an old version — pip install -U backtrack and restart the collector.
Is it safe to bind --host 0.0.0.0?
No. The collector has zero authentication. Anyone with network access can read your metrics, scan your filesystem (POST /api/scan), and re-trigger source scans. Bind to 127.0.0.1 (the default) unless you've put it behind a real auth proxy.
Does this slow down my app?
The middleware adds ~100 µs to each request (one lock acquisition + list append + status capture). The actual transport runs on a background thread and never blocks the request path. If the collector is unreachable, batches drop silently — your app stays up.
Roadmap
Things being considered, not promised:
- Percentile latencies (p50 / p95 / p99) — avg/max hides tail issues
- Background-task instrumentation (Celery, Dramatiq, RQ)
- Longer retention with downsampled rollups (7–30 day history)
- Slack / webhook alerts on rules (error rate > 5% for 5 min …)
- Local LLM provider (Ollama) for AI assist without an API key
- Per-user / per-API-key segmentation tags
- OpenTelemetry ingestion (accept OTLP from non-Python services)
- Trace explorer (waterfall view for one full chain)
Open an issue if there's something you'd actually use.
License
MIT — Copyright (c) 2026 Arcitech.
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 backtrack_route101-0.1.1.tar.gz.
File metadata
- Download URL: backtrack_route101-0.1.1.tar.gz
- Upload date:
- Size: 59.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ae98832ce4ca52db5a80ff6a8d4eae059ee91c36b040e345c227b7f97693c442
|
|
| MD5 |
b45eda47696cbb245390a4860d6880cd
|
|
| BLAKE2b-256 |
f61125eec9f08615b6fd87e6ba0fd7521a21e5366813875d8b92836feaf4abcc
|
File details
Details for the file backtrack_route101-0.1.1-py3-none-any.whl.
File metadata
- Download URL: backtrack_route101-0.1.1-py3-none-any.whl
- Upload date:
- Size: 60.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec4c83c673a780e2ba729ecb1c13db54c8312ad90da737e2fefcded6e8ab4d24
|
|
| MD5 |
5daef2152c0e1405c717e35d61a4d26d
|
|
| BLAKE2b-256 |
b0bb7ee3bfe5a9aa57d7aabecdf3724f206e34d5351cf9559df4f49068f9c9dc
|