Exceptions med Logging

Skip to content

Info

I Level 3 brukte vi HTTPException direkte og logget manuelt hver gang. Det fungerer, men det er lett å glemme å logge - og da mister du verdifull informasjon! Hva om exceptions automatisk logget seg selv? 🤔

Del 1 - Problemet

Se på dette mønsteret fra Level 3:

if product_id not in products:
    logger.warning(f"Produkt {product_id} finnes ikke!")
    raise HTTPException(status_code=404, detail="Produkt ikke funnet")

Det er to linjer som alltid hører sammen - logging og exception. Hva skjer om du glemmer å logge? Da vet du ikke at feilen skjedde! Og hva om du har 20 endpoints som alle gjør det samme? Da er det mye repeterende kode. 😩

Løsningen: lag egne exception-klasser som logger automatisk!

Del 2 - Arv og egne Exceptions

I Python kan du lage egne exception-klasser ved å arve fra eksisterende klasser. Kort forklart betyr arv at du lager en ny klasse som får alle egenskapene til en annen klasse, pluss det du legger til selv.

Her er et enkelt eksempel uten FastAPI:

class AppError(Exception):
    """En feil som automatisk printer en melding."""
    def __init__(self, message: str):
        self.message = message
        print(f"FEIL: {message}")  # Skjer automatisk!
        super().__init__(message)

# Når vi raiser denne, printes meldingen automatisk
raise AppError("Noe gikk galt")
# Output: FEIL: Noe gikk galt

Nøkkelordet super() kaller __init__foreldreklassen (altså Exception), slik at exception-en fortsatt fungerer som normalt.

Del 3 - Egne HTTP Exceptions med logging

Nå tar vi dette konseptet og kombinerer det med FastAPI og logging. Vi lager exceptions som:

  1. Arver fra HTTPException (slik at FastAPI returnerer riktig HTTP-statuskode)
  2. Logger automatisk med riktig nivå
# exceptions.py
from fastapi import HTTPException
from logger_config import setup_logger

logger = setup_logger("exceptions")


class NotFoundError(HTTPException):
    """Ressurs ble ikke funnet (404)."""
    def __init__(self, detail: str = "Ressurs ikke funnet"):
        logger.warning(detail)
        super().__init__(status_code=404, detail=detail)


class BadRequestError(HTTPException):
    """Ugyldig forespørsel (400)."""
    def __init__(self, detail: str = "Ugyldig forespørsel"):
        logger.warning(detail)
        super().__init__(status_code=400, detail=detail)


class InternalError(HTTPException):
    """Intern serverfeil (500)."""
    def __init__(self, detail: str = "Intern serverfeil"):
        logger.error(detail)
        super().__init__(status_code=500, detail=detail)


class ForbiddenError(HTTPException):
    """Ingen tilgang (403)."""
    def __init__(self, detail: str = "Ingen tilgang"):
        logger.error(detail)
        super().__init__(status_code=403, detail=detail)
Hvorfor forskjellige logg-nivåer? 🤔
  • 404 og 400 bruker WARNING - dette er “forventet” feil. En bruker som spør etter noe som ikke finnes er ikke en krise, men vi vil vite om det.
  • 500 og 403 bruker ERROR - dette er mer alvorlig. En intern feil betyr at noe er galt med koden vår, og en 403 kan bety at noen prøver å gjøre noe de ikke skal.

Før og etter

Se hvor mye enklere koden blir:

Før (manuell logging):

@app.get("/products/{product_id}")
async def get_product(product_id: int):
    logger.debug(f"Forespørsel for product_id={product_id}")
    if product_id not in products:
        logger.warning(f"Produkt {product_id} finnes ikke!")
        raise HTTPException(status_code=404, detail="Produkt ikke funnet")
    return products[product_id]

Etter (automatisk logging):

@app.get("/products/{product_id}")
async def get_product(product_id: int):
    logger.debug(f"Forespørsel for product_id={product_id}")
    if product_id not in products:
        raise NotFoundError(f"Produkt {product_id} finnes ikke!")
    return products[product_id]

Én linje istedenfor to, og du kan aldri glemme å logge! 🎉

Filstruktur

📁fastapi_logging
    🐍main.py
    🐍logger_config.py
    🐍exceptions.py
    📄requirements.txt

Oppgaver

Easy Oppgave 4.1 - Lag exceptions.py

  1. Lag filen exceptions.py med exception-klassene fra eksempelet over
  2. Oppdater main.py til å importere og bruke de nye exception-klassene istedenfor HTTPException
  3. Test at appen fortsatt fungerer - besøk /products/999 og sjekk at:
    • Du får en 404-feilmelding tilbake
    • Meldingen dukker opp i loggen automatisk

Medium Oppgave 4.2 - Utvid med flere exceptions

Legg til minst to nye exception-klasser basert på behov i appen din. For eksempel:

  • ConflictError (409) - Når noe allerede finnes (f.eks. prøver å opprette et produkt med et navn som allerede er i bruk)
  • ValidationError (422) - Når data ikke er gyldig (f.eks. negativ pris)

Bruk dem i relevante endpoints og sjekk at loggingen fungerer.

Tips

Tenk over hva slags logg-nivå som passer for hver ny exception. Ikke alt trenger å være ERROR - en konflikt er kanskje bare en WARNING?

Medium Oppgave 4.3 - Ekstra kontekst i loggene

Utvid exception-klassene slik at de logger mer nyttig informasjon. For eksempel:

  1. Legg til et valgfritt context-parameter som kan inneholde ekstra data
  2. Logg denne konteksten på DEBUG-nivå (slik at den havner i filen, men ikke i terminalen)
class NotFoundError(HTTPException):
    def __init__(self, detail: str = "Ressurs ikke funnet", context: dict = None):
        logger.warning(detail)
        if context:
            logger.debug(f"Kontekst: {context}")
        super().__init__(status_code=404, detail=detail)

Bruk:

raise NotFoundError(
    f"Produkt {product_id} finnes ikke!",
    context={"product_id": product_id, "endpoint": "/products"}
)

Hard Oppgave 4.4 - Felles baseklasse

Alle exception-klassene våre har ganske lik struktur. Lag en felles baseklasse som håndterer logging, slik at de spesifikke klassene blir enda enklere:

Hint
class LoggedException(HTTPException):
    """Baseklasse for exceptions som logger automatisk."""
    _status_code: int = 500
    _log_level: int = logging.ERROR

    def __init__(self, detail: str, context: dict = None):
        logger.log(self._log_level, detail)
        if context:
            logger.debug(f"Kontekst: {context}")
        super().__init__(status_code=self._status_code, detail=detail)


class NotFoundError(LoggedException):
    _status_code = 404
    _log_level = logging.WARNING


class InternalError(LoggedException):
    _status_code = 500
    _log_level = logging.ERROR

Se hvor lite kode hver exception trenger nå! 😎