|
| 1 | +--- |
| 2 | +paths: |
| 3 | + - "backend/**/*.py" |
| 4 | +--- |
| 5 | + |
| 6 | +# Backend logging |
| 7 | + |
| 8 | +## All logging goes through loguru |
| 9 | + |
| 10 | +- Import as `from loguru import logger`. Do not `import logging` or `getLogger(...)` in app code. |
| 11 | +- `setup_logger()` in `kayman/util.py` is called once from `main.py` at import time. It removes the default handler, adds the env-appropriate sink, and installs an `InterceptHandler` so stdlib logging (uvicorn, FastAPI, SQLAlchemy, alembic) is bridged to loguru. Don't add sinks or call `logger.remove()` elsewhere. |
| 12 | +- Library-style `logger = logging.getLogger(__name__)` per module is unnecessary in loguru. Use the single global `logger` and rely on loguru's auto-captured `name`/`function`/`line`. |
| 13 | + |
| 14 | +## Level per environment |
| 15 | + |
| 16 | +`settings.ENVIRONMENT` is the single source of truth. Pytest runs auto-coerce it to `testing` (see `_force_testing_under_pytest` in `core/config.py`). The level map lives in `_LOG_LEVELS` in `util.py`: |
| 17 | + |
| 18 | +| Environment | Level | |
| 19 | +| ------------- | --------- | |
| 20 | +| `local` | `DEBUG` | |
| 21 | +| `development` | `DEBUG` | |
| 22 | +| `testing` | `WARNING` | |
| 23 | +| `production` | `INFO` | |
| 24 | + |
| 25 | +- Tests stay quiet by default because pytest forces `testing`. Use loguru's `caplog` shim when a test needs to assert on log output. |
| 26 | +- Don't rely on `LOGURU_LEVEL` env var, it doesn't override the explicit `level=` passed to `logger.add(...)`. |
| 27 | + |
| 28 | +## When to use which level |
| 29 | + |
| 30 | +- `DEBUG`: noisy diagnostics useful during development (query params, computed intermediates). Never assume DEBUG is enabled at the call site. |
| 31 | +- `INFO`: normal lifecycle events worth keeping in production (app started, migration applied, scheduled job ran). |
| 32 | +- `WARNING`: unexpected but recoverable (retry kicked in, falling back to default, deprecated path hit). |
| 33 | +- `ERROR`: a request or job failed with user-visible impact. Include the exception with `logger.exception(...)` or `logger.opt(exception=True).error(...)`. |
| 34 | +- `CRITICAL`: process can't continue (DB unreachable at startup, required secret missing). |
| 35 | +- Don't log at `INFO` inside hot loops. If you're tempted, it's `DEBUG`. |
| 36 | + |
| 37 | +## Structured output |
| 38 | + |
| 39 | +- Production sink uses `serialize=True` (JSON lines) so the log aggregator can parse fields directly. |
| 40 | +- Local sink stays human-readable with colors. |
| 41 | +- Attach request-scoped context via `logger.bind(request_id=..., user_id=...)` in a FastAPI middleware, not by string-formatting into the message. |
| 42 | + |
| 43 | +## Safety |
| 44 | + |
| 45 | +- In `production`, configure the sink with `diagnose=False` and `backtrace=False`. Loguru's diagnose mode dumps local variables on exception, which can leak secrets, tokens, or PII. |
| 46 | +- This is a personal finance app: never log full account numbers, balances tied to an identifiable user, raw auth tokens, password hashes, or `Authorization` headers. Redact or use a stable surrogate (`user_id`, `account_id`). |
| 47 | +- Use `enqueue=True` on the production sink if uvicorn runs with multiple workers, to keep writes process-safe. |
| 48 | + |
| 49 | +## Exceptions |
| 50 | + |
| 51 | +- Prefer `logger.exception("what failed")` inside `except` blocks over manual traceback formatting. |
| 52 | +- For background tasks and event handlers where an unhandled exception would otherwise be swallowed, wrap the entry point with `@logger.catch(reraise=True)`. |
| 53 | +- Don't `except Exception: logger.error(e)` and continue, either re-raise or handle it deliberately. |
| 54 | + |
| 55 | +## Don'ts |
| 56 | + |
| 57 | +- No `print()` for diagnostics, ever. It bypasses sinks, levels, and JSON formatting. |
| 58 | +- Don't f-string into the message when the value comes from user input that could include format placeholders. Pass values as `extra=` or as positional args to loguru's `{}`-style template. |
| 59 | +- Don't log inside import-time code in `__init__.py`. Sinks aren't necessarily configured yet. |
0 commit comments