__init__.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. """
  2. __init__.py -
  3. auther: tanlie
  4. date: 2026/8/30
  5. """
  6. import logging
  7. import os
  8. from logging.handlers import RotatingFileHandler
  9. import traceback
  10. import re
  11. from fastapi import FastAPI, Request, HTTPException
  12. from fastapi.exceptions import RequestValidationError
  13. from fastapi.responses import JSONResponse
  14. from app.mobile import register_mobile_routes
  15. from app.core.response import error_response
  16. # Configure logging: write to logs/app.log (rotating) and also to console. Avoid duplicate handlers.
  17. logger = logging.getLogger("app")
  18. if not logger.handlers:
  19. # Determine logs directory relative to this file (project-root/app/../logs)
  20. base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
  21. log_dir = os.path.join(base_dir, "logs")
  22. os.makedirs(log_dir, exist_ok=True)
  23. log_file = os.path.join(log_dir, "app.log")
  24. formatter = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
  25. # Rotating file handler
  26. file_handler = RotatingFileHandler(log_file, maxBytes=10 * 1024 * 1024, backupCount=7, encoding="utf-8")
  27. file_handler.setFormatter(formatter)
  28. file_handler.setLevel(logging.INFO)
  29. logger.addHandler(file_handler)
  30. # Console handler
  31. console_handler = logging.StreamHandler()
  32. console_handler.setFormatter(formatter)
  33. console_handler.setLevel(logging.INFO)
  34. logger.addHandler(console_handler)
  35. logger.setLevel(logging.INFO)
  36. def _clean_message(s: str) -> str:
  37. """Normalize exception messages by removing wrapper prefixes like "ValueError,"."""
  38. if not s:
  39. return s
  40. return re.sub(r'^\s*(?:ValueError|Value\s*error)[:,]?\s*', '', str(s), flags=re.I)
  41. def create_app():
  42. app = FastAPI()
  43. register_mobile_routes(app)
  44. @app.exception_handler(RequestValidationError)
  45. async def validation_exception_handler(request: Request, exc: RequestValidationError):
  46. # Build a friendly, JSON-serializable summary of validation errors
  47. errors = []
  48. for e in exc.errors():
  49. loc = e.get("loc", [])
  50. # remove leading location indicators like 'body', 'query', 'path', 'header'
  51. if loc and len(loc) > 0 and str(loc[0]) in ("body", "query", "path", "header"):
  52. loc_parts = loc[1:]
  53. else:
  54. loc_parts = loc
  55. # join remaining location parts into a dotted path for readability
  56. loc_str = ".".join([str(p) for p in loc_parts if p is not None]) if loc_parts else ""
  57. cleaned_msg = _clean_message(e.get("msg", ""))
  58. errors.append({"field": loc_str, "message": cleaned_msg})
  59. # Log validation details for debugging
  60. logger.info("Validation error for %s: %s", request.url.path, errors)
  61. return JSONResponse(
  62. status_code=422,
  63. content=error_response(msg="参数校验失败", code=422, data=errors),
  64. )
  65. @app.exception_handler(HTTPException)
  66. async def http_exception_handler(request: Request, exc: HTTPException):
  67. # Use HTTPException's status_code and detail
  68. detail = exc.detail if exc.detail is not None else ""
  69. logger.warning("HTTPException for %s: %s", request.url.path, detail)
  70. return JSONResponse(
  71. status_code=getattr(exc, "status_code", 500),
  72. content=error_response(msg=str(detail), code=getattr(exc, "status_code", 500)),
  73. )
  74. @app.exception_handler(Exception)
  75. async def generic_exception_handler(request: Request, exc: Exception):
  76. # Map common ValueError to a friendlier 400 parameter error
  77. if isinstance(exc, ValueError):
  78. # Log with stack for debugging
  79. tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
  80. raw_msg = str(exc) or ""
  81. cleaned_msg = _clean_message(raw_msg)
  82. logger.warning("ValueError on %s: %s\n%s", request.url.path, cleaned_msg, tb)
  83. return JSONResponse(
  84. status_code=400,
  85. content=error_response(msg=cleaned_msg or "参数错误", code=400),
  86. )
  87. # Generic fallback to avoid leaking internal stack traces
  88. # Log full stack trace for internal debugging
  89. tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
  90. logger.error("Unhandled exception on %s: %s\n%s", request.url.path, str(exc), tb)
  91. return JSONResponse(
  92. status_code=500,
  93. content=error_response(msg="服务器内部错误", code=500),
  94. )
  95. return app