|
|
import logging |
|
|
import os |
|
|
import contextvars |
|
|
from logging.handlers import RotatingFileHandler |
|
|
|
|
|
|
|
|
BASE_LOG_DIR = 'logs' |
|
|
MAX_BYTES = 10 * 1024 * 1024 |
|
|
BACKUP_COUNT = 5 |
|
|
|
|
|
|
|
|
task_id_var = contextvars.ContextVar("task_id", default=None) |
|
|
|
|
|
|
|
|
class TaskIdFormatter(logging.Formatter): |
|
|
""" |
|
|
Custom Formatter that injects task_id if available, or 'N/A' if not. |
|
|
Does NOT rely on Filter injection, making it robust for third-party loggers. |
|
|
""" |
|
|
def format(self, record): |
|
|
|
|
|
task_id = task_id_var.get() |
|
|
|
|
|
|
|
|
if not hasattr(record, 'task_id'): |
|
|
record.task_id = task_id if task_id else 'N/A' |
|
|
elif record.task_id is None: |
|
|
|
|
|
record.task_id = 'N/A' |
|
|
|
|
|
return super().format(record) |
|
|
|
|
|
|
|
|
def setup_logging(service_name: str, level=logging.INFO): |
|
|
""" |
|
|
集中配置日志系统,每个服务使用单独的日志文件。 |
|
|
|
|
|
Args: |
|
|
service_name (str): 服务的名称(例如 'app' 或 'worker'),用于命名 logger 和日志文件。 |
|
|
level (int): 日志级别。 |
|
|
""" |
|
|
|
|
|
os.makedirs(BASE_LOG_DIR, exist_ok=True) |
|
|
|
|
|
|
|
|
log_file_name = f'{service_name}.log' |
|
|
log_file_path = os.path.join(BASE_LOG_DIR, log_file_name) |
|
|
|
|
|
|
|
|
logger = logging.getLogger(service_name) |
|
|
logger.setLevel(level) |
|
|
|
|
|
|
|
|
|
|
|
formatter = TaskIdFormatter( |
|
|
'%(asctime)s - [%(task_id)s] - %(name)s - %(levelname)s - %(message)s' |
|
|
) |
|
|
|
|
|
|
|
|
console_handler = logging.StreamHandler() |
|
|
console_handler.setFormatter(formatter) |
|
|
|
|
|
|
|
|
file_handler = RotatingFileHandler( |
|
|
log_file_path, |
|
|
maxBytes=MAX_BYTES, |
|
|
backupCount=BACKUP_COUNT, |
|
|
encoding='utf-8' |
|
|
) |
|
|
file_handler.setFormatter(formatter) |
|
|
|
|
|
|
|
|
|
|
|
abs_log_file_path = os.path.abspath(log_file_path) |
|
|
|
|
|
|
|
|
def attach_handlers_to(target_logger_name): |
|
|
target = logging.getLogger(target_logger_name) |
|
|
target.setLevel(level) |
|
|
|
|
|
|
|
|
has_file_handler = any( |
|
|
isinstance(h, RotatingFileHandler) and |
|
|
os.path.abspath(h.baseFilename) == abs_log_file_path |
|
|
for h in target.handlers |
|
|
) |
|
|
|
|
|
if not has_file_handler: |
|
|
target.addHandler(file_handler) |
|
|
|
|
|
if not any(isinstance(h, logging.StreamHandler) for h in target.handlers): |
|
|
target.addHandler(console_handler) |
|
|
|
|
|
|
|
|
attach_handlers_to('services') |
|
|
|
|
|
|
|
|
|
|
|
attach_handlers_to(service_name) |
|
|
|
|
|
return logger |
|
|
|
|
|
|
|
|
|
|
|
def get_app_logger(): |
|
|
return setup_logging(service_name='app') |
|
|
|
|
|
|
|
|
def get_process_worker_logger(): |
|
|
return setup_logging(service_name='process_worker') |
|
|
|
|
|
|
|
|
def get_upload_worker_logger(): |
|
|
return setup_logging(service_name='upload_worker') |
|
|
|
|
|
|
|
|
class RequestLogger: |
|
|
"""请求日志记录器(用于Flask)""" |
|
|
|
|
|
@staticmethod |
|
|
def log_request(request, response, duration: float = None): |
|
|
""" |
|
|
记录HTTP请求日志 |
|
|
|
|
|
Args: |
|
|
request: Flask request对象 |
|
|
response: Flask response对象 |
|
|
duration: 请求处理时间(秒) |
|
|
""" |
|
|
logger = get_app_logger() |
|
|
|
|
|
|
|
|
msg_parts = [ |
|
|
f"{request.method} {request.path}", |
|
|
f"status={response.status_code}", |
|
|
] |
|
|
|
|
|
if duration is not None: |
|
|
msg_parts.append(f"duration={duration:.3f}s") |
|
|
|
|
|
|
|
|
if request.query_string: |
|
|
msg_parts.append(f"query={request.query_string.decode('utf-8')}") |
|
|
|
|
|
|
|
|
client_ip = request.headers.get('X-Forwarded-For', request.remote_addr) |
|
|
msg_parts.append(f"ip={client_ip}") |
|
|
|
|
|
msg = " | ".join(msg_parts) |
|
|
|
|
|
|
|
|
if response.status_code >= 500: |
|
|
logger.error(msg) |
|
|
elif response.status_code >= 400: |
|
|
logger.warning(msg) |
|
|
else: |
|
|
logger.info(msg) |
|
|
|
|
|
@staticmethod |
|
|
def log_error(request, error: Exception): |
|
|
""" |
|
|
记录错误日志 |
|
|
|
|
|
Args: |
|
|
request: Flask request对象 |
|
|
error: 异常对象 |
|
|
""" |
|
|
logger = get_app_logger() |
|
|
logger.exception( |
|
|
f"请求错误 | {request.method} {request.path} | " |
|
|
f"error={type(error).__name__}: {str(error)}" |
|
|
) |
|
|
|