Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion app/core/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from sqlspec.adapters.pymysql import PyMysqlConfig

from app.core.logging import get_logger
from app.utils.exceptions import DomainError

logger = get_logger()

Expand Down Expand Up @@ -77,5 +78,7 @@ def get_session() -> Generator:
f"Failed to rollback transaction: {rollback_error}",
exc_info=True,
)
logger.error(f"Database session failed: {e}", exc_info=True)
# A rejected request travelling through the session is not a database failure.
if not isinstance(e, DomainError):
logger.error(f"Database session failed: {e}", exc_info=True)
raise
83 changes: 15 additions & 68 deletions app/core/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

import json
import logging
import os
import time
from datetime import datetime
from logging.handlers import RotatingFileHandler
from os import getenv

_RESERVED_RECORD_KEYS = set(logging.makeLogRecord({}).__dict__) | {
"message",
"asctime",
"taskName",
}


class JsonFormatter(logging.Formatter):
def format(self, record) -> str:
Expand All @@ -19,34 +23,14 @@ def format(self, record) -> str:
"message": record.getMessage(),
}

user_id = getattr(record, "user_id", None)
if user_id is not None:
log_data["user_id"] = user_id

endpoint = getattr(record, "endpoint", None)
if endpoint:
log_data["endpoint"] = endpoint

method = getattr(record, "method", None)
if method:
log_data["method"] = method

status_code = getattr(record, "status_code", None)
if status_code:
log_data["status_code"] = status_code

duration_ms = getattr(record, "duration_ms", None)
if duration_ms is not None:
log_data["duration_ms"] = duration_ms

ip = getattr(record, "ip", None)
if ip:
log_data["ip"] = ip
for key, value in record.__dict__.items():
if key not in _RESERVED_RECORD_KEYS:
log_data[key] = value

if record.exc_info:
log_data["exception"] = self.formatException(record.exc_info)

return json.dumps(log_data)
return json.dumps(log_data, default=str)


class ConsoleFormatter(logging.Formatter):
Expand Down Expand Up @@ -74,26 +58,11 @@ def format(self, record):

message = record.getMessage()

extras = []
user_id = getattr(record, "user_id", None)
if user_id is not None:
extras.append(f"user={user_id}")

endpoint = getattr(record, "endpoint", None)
if endpoint:
extras.append(f"endpoint={endpoint}")

method = getattr(record, "method", None)
if method:
extras.append(f"method={method}")

status_code = getattr(record, "status_code", None)
if status_code:
extras.append(f"status={status_code}")

duration_ms = getattr(record, "duration_ms", None)
if duration_ms:
extras.append(f"duration={duration_ms}")
extras = [
f"{key}={value}"
for key, value in record.__dict__.items()
if key not in _RESERVED_RECORD_KEYS and value is not None
]

extra_str = f" | {' | '.join(extras)}" if extras else ""

Expand Down Expand Up @@ -141,28 +110,6 @@ def setup_logging(cls, app=None):

cls._logger.addHandler(console_handler)

if is_production:
log_dir = "logs"
os.makedirs(log_dir, exist_ok=True)

file_handler = RotatingFileHandler(
f"{log_dir}/drssed-api.log",
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=10,
)
file_handler.setFormatter(JsonFormatter())
file_handler.setLevel(log_level)
cls._logger.addHandler(file_handler)

error_handler = RotatingFileHandler(
f"{log_dir}/drssed-errors.log",
maxBytes=5 * 1024 * 1024, # 5MB
backupCount=5,
)
error_handler.setFormatter(JsonFormatter())
error_handler.setLevel(logging.ERROR)
cls._logger.addHandler(error_handler)

logging.getLogger("werkzeug").setLevel(logging.WARNING)

cls._initialized = True
Expand Down
2 changes: 2 additions & 0 deletions app/utils/exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
ClothingNotFoundError,
ClothingValidationError,
ConflictError,
DomainError,
NotFoundError,
OutfitNotFoundError,
OutfitPermissionError,
Expand Down Expand Up @@ -83,6 +84,7 @@
)

__all__ = [
"DomainError",
"ValidationError",
"NotFoundError",
"ConflictError",
Expand Down
16 changes: 10 additions & 6 deletions app/utils/exceptions/base.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
class ValidationError(Exception):
class DomainError(Exception):
"""Base for errors that describe a rejected request rather than a failure."""


class ValidationError(DomainError):
def __init__(self, message="Validation failed"):
super().__init__(message)


class NotFoundError(Exception):
class NotFoundError(DomainError):
def __init__(self, message="Resource not found"):
super().__init__(message)


class ConflictError(Exception):
class ConflictError(DomainError):
def __init__(self, message="Resource already exists", field=None):
super().__init__(message)
self.field = field


class PermissionError(Exception):
class PermissionError(DomainError):
def __init__(self, message="Permission denied"):
super().__init__(message)


class UnauthorizedError(Exception):
class UnauthorizedError(DomainError):
def __init__(self, message="Invalid credentials"):
super().__init__(message)


class UnprocessableEntityError(Exception):
class UnprocessableEntityError(DomainError):
def __init__(self, message="Unprocessable request") -> None:
super().__init__(message)

Expand Down
Loading