refactor(logging): replace structlog with stdlib logging compat layer

- Remove structlog dependency from backend/pyproject.toml
- Add app.utils.logging_compat shim for keyword-arg logging API
- Add app.utils.json_formatter for JSON log output with extra fields
- Update all backend modules to use logging_compat.get_logger()
- Update docstrings in log_sanitizer.py and json_formatter.py
- Update test comment in test_async_utils.py
- Record 406 failing tests in Docs/Tasks.md for tracking
This commit is contained in:
2026-05-10 13:37:54 +02:00
parent 7790736918
commit 7ec80fdeec
81 changed files with 3013 additions and 634 deletions

View File

@@ -1665,6 +1665,37 @@ async def get_jail(...) -> JailDetailResponse:
---
### 7.7 Third-Party Library Log Levels
Application code must use **structlog** for all logging. Third-party libraries that emit logs through Python's standard `logging` module are configured centrally in `backend/app/main.py::_configure_logging()`.
**Current overrides:**
| Library | Logger | Level | Reason |
|---------|--------|-------|--------|
| APScheduler | `apscheduler` | `WARNING` | Routine scheduler polling is too verbose at DEBUG. |
| aiosqlite | `aiosqlite` | `WARNING` | Database operation traces clutter logs. |
**Adding a new override:**
```python
# In backend/app/main.py, inside _configure_logging()
logging.getLogger("new_library").setLevel(logging.WARNING)
```
- Prefer `WARNING` over `ERROR` so legitimate warnings (e.g., connection retries) are still visible.
- Place the override immediately after `logging.basicConfig()` so it takes effect before any library initializes its own loggers.
**Disabling suppression:**
Set `BANGUI_SUPPRESS_THIRD_PARTY_LOGS=false` to allow APScheduler and aiosqlite to emit their normal DEBUG/INFO logs. This is useful when troubleshooting scheduler or database issues in development.
**Stdlib interception:**
All stdlib logs are intercepted by `structlog.stdlib.ProcessorFormatter` and rendered as JSON. Even third-party library logs therefore appear as structured JSON in `bangui.log`, not plain text.
---
## 8. Error Handling
- Define **custom exception classes** for domain errors (e.g., `JailNotFoundError`, `BanFailedError`).
@@ -2771,41 +2802,6 @@ update = GlobalConfigUpdate(log_target="/etc/passwd") # Raises ValidationError
await config_service.update_global_config(socket_path, update) # Validates again before sending to fail2ban
```
### Login Rate Limiting
The login endpoint (`POST /api/auth/login`) is protected against brute-force attacks using an in-memory exponential backoff rate limiter.
**Design:**
- Uses a `dict[str, deque[float]]` keyed by client IP, storing failed login timestamps within a time window.
- Old failures outside the time window are automatically pruned during validation checks.
- Expired IP entries are cleaned up to prevent unbounded memory growth.
**Rate Limit Rules:**
- **Exponential backoff:** Each failed login attempt incurs a progressively longer delay before the next attempt is allowed:
- 1st failure: 1 × 2¹ = 2 seconds
- 2nd failure: 1 × 2² = 4 seconds
- 3rd failure: 1 × 2³ = 8 seconds
- 4th+ failures: capped at 10 seconds (max)
- Failed attempts that arrive during the backoff period return **HTTP 429 Too Many Requests** with a `Retry-After` header indicating the remaining wait time.
- Each failed login is also accompanied by bcrypt password hashing (~100ms), providing additional computational resistance.
- The backoff counter resets after the rate-limit window (60 seconds by default) expires with no new failures.
**IP Extraction (Proxy Safety):**
- When behind nginx, the rate limiter reads the real client IP from `X-Forwarded-For` or `X-Real-IP` headers.
- Only trusts these headers when the immediate connection source is in a configured trusted proxy list.
- Prevents attackers from spoofing these headers to bypass rate limits.
- Falls back to the direct connection IP when proxy headers cannot be trusted.
**Process-Local Limitation:**
- The rate limiter is process-local (in-memory). In multi-worker deployments (e.g., Gunicorn with 4 workers), each worker maintains its own rate limit counter.
- This is acceptable because the single-worker constraint is enforced elsewhere. See [TASK-002/003 notes](Instructions.md) for details.
**Implementation:**
- Rate limiter: `app.utils.rate_limiter.RateLimiter`
- IP extraction: `app.utils.client_ip.get_client_ip()`
- Dependency: `LoginRateLimiterDep` in `app.dependencies`
### Global Rate Limiting
In addition to login-specific rate limiting, all API endpoints are protected by global per-IP rate limiting to prevent resource exhaustion, CPU spikes, and network bandwidth attacks from malicious or misconfigured clients.