Metadata-Version: 2.4
Name: leakyspanner
Version: 0.1.2
Summary: Non-intrusive FastAPI memory leak profiler with web UI
License: MIT
Keywords: fastapi,memory,profiler,leak,debug
Classifier: Programming Language :: Python :: 3
Classifier: Framework :: FastAPI
Classifier: Topic :: Software Development :: Debuggers
Classifier: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Provides-Extra: recommended
Requires-Dist: psutil>=5.9; extra == "recommended"

# 🔧 LeakySpanner

> Zero-interference memory leak profiler for FastAPI.  
> **Pure spectator mode — watches everything, touches nothing.**

---

## The Problem

FastAPI + Python async apps can silently grow to 4–5 GB RAM at peak load.  
Finding *where* is painful because the leak could be anywhere — images held in memory,  
uncleaned async tasks, connection pools, Gemini API response objects.

## The Solution

LeakySpanner attaches to your FastAPI app as a **pure spectator**:

- ✅ Watches every request
- ✅ Samples memory in a background thread every N seconds  
- ✅ Tracks memory delta per endpoint  
- ✅ Uses `tracemalloc` to find top allocation sites  
- ✅ Writes a clean log file you can grep through  
- ❌ Never blocks a request  
- ❌ Never adds latency  
- ❌ Never modifies request/response  
- ❌ If LeakySpanner crashes — your app keeps running  

---

## Install

```bash
pip install leakyspanner
```

---

## Usage

```python
from fastapi import FastAPI
from leakyspanner import leakyspanner

app = FastAPI()

# One line — that's it
leakyspanner(app)
```

### With options:

```python
leakyspanner(
    app,
    enabled=True,               # set False in tests
    log_path="./leak_logs",     # where to write logs
    level="deep",               # basic | mid | deep
    interval_seconds=5.0,       # memory snapshot every 5s
    leak_threshold_mb=50.0,     # warn if RSS grows 50MB above baseline
)
```

---

## Levels

| Level | What it tracks |
|-------|---------------|
| `basic` | Endpoint-level memory delta per request |
| `mid` | Endpoint deltas + tracemalloc top allocation stats *(default)* |
| `deep` | Everything + per-snapshot top allocation traces with file/line |

---

## Log Output

LeakySpanner writes to `./leakyspanner_logs/leakyspanner_YYYYMMDD_HHMMSS.log`

### Snapshot log (every 5s):
```
[14:32:05] RSS=312.4MB  VMS=890.1MB  delta=+12.1MB  GC=(142, 8, 1)
[14:32:10] RSS=318.7MB  VMS=890.1MB  delta=+18.4MB  GC=(198, 8, 1)
```

### Endpoint delta log (when delta > 1MB):
```
  [14:32:07] ENDPOINT POST /ocr/process  mem_delta=+6.23MB  status=200
  [14:32:08] ENDPOINT POST /ocr/process  mem_delta=+5.91MB  status=200
```

### Warning (when growth > threshold):
```
  ⚠️  [14:35:00] MEMORY GROWTH ALERT: +87.3 MB above baseline
                 (baseline=312.4 MB, current=399.7 MB)
```

### Session summary (on shutdown):
```
======================================================================
  LEAKYSPANNER SESSION SUMMARY
======================================================================
  Peak RSS    : 4821.3 MB
  Final RSS   : 4103.7 MB
  Baseline RSS: 312.4 MB
  Net growth  : +3791.3 MB

  ENDPOINT MEMORY DELTAS (sorted by avg):
    POST   /ocr/process                        calls= 412  avg=+8.73MB  max=+41.20MB
    GET    /health                             calls=1204  avg=+0.00MB  max= +0.01MB
======================================================================
```

### Deep level — tracemalloc traces:
```
  ── top allocations ──
    42.3KB × 18  /app/services/gemini.py:87
    38.1KB × 412 /app/services/ocr.py:134
    21.7KB × 1   /usr/lib/python3.11/ssl.py:1092
```

---

## Reading the Logs

Once you have the log, look for:

1. **Endpoint with highest `avg` delta** → that's your primary suspect
2. **Steadily rising RSS snapshots** that never drops → classic leak
3. **tracemalloc lines** pointing to specific file:line → that's the smoking gun
4. **GC counts growing** without dropping → reference cycles not being collected

---

## Real-world Example (OCR App)

```python
app = FastAPI()

leakyspanner(
    app,
    level="deep",
    log_path="/var/log/myapp/leaks",
    leak_threshold_mb=200.0,     # alert at 200MB growth
    interval_seconds=10.0,       # sample every 10s in prod
)

@app.post("/ocr/process")
async def process(file_id: str):
    # LeakySpanner watches this — you change nothing
    results = await asyncio.gather(
        call_gemini_1(file_id),
        call_gemini_2(file_id),
        call_qr_service(file_id),
        fetch_from_db(file_id),
    )
    return results
```

---

## License

MIT
