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__ på 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:
- Arver fra
HTTPException(slik at FastAPI returnerer riktig HTTP-statuskode) - 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
Oppgave 4.1 - Lag exceptions.py
- Lag filen
exceptions.pymed exception-klassene fra eksempelet over - Oppdater
main.pytil å importere og bruke de nye exception-klassene istedenforHTTPException - Test at appen fortsatt fungerer - besøk
/products/999og sjekk at:- Du får en 404-feilmelding tilbake
- Meldingen dukker opp i loggen automatisk
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?
Oppgave 4.3 - Ekstra kontekst i loggene
Utvid exception-klassene slik at de logger mer nyttig informasjon. For eksempel:
- Legg til et valgfritt
context-parameter som kan inneholde ekstra data - 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"}
)
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å! 😎