# Development guide
## Logging
All processes (FastAPI app, scheduler, telegram_bot, ws_gateway, corporate_memory,
session_collector, verification_detector, CLI scripts) use
`app.logging_config.setup_logging` to configure the root logger. Each entrypoint
calls it once:
```python
from app.logging_config import setup_logging
setup_logging(__name__)
```
Library modules just do `logger = logging.getLogger(__name__)` — they NEVER
configure root.
| Env | Handler | Format |
|-----|---------|--------|
| `DEBUG=1` | `rich.logging.RichHandler` | colored, clickable file:line, pretty tracebacks |
| (default) | stdlib `StreamHandler` | JSON to stderr (`ts`, `lvl`, `logger`, `service`, `msg`, optional `request_id`) |
`LOG_LEVEL` overrides the level (default `DEBUG` when `DEBUG=1`, else `INFO`).
`DEBUG` and `LOG_LEVEL` are read at process start by `app/main.py` to decide
whether to mount the toolbar middleware and configure logging handlers. The
DuckDB connection wrapper in `src/db.py` reads `DEBUG` at call time, so tests
can toggle it via `monkeypatch.setenv` — but the toolbar itself only mounts
on initial app construction.
## Request correlation
`RequestIdMiddleware` is mounted unconditionally on the FastAPI app. It assigns
or propagates `X-Request-ID` and exposes it via the `request_id_var`
ContextVar so the JSON formatter and the debug-toolbar logging panel see the
same id.
## Debug toolbar
### What it is
Per-request HTML overlay that surfaces what the server did to produce the
page in front of you — headers, routes matched, every DuckDB query, log
records, timing — without leaving the browser. Powered by
[`fastapi-debug-toolbar`](https://github.com/mongkok/fastapi-debug-toolbar)
plus a custom `DuckDBPanel` (see `app/debug_panels/duckdb_panel.py`) that
intercepts every `con.execute(sql, params)` from `src/db.py`.
The toolbar is mounted innermost so it sees raw HTML before
`_SelectiveGZipMiddleware` compresses the body, and gated by `DEBUG=1` —
**never imported in production**. The dev dependency group
(`uv pip install ".[dev]"`) is the only place `fastapi-debug-toolbar` lives.
### Enabling it
```bash
DEBUG=1 uv run uvicorn app.main:app --reload --port 8011
```
Or persist in `.env` at repo root (auto-loaded by uvicorn):
```env
DEBUG=1
LOG_LEVEL=DEBUG
SESSION_SECRET=<32+ chars>
```
Visit any HTML page (`/setup`, `/login`, `/dashboard`, `/admin/access`) →
small collapsed handle on the right edge of the viewport → click to expand.
### Panels
| Panel | Shows |
|-------|-------|
| **Headers** | Request + response headers (incl. `x-request-id`) |
| **Routes** | All registered FastAPI routes; matched route highlighted |
| **Settings** | Pydantic settings, `instance_config` values |
| **Versions** | Installed package versions (Python, FastAPI, deps) |
| **Timer** | Wall-clock + CPU time for the request |
| **Logging** | Every `logger.*` call during the request, with rid prefix |
| **DuckDB** | Every SQL via `src/db.py` — DB tag (`system`/`analytics`/`analytics_ro`), parameters, duration, row count |
Profiling panel (pyinstrument) intentionally omitted — clashes with
uvicorn's async task context. Re-enable in `app/main.py` if you set
`PROFILER_OPTIONS={"async_mode": "disabled"}` or swap profilers.
JSON-only endpoints (Swagger UI at `/docs`) replay the most recent
request's panels via a cookie mechanism — open `/docs`, fire a request,
then navigate to any HTML page to inspect it.
### When to reach for it
| Symptom | Panel |
|---------|-------|
| "Why is this page slow?" | Timer + DuckDB (look for N+1 or unindexed scans) |
| "Which route handler ran?" | Routes |
| "Which user / session did the server see?" | Headers + Logging |
| "Why is this query returning N rows?" | DuckDB (full SQL + params + tag) |
| "Did this log line fire?" | Logging |
| "Is rid propagating end-to-end?" | Headers (`x-request-id`) + Logging (rid prefix on every line) |
### Forcing an error page (for testing)
Two dev-only routes (mounted only when `DEBUG=1`, otherwise 404):
| URL | Behavior |
|-----|----------|
| `/_debug/throw/http/{code}` | Raises `HTTPException(code)` → goes through `StarletteHTTPException` handler → renders `error.html` for any code (`/_debug/throw/http/404`, `/_debug/throw/http/418`, `/_debug/throw/http/500`, …). Matched route, so the toolbar mounts. |
| `/_debug/throw/exc` | Raises unhandled `KeyError` → goes through `_unhandled_exception_handler` → renders the **5xx path**, including the `Traceback ` block (DEBUG-only). **Toolbar NOT injected on this page** — see note below. |
Both echo the active `x-request-id` in response header and `Reference: `
on the rendered error page.
**Toolbar gap on unhandled exceptions.** `fastapi-debug-toolbar` uses
`BaseHTTPMiddleware`, which composes poorly with Starlette's
`ServerErrorMiddleware`: when the route raises a bare `Exception` (not
`HTTPException`), the exception propagates past the toolbar's
`call_next` boundary before any response is sent, so the toolbar dispatch
never sees a response body to inject into. The 500 page is produced
*outside* the toolbar. Use `/_debug/throw/http/500` instead to eyeball
the 500 chrome WITH toolbar panels. Use `/_debug/throw/exc` only to
verify the unhandled-exception code path itself (traceback ``
block, JSON 500 body).
### Source
- Mount + show-callback: `app/main.py` (search for `DebugToolbarMiddleware`)
- DuckDB panel: `app/debug_panels/duckdb_panel.py`
- Dev throw routes: `app/web/router.py` (`/_debug/throw/...`)
## Running locally
```bash
uv pip install ".[dev]"
DEBUG=1 LOCAL_DEV_MODE=1 uv run uvicorn app.main:app --reload --port 8000
```
Open `http://localhost:8000/dashboard`.
## Bot logs
The telegram bot writes to stdout (captured by Docker). Read its logs with:
```bash
docker compose logs -f notify-bot
```
(Previously bots wrote to `/data/notifications/bot.log` via a FileHandler. That
file is no longer produced; use `docker logs` for runtime tail.)