From 22e8dfb34bdf96beaef6443de8288ecd3f539114 Mon Sep 17 00:00:00 2001 From: David Riegel <40246197+davidriegel@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:49:52 +0200 Subject: [PATCH] fix: keep structured log fields and stop logging domain errors as database failures --- app/core/database.py | 5 +- app/core/logging.py | 83 ++++++-------------------------- app/utils/exceptions/__init__.py | 2 + app/utils/exceptions/base.py | 16 +++--- 4 files changed, 31 insertions(+), 75 deletions(-) diff --git a/app/core/database.py b/app/core/database.py index 3a9d709..f254c9b 100755 --- a/app/core/database.py +++ b/app/core/database.py @@ -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() @@ -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 diff --git a/app/core/logging.py b/app/core/logging.py index b95b431..5c9b7ed 100644 --- a/app/core/logging.py +++ b/app/core/logging.py @@ -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: @@ -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): @@ -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 "" @@ -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 diff --git a/app/utils/exceptions/__init__.py b/app/utils/exceptions/__init__.py index 7ddf669..9790e6d 100644 --- a/app/utils/exceptions/__init__.py +++ b/app/utils/exceptions/__init__.py @@ -3,6 +3,7 @@ ClothingNotFoundError, ClothingValidationError, ConflictError, + DomainError, NotFoundError, OutfitNotFoundError, OutfitPermissionError, @@ -83,6 +84,7 @@ ) __all__ = [ + "DomainError", "ValidationError", "NotFoundError", "ConflictError", diff --git a/app/utils/exceptions/base.py b/app/utils/exceptions/base.py index 512ab98..a38d96d 100644 --- a/app/utils/exceptions/base.py +++ b/app/utils/exceptions/base.py @@ -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)