Skip to main content

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.

PyPI Python License: MIT FastAPI Django


┌──────────────────┐    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

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

  • Per-route RPS, error rate, avg / max latency
  • Service-level CPU & memory (psutil)
  • In-flight request counter
  • 1m / 5m / 15m / 1h window selector
  • Plotly time-series for RPS + latency

Code-aware

  • Static scan finds every FastAPI / Django route
  • Tree view of folders → files → endpoints
  • Highlights endpoints with no runtime traffic
  • Click any folder → filter routes & analysis to it

ORM tracking

  • Counts SQL queries per request
  • Tracks DB time per request
  • Captures top-3 slowest queries
  • N+1 hint: routes with ≥10 q/req turn red
  • Django auto-instrumented; SQLAlchemy one-liner

Error grouping

  • Tracebacks captured with frame fingerprints
  • Same bug from same place = one group
  • Count, first seen, last seen, affected routes
  • Click a group → expand traceback inline

Outgoing HTTP

  • Auto-instruments httpx + requests
  • Per-host call counts, error rate, latency
  • Bipartite graph: your routes → external hosts
  • Edge width = volume, color = error rate

AI assist (optional)

  • "Analyze" button on every route + folder
  • Paste an Anthropic or OpenAI key once
  • Key stays in your browser — never sent to us
  • Get root-cause + mitigation suggestions

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 hops
  • X-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 --retention seconds, default 1 h)
  • outgoing_calls — one row per outbound HTTP
  • service_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.py files, path() / re_path() patterns, recursive include() 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_wrappers list (existing connections plus any future ones via the connection_created signal).
  • SQLAlchemy: Listens on the engine's before_cursor_execute / after_cursor_execute events. You call instrument_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
  1. Is the middleware actually loaded? Hit any endpoint, then check http://127.0.0.1:9876/api/services — your service name should appear.
  2. Is the collector running? backtrack start printed its bind address.
  3. Is BACKTRACK_ENDPOINT (env var) pointing at the wrong port?
  4. 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

backtrack_route101-0.1.1.tar.gz (59.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

backtrack_route101-0.1.1-py3-none-any.whl (60.1 kB view details)

Uploaded Python 3

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

Hashes for backtrack_route101-0.1.1.tar.gz
Algorithm Hash digest
SHA256 ae98832ce4ca52db5a80ff6a8d4eae059ee91c36b040e345c227b7f97693c442
MD5 b45eda47696cbb245390a4860d6880cd
BLAKE2b-256 f61125eec9f08615b6fd87e6ba0fd7521a21e5366813875d8b92836feaf4abcc

See more details on using hashes here.

File details

Details for the file backtrack_route101-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for backtrack_route101-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 ec4c83c673a780e2ba729ecb1c13db54c8312ad90da737e2fefcded6e8ab4d24
MD5 5daef2152c0e1405c717e35d61a4d26d
BLAKE2b-256 b0bb7ee3bfe5a9aa57d7aabecdf3724f206e34d5351cf9559df4f49068f9c9dc

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page