Day 9: Error Handling & Logging

~5 min read

What I Built

  • Global error handlers for FastAPI (validation, HTTP, and general exceptions)
  • Retry logic with exponential backoff and jitter for resilient external calls
  • Structured logging throughout trade service with context and sanitization
  • Enhanced log sanitization to mask equity and balance fields
  • Comprehensive test coverage for error handling and logging functionality

Code Highlight


# Global error handlers
def register_error_handlers(app: FastAPI):
    @app.exception_handler(RequestValidationError)
    async def validation_exception_handler(request: Request, exc: RequestValidationError):
        logger.warning(f"Validation error: {exc.errors()}")
        return JSONResponse(
            status_code=422,
            content={"detail": "Validation failed", "errors": exc.errors()},
        )

    @app.exception_handler(Exception)
    async def general_exception_handler(request: Request, exc: Exception):
        logger.error(f"Unhandled exception: {exc}", exc_info=True)
        return JSONResponse(
            status_code=500,
            content={"detail": "Internal server error"},
        )

# Retry with exponential backoff
async def retry_with_backoff(
    func: Callable,
    max_retries: int = 3,
    base_delay: int = 1,
    max_delay: int = 60,
    exponential_base: float = 2.0,
    jitter: bool = True,
    exceptions: tuple = (Exception,),
    *args,
    **kwargs,
) -> Any:
    for attempt in range(max_retries):
        try:
            result = await func(*args, **kwargs) if asyncio.iscoroutinefunction(func) else func(*args, **kwargs)
            return result
        except exceptions as e:
            if attempt == max_retries - 1:
                logger.error(f"Failed after {max_retries} retries: {e}")
                raise
            delay = min(base_delay * (exponential_base ** attempt), max_delay)
            if jitter:
                delay *= (0.5 + random.random())
            logger.warning(f"Attempt {attempt + 1}/{max_retries} failed, retrying in {delay:.1f}s: {e}")
            await asyncio.sleep(delay)

# Structured logging in trade service
logger.info(
    "trade_decision_started",
    account_id=str(account_id),
    symbol=symbol,
    action=action.value,
    shares=float(shares),
    entry_price=float(entry_price),
)
          

Architecture Decision

The error handling architecture prioritizes observability and resilience. Global error handlers ensure consistent error responses while logging critical information for debugging. The retry logic uses exponential backoff with jitter to prevent thundering herd problems during service recovery. Structured logging with sanitization provides rich context without leaking sensitive financial data.

Testing Results

All 15 new unit tests pass, covering critical error handling and logging scenarios:

  • Retry logic with success on first attempt, after failures, and exhaustion
  • Custom exception handling and exponential backoff timing
  • Log sanitization for passwords, API keys, balances, and equity fields
  • Recursive masking in nested dictionaries and lists
  • Case-insensitive pattern matching for sensitive fields

Next Steps

Day 10: Pre-commit hooks and security audit - adding automated code quality checks, secret scanning, and comprehensive security validation before production deployment.


Follow @therealkamba on X for regular updates. View all posts →