Logging (Python / FastAPI)
Standard: Python logging module
No external logging libraries. Use Python's stdlib logging. Output goes to stdout via Docker.
Setup
# backend/app/main.py (or any module)
import logging
logger = logging.getLogger(__name__)
Each module gets its own logger via __name__. This gives log messages like app.api.exams, app.services.exam_verifier.
Levels
| Level | When | Example |
|---|---|---|
DEBUG | Diagnostic detail, not in production | "Parsing verification output" |
INFO | Normal operations, key events | "User logged in", "Container created" |
WARNING | Unexpected but not breaking | "Container not found during cleanup" |
ERROR | Something failed, needs attention | "SSH connection failed", "LXD error" |
Usage
import logging
logger = logging.getLogger(__name__)
# GOOD — descriptive, includes context
logger.info(f"Exam attempt {attempt_id} submitted by user {user_id}")
logger.error(f"Verification script failed for attempt {attempt_id}: {e}")
logger.warning(f"Container {container_id} not found during cleanup, skipping")
# BAD — no context
logger.info("Exam submitted")
logger.error("Error")
print("Something happened") # Never use print for logging
What to Log
Always log at INFO:
- User login / logout
- Container creation and deletion
- Exam start and submission
- Lab start and submission
- Admin actions (role changes, user deletion)
Always log at ERROR (before raising 500):
- LXD API failures
- SSH connection failures
- Verification script failures
- Unexpected exceptions in endpoints
Never log:
- Passwords or JWT tokens
- Full request/response bodies (may contain sensitive data)
- PII beyond user_id (no emails, usernames in error logs)
Patterns
# Endpoint-level logging — log actions and errors here
@router.post("/exams/{exam_id}/submit")
async def submit_exam(exam_id: int, ...):
logger.info(f"Exam {exam_id} submission started by user {current_user.id}")
try:
result = await verify_exam_attempt(attempt)
logger.info(f"Exam {exam_id} verified: score={result.score}")
return result
except Exception as e:
logger.error(f"Exam {exam_id} submission failed for user {current_user.id}: {e}")
raise HTTPException(status_code=500, detail="Submission failed")
# Service-level logging — log significant operations
class ExamVerifier:
def __init__(self):
self.logger = logging.getLogger(__name__)
async def verify(self, attempt_id: int, container_name: str):
self.logger.info(f"Verifying attempt {attempt_id} in container {container_name}")
try:
output = await execute_script_via_ssh(container_name, script)
passed = self._parse_output(output)
self.logger.info(f"Attempt {attempt_id} verification result: {'PASS' if passed else 'FAIL'}")
return passed
except Exception as e:
self.logger.error(f"SSH verification failed for attempt {attempt_id}: {e}")
raise
Rules
- Use
logger = logging.getLogger(__name__)— one logger per module, never use the root logger - Never use
print()— always use the logger - Log at error site — where you catch the exception, not deep in helpers
- Always log before raising 500 —
logger.error(...)thenraise HTTPException(500, ...) - Include context in messages — user_id, attempt_id, container_name, exam_id
- Never log secrets — no passwords, tokens, SSH keys
- Don't log inside tight loops — log summaries, not per-iteration noise