Files
codexTool/logger.py
kyx236 ad120687b3 feat(logger, telegram_bot): Add real-time Telegram logging capability
- Add TelegramLogHandler class to stream logs to Telegram in real-time
- Implement enable_telegram_logging() method to activate Telegram log streaming
- Implement disable_telegram_logging() method to deactivate Telegram log streaming
- Implement is_telegram_logging_enabled() method to check logging status
- Add /logs_live command to enable real-time log push notifications
- Add /logs_stop command to disable real-time log push notifications
- Update help text to document new real-time logging commands
- Remove obsolete team.json.example file
- Enables administrators to monitor system logs in real-time through Telegram bot
2026-01-18 01:16:20 +08:00

355 lines
11 KiB
Python

# ==================== 日志模块 ====================
# 统一的日志输出,支持控制台和文件日志,带日志轮转
import os
import sys
import logging
from datetime import datetime
from pathlib import Path
from logging.handlers import RotatingFileHandler
# ==================== 日志配置 ====================
LOG_DIR = Path(__file__).parent / "logs"
LOG_FILE = LOG_DIR / "app.log"
LOG_MAX_BYTES = 10 * 1024 * 1024 # 10MB
LOG_BACKUP_COUNT = 5 # 保留 5 个备份
def _ensure_log_dir():
"""确保日志目录存在"""
LOG_DIR.mkdir(exist_ok=True)
class ColoredFormatter(logging.Formatter):
"""带颜色的控制台日志格式化器"""
COLORS = {
logging.DEBUG: "\033[90m", # 灰色
logging.INFO: "\033[0m", # 默认
logging.WARNING: "\033[93m", # 黄色
logging.ERROR: "\033[91m", # 红色
logging.CRITICAL: "\033[91m", # 红色
}
RESET = "\033[0m"
GREEN = "\033[92m" # 用于 success
BLUE = "\033[94m" # 用于 highlight
def format(self, record):
# 自定义 level 颜色
color = self.COLORS.get(record.levelno, self.RESET)
# 处理自定义的 success level
if hasattr(record, 'is_success') and record.is_success:
color = self.GREEN
# 处理自定义的 highlight level (蓝色)
if hasattr(record, 'is_highlight') and record.is_highlight:
color = self.BLUE
# 格式化时间
timestamp = datetime.fromtimestamp(record.created).strftime("%H:%M:%S")
# 获取图标
icon = getattr(record, 'icon', '')
if icon:
icon = f"{icon} "
# 构建消息
message = f"[{timestamp}] {color}{icon}{record.getMessage()}{self.RESET}"
return message
class FileFormatter(logging.Formatter):
"""文件日志格式化器 (不带颜色)"""
def format(self, record):
timestamp = datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S")
level = record.levelname.ljust(8)
icon = getattr(record, 'icon', '')
if icon:
icon = f"{icon} "
return f"[{timestamp}] [{level}] {icon}{record.getMessage()}"
class TelegramLogHandler(logging.Handler):
"""Telegram 实时日志处理器"""
def __init__(self, callback, level=logging.INFO):
"""初始化 Telegram 日志处理器
Args:
callback: 回调函数,接收 (message: str, level: str) 参数
level: 日志级别
"""
super().__init__(level)
self.callback = callback
self.setFormatter(FileFormatter())
def emit(self, record):
"""发送日志记录到 Telegram"""
try:
msg = self.format(record)
# 映射日志级别到 BotNotifier 的级别
level_map = {
logging.DEBUG: "debug",
logging.INFO: "info",
logging.WARNING: "warning",
logging.ERROR: "error",
logging.CRITICAL: "error"
}
level = level_map.get(record.levelno, "info")
self.callback(msg, level)
except Exception:
self.handleError(record)
class Logger:
"""统一日志输出 (基于 Python logging 模块)"""
# 日志级别
LEVEL_DEBUG = logging.DEBUG
LEVEL_INFO = logging.INFO
LEVEL_WARNING = logging.WARNING
LEVEL_ERROR = logging.ERROR
# 日志级别图标
ICONS = {
"info": "",
"success": "",
"warning": "",
"error": "",
"debug": "",
"start": "",
"browser": "",
"email": "",
"code": "",
"save": "",
"time": "",
"wait": "",
"account": "",
"team": "",
"auth": "",
}
def __init__(self, name: str = "app", use_color: bool = True, level: int = None,
enable_file_log: bool = True):
"""初始化日志器
Args:
name: 日志器名称
use_color: 是否使用颜色 (仅控制台)
level: 日志级别
enable_file_log: 是否启用文件日志
"""
self.name = name
self.use_color = use_color
self.enable_file_log = enable_file_log
self._telegram_handler = None # Telegram 日志处理器
# 从环境变量读取日志级别,默认 INFO
if level is None:
env_level = os.environ.get("LOG_LEVEL", "INFO").upper()
level_map = {"DEBUG": logging.DEBUG, "INFO": logging.INFO,
"WARNING": logging.WARNING, "ERROR": logging.ERROR}
level = level_map.get(env_level, logging.INFO)
self.level = level
self._setup_logger()
def _setup_logger(self):
"""设置日志器"""
self._logger = logging.getLogger(self.name)
self._logger.setLevel(self.level)
self._logger.handlers.clear() # 清除已有的处理器
# 控制台处理器
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(self.level)
if self.use_color:
console_handler.setFormatter(ColoredFormatter())
else:
console_handler.setFormatter(FileFormatter())
self._logger.addHandler(console_handler)
# 文件处理器 (带轮转)
if self.enable_file_log:
try:
_ensure_log_dir()
file_handler = RotatingFileHandler(
LOG_FILE,
maxBytes=LOG_MAX_BYTES,
backupCount=LOG_BACKUP_COUNT,
encoding='utf-8'
)
file_handler.setLevel(self.level)
file_handler.setFormatter(FileFormatter())
self._logger.addHandler(file_handler)
except Exception as e:
# 文件日志初始化失败时继续使用控制台日志
print(f"[WARNING] 文件日志初始化失败: {e}")
def _get_icon(self, icon: str = None) -> str:
"""获取图标"""
if icon:
return self.ICONS.get(icon, icon)
return ""
def enable_telegram_logging(self, callback, level: int = logging.INFO):
"""启用 Telegram 实时日志
Args:
callback: 回调函数,接收 (message: str, level: str) 参数
level: 日志级别,默认 INFO
"""
if self._telegram_handler is None:
self._telegram_handler = TelegramLogHandler(callback, level)
self._logger.addHandler(self._telegram_handler)
def disable_telegram_logging(self):
"""禁用 Telegram 实时日志"""
if self._telegram_handler is not None:
self._logger.removeHandler(self._telegram_handler)
self._telegram_handler = None
def is_telegram_logging_enabled(self) -> bool:
"""检查 Telegram 实时日志是否启用"""
return self._telegram_handler is not None
def info(self, msg: str, icon: str = None, indent: int = 0):
"""信息日志"""
prefix = " " * indent
extra = {'icon': self._get_icon(icon)}
self._logger.info(f"{prefix}{msg}", extra=extra)
def success(self, msg: str, indent: int = 0):
"""成功日志"""
prefix = " " * indent
extra = {'icon': self._get_icon("success"), 'is_success': True}
self._logger.info(f"{prefix}{msg}", extra=extra)
def highlight(self, msg: str, icon: str = None, indent: int = 0):
"""高亮日志 (蓝色)"""
prefix = " " * indent
extra = {'icon': self._get_icon(icon), 'is_highlight': True}
self._logger.info(f"{prefix}{msg}", extra=extra)
def warning(self, msg: str, indent: int = 0):
"""警告日志"""
prefix = " " * indent
extra = {'icon': self._get_icon("warning")}
self._logger.warning(f"{prefix}{msg}", extra=extra)
def error(self, msg: str, indent: int = 0):
"""错误日志"""
prefix = " " * indent
extra = {'icon': self._get_icon("error")}
self._logger.error(f"{prefix}{msg}", extra=extra)
def debug(self, msg: str, indent: int = 0):
"""调试日志"""
prefix = " " * indent
extra = {'icon': self._get_icon("debug")}
self._logger.debug(f"{prefix}{msg}", extra=extra)
def step(self, msg: str, indent: int = 0):
"""步骤日志 (INFO 级别)"""
prefix = " " * indent
extra = {'icon': ''}
self._logger.info(f"{prefix}-> {msg}", extra=extra)
def verbose(self, msg: str, indent: int = 0):
"""详细日志 (DEBUG 级别)"""
prefix = " " * indent
extra = {'icon': ''}
self._logger.debug(f"{prefix}. {msg}", extra=extra)
def progress(self, current: int, total: int, msg: str = ""):
"""进度日志"""
pct = (current / total * 100) if total > 0 else 0
bar_len = 20
filled = int(bar_len * current / total) if total > 0 else 0
bar = "=" * filled + "-" * (bar_len - filled)
extra = {'icon': ''}
self._logger.info(f"[{bar}] {current}/{total} ({pct:.0f}%) {msg}", extra=extra)
def progress_inline(self, msg: str):
"""内联进度 (覆盖当前行)"""
print(f"\r{msg}" + " " * 10, end='', flush=True)
def progress_clear(self):
"""清除内联进度"""
print("\r" + " " * 50 + "\r", end='', flush=True)
def countdown(self, seconds: int, msg: str = "等待"):
"""倒计时显示 (同一行更新)
Args:
seconds: 倒计时秒数
msg: 提示消息
"""
import time
for remaining in range(seconds, 0, -1):
timestamp = datetime.now().strftime("%H:%M:%S")
print(f"\r[{timestamp}] {msg} {remaining}s... ", end='', flush=True)
time.sleep(1)
self.progress_clear()
def separator(self, char: str = "=", length: int = 60):
"""分隔线"""
extra = {'icon': ''}
self._logger.info(char * length, extra=extra)
def header(self, title: str):
"""标题"""
self.separator()
extra = {'icon': ''}
self._logger.info(f" {title}", extra=extra)
self.separator()
def section(self, title: str):
"""小节标题"""
extra = {'icon': ''}
self._logger.info("#" * 40, extra=extra)
self._logger.info(f"# {title}", extra=extra)
self._logger.info("#" * 40, extra=extra)
# ==================== 配置日志辅助函数 ====================
def log_config_error(source: str, error: str, details: str = None):
"""记录配置加载错误
Args:
source: 配置来源 (如 config.toml, team.json)
error: 错误类型
details: 详细信息
"""
msg = f"配置加载失败 [{source}]: {error}"
if details:
msg += f" - {details}"
log.warning(msg)
def log_config_warning(source: str, message: str):
"""记录配置警告
Args:
source: 配置来源
message: 警告信息
"""
log.warning(f"配置警告 [{source}]: {message}")
def log_config_info(source: str, message: str):
"""记录配置信息
Args:
source: 配置来源
message: 信息内容
"""
log.info(f"配置 [{source}]: {message}")
# 全局日志实例
log = Logger()