Files
myaps_api/globalobjects/logger/core.py
T
chaoge bf42299ead 重构: 迁移至统一日志系统
- 新增 globalobjects/logger/ 模块化日志系统
- 支持异步写入、多目标输出、敏感信息脱敏
- 完全向后兼容原有logger API
- 备份旧版本为 logger_v1_backup.py 和 logger_v2_backup.py
- 更新 .env.example 和 AGENTS.md 文档
2026-05-22 00:23:30 +08:00

497 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
统一日志系统 - SmartLogger核心实现
提供统一的日志记录API,保持与现有logger.py完全兼容
"""
import os
import sys
import time
import logging
import threading
import asyncio
from datetime import datetime
from typing import Optional, Dict, Any, List
from .models import LogRecord, LoggerConfig
from .queue import AsyncLogQueue
from .router import LogRouter
from .tracer import StackTraceTracer
from .handlers import ConsoleHandler, SmartFileHandler, DatabaseHandler, WebSocketHandler
from .handlers import _log_stream_manager
from .helpers import LogHelper, emoji_manager
class SmartLogger:
"""
智能日志器
特性:
- 统一的日志记录API
- 异步写入不阻塞
- 多目标输出
- 业务语义化便捷方法
- 完全兼容现有logger.py
"""
_instance = None
_initialized = False
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(
self,
name: str = "app",
config: Optional[LoggerConfig] = None
):
"""
初始化日志器
Args:
name: 日志器名称
config: 日志配置
"""
if self._initialized:
return
self._initialized = True
self._name = name
self._config = config or LoggerConfig.from_env()
self._queue = AsyncLogQueue(
max_size=self._config.queue_size,
batch_size=self._config.batch_size,
flush_interval=self._config.flush_interval
)
self._tracer = StackTraceTracer(enabled=self._config.stack_trace)
self._router = LogRouter(config=self._config, queue=self._queue)
self._console_handler: Optional[ConsoleHandler] = None
self._file_handler: Optional[SmartFileHandler] = None
self._database_handler: Optional[DatabaseHandler] = None
self._websocket_handler: Optional[WebSocketHandler] = None
self._level = self._config.get_level_int()
self._running = False
self._targets = {
'console': self._config.to_console,
'file': self._config.to_file,
'database': self._config.to_database,
'websocket': self._config.to_websocket
}
async def initialize(self) -> None:
"""初始化日志系统"""
if self._running:
return
self._setup_handlers()
await self._queue.start()
self._setup_queue_handlers()
self._running = True
def _setup_handlers(self) -> None:
"""设置处理器"""
if self._targets.get('console', True):
self._console_handler = ConsoleHandler(
level=self._level,
enabled=True,
colorize=True,
show_module=False
)
if self._targets.get('file', True):
self._file_handler = SmartFileHandler(
log_dir=self._config.log_dir,
level=logging.WARNING,
enabled=True,
retention_days=self._config.retention_days
)
if self._targets.get('database', True):
self._database_handler = DatabaseHandler(
level=logging.INFO,
enabled=True,
batch_size=self._config.batch_size,
flush_interval=self._config.flush_interval
)
if self._targets.get('websocket', True):
self._websocket_handler = WebSocketHandler(
level=logging.DEBUG,
enabled=True,
max_connections=100
)
def _setup_queue_handlers(self) -> None:
"""设置队列处理器"""
if self._console_handler:
self._queue.add_handler(self._console_handler.handle)
if self._file_handler:
self._queue.add_handler(self._file_handler.handle)
if self._database_handler:
self._queue.add_handler(self._database_handler.emit)
async def shutdown(self) -> None:
"""关闭日志系统"""
if not self._running:
return
self._running = False
await self._queue.stop()
if self._console_handler:
self._console_handler.close()
if self._file_handler:
self._file_handler.close()
if self._database_handler:
self._database_handler.close()
if self._websocket_handler:
self._websocket_handler.close()
def _log(
self,
level: int,
msg: Any,
*args,
exc_info: bool = False,
**kwargs
) -> None:
"""
内部日志方法
Args:
level: 日志级别
msg: 日志消息
args: 格式化参数
exc_info: 是否捕获异常信息
kwargs: 额外参数
"""
if level < self._level:
return
formatted_msg = self._format_message(msg, args)
caller_info = self._tracer.get_caller_info(skip_frames=3)
stack_trace = None
if exc_info or level >= logging.ERROR:
import traceback
try:
stack_trace = ''.join(traceback.format_stack())
except Exception:
pass
record = LogRecord(
level=level,
level_name=logging.getLevelName(level),
message=formatted_msg,
module=caller_info.get('module') if caller_info else None,
function=caller_info.get('function') if caller_info else None,
line=caller_info.get('line_number') if caller_info else None,
stack_trace=stack_trace,
process_id=os.getpid(),
thread_id=threading.get_ident(),
thread_name=threading.current_thread().name,
extra=kwargs.get('extra')
)
self._send_to_log_stream(record, formatted_msg)
if not self._running:
return
self._router.route(record)
if self._console_handler and self._console_handler.enabled:
self._console_handler.handle(record)
def _format_message(self, msg: Any, args: tuple) -> str:
"""格式化消息"""
if not args:
return str(msg)
try:
return msg % args
except (TypeError, ValueError):
return f"{msg} {' '.join(map(str, args))}"
def _send_to_log_stream(self, record: LogRecord, message: str) -> None:
"""
发送日志到日志流服务
Args:
record: 日志记录对象
message: 格式化后的消息
"""
try:
import logging as logging_module
pathname = record.module or "unknown"
lineno = record.line or 0
std_record = logging_module.LogRecord(
name=self._name,
level=record.level,
pathname=pathname,
lineno=lineno,
msg=message,
args=(),
exc_info=None
)
std_record.module = record.module or "unknown"
std_record.funcName = record.function or ""
std_record.levelname = record.level_name
std_record.created = record.timestamp.timestamp()
std_record.msecs = (std_record.created - int(std_record.created)) * 1000
std_record.message = message
for handler in _log_stream_manager.get_handlers():
handler.emit(std_record)
except Exception:
pass
def debug(self, msg: Any, *args, **kwargs) -> None:
"""记录DEBUG级别日志"""
self._log(logging.DEBUG, msg, *args, **kwargs)
def info(self, msg: Any, *args, **kwargs) -> None:
"""记录INFO级别日志"""
self._log(logging.INFO, msg, *args, **kwargs)
def warning(self, msg: Any, *args, **kwargs) -> None:
"""记录WARNING级别日志"""
self._log(logging.WARNING, msg, *args, **kwargs)
def error(self, msg: Any, *args, **kwargs) -> None:
"""记录ERROR级别日志"""
self._log(logging.ERROR, msg, *args, **kwargs)
def critical(self, msg: Any, *args, **kwargs) -> None:
"""记录CRITICAL级别日志"""
self._log(logging.CRITICAL, msg, *args, **kwargs)
def exception(self, msg: Any, *args, **kwargs) -> None:
"""记录异常日志(自动捕获异常堆栈)"""
import traceback
exc_traceback = traceback.format_exc()
kwargs['extra'] = kwargs.get('extra', {})
kwargs['extra']['exception'] = exc_traceback
self._log(logging.ERROR, msg, *args, exc_info=True, **kwargs)
def success(self, action: str, subject: str = "", details: str = "", to_file: bool = False) -> None:
"""记录成功消息"""
msg = LogHelper.success(action, subject, details)
self.info(msg)
def fail(self, action: str, subject: str = "", reason: str = "", to_file: bool = True) -> None:
"""记录失败消息"""
msg = LogHelper.fail(action, subject, reason)
self.error(msg)
def start(self, action: str, subject: str = "", to_file: bool = False) -> None:
"""记录开始消息"""
msg = LogHelper.start(action, subject)
self.info(msg)
def stop(self, action: str, subject: str = "", to_file: bool = False) -> None:
"""记录结束消息"""
msg = LogHelper.stop(action, subject)
self.info(msg)
def status_change(self, subject: str, old_status: str, new_status: str, to_file: bool = False) -> None:
"""记录状态变更消息"""
msg = LogHelper.status_change(subject, old_status, new_status)
self.info(msg)
def api_response(self, api_name: str, status_code: int, details: str = "", to_file: bool = False) -> None:
"""记录API响应消息"""
msg = LogHelper.api_response(api_name, status_code, details)
if 200 <= status_code < 300:
self.info(msg)
else:
self.error(msg)
def query(self, target: str, result: str = "", count: int = None, to_file: bool = False) -> None:
"""记录查询消息"""
msg = LogHelper.query(target, result, count)
self.info(msg)
def insert(self, target: str, subject: str = "", count: int = None, to_file: bool = False) -> None:
"""记录插入消息"""
msg = LogHelper.insert(target, subject, count)
self.info(msg)
def update(self, target: str, subject: str = "", count: int = None, to_file: bool = False) -> None:
"""记录更新消息"""
msg = LogHelper.update(target, subject, count)
self.info(msg)
def delete(self, target: str, subject: str = "", count: int = None, to_file: bool = False) -> None:
"""记录删除消息"""
msg = LogHelper.delete(target, subject, count)
self.info(msg)
def warning_msg(self, subject: str, message: str, to_file: bool = True) -> None:
"""记录警告消息"""
msg = LogHelper.warning(subject, message)
self.warning(msg)
def sync(self, action: str, subject: str = "", details: str = "", to_file: bool = False) -> None:
"""记录同步消息"""
msg = LogHelper.sync(action, subject, details)
self.info(msg)
def connect(self, target: str, status: str = "成功", to_file: bool = False) -> None:
"""记录连接消息"""
msg = LogHelper.connect(target, status)
if status == "成功":
self.info(msg)
else:
self.error(msg)
def disconnect(self, target: str, to_file: bool = False) -> None:
"""记录断开连接消息"""
msg = LogHelper.disconnect(target)
self.info(msg)
def cache(self, action: str, target: str = "", details: str = "", to_file: bool = False) -> None:
"""记录缓存消息"""
msg = LogHelper.cache(action, target, details)
self.info(msg)
def set_level(self, level: str) -> None:
"""设置日志级别"""
level_map = {
'DEBUG': logging.DEBUG,
'INFO': logging.INFO,
'WARNING': logging.WARNING,
'ERROR': logging.ERROR,
'CRITICAL': logging.CRITICAL
}
self._level = level_map.get(level.upper(), logging.INFO)
self._router.set_level(level)
if self._console_handler:
self._console_handler.set_level(self._level)
def get_level(self) -> int:
"""获取当前日志级别"""
return self._level
def enable_target(self, target: str) -> None:
"""启用输出目标"""
self._targets[target] = True
if target == 'console' and self._console_handler:
self._console_handler.enable()
elif target == 'file' and self._file_handler:
self._file_handler.enable()
elif target == 'database' and self._database_handler:
self._database_handler.enable()
elif target == 'websocket' and self._websocket_handler:
self._websocket_handler.enable()
def disable_target(self, target: str) -> None:
"""禁用输出目标"""
self._targets[target] = False
if target == 'console' and self._console_handler:
self._console_handler.disable()
elif target == 'file' and self._file_handler:
self._file_handler.disable()
elif target == 'database' and self._database_handler:
self._database_handler.disable()
elif target == 'websocket' and self._websocket_handler:
self._websocket_handler.disable()
def set_db_initialized(self, initialized: bool = True) -> None:
"""设置数据库初始化状态"""
if self._database_handler:
self._database_handler.mark_db_ready()
def isEnabledFor(self, level: int) -> bool:
"""检查是否启用指定级别"""
return level >= self._level
def get_logger(self, name: str = None, level: str = None) -> 'SmartLogger':
"""
获取日志器实例(兼容旧API)
Args:
name: 日志器名称
level: 日志级别
Returns:
SmartLogger实例(返回自身)
"""
if level:
self.set_level(level)
return self
def initialize_logging_unified(self) -> None:
"""初始化统一日志系统(兼容旧API"""
import asyncio
try:
loop = asyncio.get_event_loop()
if loop.is_running():
asyncio.create_task(self._async_init())
else:
loop.run_until_complete(self._async_init())
except Exception:
pass
async def _async_init(self) -> None:
"""异步初始化"""
await self.initialize()
def shutdown_logging_unified(self) -> None:
"""关闭统一日志系统(兼容旧API"""
import asyncio
try:
loop = asyncio.get_event_loop()
if loop.is_running():
asyncio.create_task(self.shutdown())
else:
loop.run_until_complete(self.shutdown())
except Exception:
pass
def get_stats(self) -> Dict[str, Any]:
"""获取统计信息"""
stats = {
'running': self._running,
'level': logging.getLevelName(self._level),
'targets': self._targets,
'queue': self._queue.get_stats() if self._queue else None
}
if self._database_handler:
stats['database'] = self._database_handler.get_stats()
if self._websocket_handler:
stats['websocket'] = self._websocket_handler.get_stats()
return stats
@property
def name(self) -> str:
"""获取日志器名称"""
return self._name
def __repr__(self) -> str:
return f"<SmartLogger '{self._name}' level={logging.getLevelName(self._level)}>"