Skip to main content

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

LevelWhenExample
DEBUGDiagnostic detail, not in production"Parsing verification output"
INFONormal operations, key events"User logged in", "Container created"
WARNINGUnexpected but not breaking"Container not found during cleanup"
ERRORSomething 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

  1. Use logger = logging.getLogger(__name__) — one logger per module, never use the root logger
  2. Never use print() — always use the logger
  3. Log at error site — where you catch the exception, not deep in helpers
  4. Always log before raising 500logger.error(...) then raise HTTPException(500, ...)
  5. Include context in messages — user_id, attempt_id, container_name, exam_id
  6. Never log secrets — no passwords, tokens, SSH keys
  7. Don't log inside tight loops — log summaries, not per-iteration noise