diff --git a/backend/common/log.py b/backend/common/log.py index fd0fe738..643c9f83 100644 --- a/backend/common/log.py +++ b/backend/common/log.py @@ -3,6 +3,7 @@ import inspect import logging import os +import re import sys from asgi_correlation_id import correlation_id @@ -36,6 +37,18 @@ class InterceptHandler(logging.Handler): logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage()) +def default_formatter(record): + """默认日志格式化程序""" + + # 重写 sqlalchemy echo 输出 + # https://github.com/sqlalchemy/sqlalchemy/discussions/12791 + record_name = record['name'] or '' + if record_name.startswith('sqlalchemy'): + record['message'] = re.sub(r'\s+', ' ', record['message']).strip() + + return settings.LOG_FORMAT if settings.LOG_FORMAT.endswith('\n') else f'{settings.LOG_FORMAT}\n' + + def setup_logging() -> None: """ 设置日志处理器 @@ -48,9 +61,11 @@ def setup_logging() -> None: logging.root.handlers = [InterceptHandler()] logging.root.setLevel(settings.LOG_STD_LEVEL) - # 配置日志传播规则 for name in logging.root.manager.loggerDict.keys(): + # 清空所有默认日志处理器 logging.getLogger(name).handlers = [] + + # 配置日志传播规则 if 'uvicorn.access' in name or 'watchfiles.main' in name: logging.getLogger(name).propagate = False else: @@ -59,22 +74,24 @@ def setup_logging() -> None: # Debug log handlers # logging.debug(f'{logging.getLogger(name)}, {logging.getLogger(name).propagate}') + # 移除 loguru 默认处理器 + logger.remove() + # correlation_id 过滤器 # https://github.com/snok/asgi-correlation-id/issues/7 def correlation_id_filter(record): cid = correlation_id.get(settings.TRACE_ID_LOG_DEFAULT_VALUE) - record['correlation_id'] = cid[: settings.TRACE_ID_LOG_UUID_LENGTH] + record['correlation_id'] = cid[: settings.TRACE_ID_LOG_LENGTH] return record # 配置 loguru 处理器 - logger.remove() # 移除默认处理器 logger.configure( handlers=[ { 'sink': sys.stdout, 'level': settings.LOG_STD_LEVEL, + 'format': default_formatter, 'filter': lambda record: correlation_id_filter(record), - 'format': settings.LOG_STD_FORMAT, } ] ) @@ -100,7 +117,7 @@ def set_custom_logfile() -> None: # 日志文件通用配置 # https://loguru.readthedocs.io/en/stable/api/logger.html#loguru._logger.Logger.add log_config = { - 'format': settings.LOG_FILE_FORMAT, + 'format': default_formatter, 'enqueue': True, 'rotation': '00:00', 'retention': '7 days', @@ -110,7 +127,7 @@ def set_custom_logfile() -> None: # 标准输出文件 logger.add( str(log_access_file), - level=settings.LOG_ACCESS_FILE_LEVEL, + level=settings.LOG_FILE_ACCESS_LEVEL, filter=lambda record: record['level'].no <= 25, backtrace=False, diagnose=False, @@ -120,7 +137,7 @@ def set_custom_logfile() -> None: # 标准错误文件 logger.add( str(log_error_file), - level=settings.LOG_ERROR_FILE_LEVEL, + level=settings.LOG_FILE_ERROR_LEVEL, filter=lambda record: record['level'].no >= 30, backtrace=True, diagnose=True, diff --git a/backend/core/conf.py b/backend/core/conf.py index a86027ef..a5219aa8 100644 --- a/backend/core/conf.py +++ b/backend/core/conf.py @@ -143,22 +143,24 @@ class Settings(BaseSettings): IP_LOCATION_REDIS_PREFIX: str = 'fba:ip:location' IP_LOCATION_EXPIRE_SECONDS: int = 60 * 60 * 24 # 1 天 - # 日志(Trace ID) + # Trace ID TRACE_ID_REQUEST_HEADER_KEY: str = 'X-Request-ID' + TRACE_ID_LOG_LENGTH: int = 32 # UUID 长度,必须小于等于 32 TRACE_ID_LOG_DEFAULT_VALUE: str = '-' - TRACE_ID_LOG_UUID_LENGTH: int = 32 # UUID 长度,必须小于等于 32 + + # 日志 + LOG_FORMAT: str = ( + '{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {correlation_id} | {message}' + ) # 日志(控制台) LOG_STD_LEVEL: str = 'INFO' - LOG_STD_FORMAT: str = ( - '{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {correlation_id} | {message}' - ) + # 日志(文件) - LOG_ACCESS_FILE_LEVEL: str = 'INFO' - LOG_ERROR_FILE_LEVEL: str = 'ERROR' + LOG_FILE_ACCESS_LEVEL: str = 'INFO' + LOG_FILE_ERROR_LEVEL: str = 'ERROR' LOG_ACCESS_FILENAME: str = 'fba_access.log' LOG_ERROR_FILENAME: str = 'fba_error.log' - LOG_FILE_FORMAT: str = '{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {correlation_id} | {message}' # 操作日志 OPERA_LOG_PATH_EXCLUDE: list[str] = [