Initial commit: SenSu Alpha 0.2.0

- 13-service async plugin framework
- Textual TUI with CLI fallback
- Plugin hot-reload + permission system
- Web management panel (aiohttp)
- Bridge-based inter-module communication
- 10 regression tests

Fixes applied:
- PBKDF2-SHA256 auth (was plain SHA256)
- Auth bypass removed (was allow-all on fail)
- Bare excepts replaced with logged errors
- CatFramework/DreamSu -> SenSu naming unified
- ServiceManager: health checks + startup_order
- Env var credentials (SENSU_ADMIN_PASSWORD etc)
This commit is contained in:
2026-06-10 12:27:14 +08:00
commit e6875f0b4b
78 changed files with 14843 additions and 0 deletions
+332
View File
@@ -0,0 +1,332 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import logging.handlers
import os
import asyncio
from pathlib import Path
from typing import Dict, List, Callable
import json
from datetime import datetime
logger = logging.getLogger(__name__)
class LogService:
"""日志服务"""
def __init__(self, config: Dict):
self.config = config
self.log_consumers: List[Callable] = []
self.log_buffer: List[Dict] = []
self.buffer_size = 100
self.log_dir = Path("logs")
self.is_initialized = False
self._in_emit = False
# 文件数量限制
self.max_log_files_per_folder = self.config['logging']['max_log_files'] # 每个文件夹文件数量上限
# 立即初始化日志系统
self._setup_logging_sync()
def _setup_logging_sync(self):
"""同步设置日志系统 - 增强版"""
try:
# 创建日志目录
self.log_dir.mkdir(exist_ok=True)
(self.log_dir / "debug").mkdir(exist_ok=True)
(self.log_dir / "runtime").mkdir(exist_ok=True)
# 清理旧日志文件(在创建新文件之前)
self._cleanup_old_log_files()
# 生成基于时间戳和会话ID的日志文件名
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
session_id = os.urandom(4).hex() # 生成8位随机会话ID
runtime_log_file = f"framework_{timestamp}_{session_id}.log"
debug_log_file = f"debug_{timestamp}_{session_id}.log"
# 保存当前会话的日志文件名(用于后续引用)
self.current_session_logs = {
'runtime': runtime_log_file,
'debug': debug_log_file,
'timestamp': timestamp,
'session_id': session_id
}
# 配置根日志记录器
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
# 清除现有处理器
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
# 创建过滤器实例
shared_filter = self.SafeLogFilter(self)
# 控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(getattr(logging, self.config['logging']['level'], logging.INFO))
console_formatter = logging.Formatter(
'%(asctime)s [%(levelname)-8s] %(name)s: %(message)s',
datefmt='%H:%M:%S'
)
console_handler.setFormatter(console_formatter)
console_handler.addFilter(shared_filter)
root_logger.addHandler(console_handler)
# 文件处理器
if self.config['logging'].get('enable_file_logging', True):
file_formatter = logging.Formatter(
'%(asctime)s [%(levelname)-8s] %(name)s:%(lineno)d - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# 运行时文件处理器
runtime_handler = logging.handlers.RotatingFileHandler(
self.log_dir / "runtime" / runtime_log_file,
maxBytes=self._parse_size(self.config['logging'].get('max_file_size', '10MB')),
backupCount=self.config['logging'].get('max_log_files', 3)
)
runtime_handler.setLevel(getattr(logging, self.config['logging']['level'], logging.INFO))
runtime_handler.setFormatter(file_formatter)
runtime_handler.addFilter(shared_filter)
root_logger.addHandler(runtime_handler)
# Debug文件处理器
if self.config['logging'].get('debug_level_file', True):
debug_handler = logging.handlers.RotatingFileHandler(
self.log_dir / "debug" / debug_log_file,
maxBytes=self._parse_size(self.config['logging'].get('max_file_size', '10MB')),
backupCount=self.config['logging'].get('max_log_files', 3)
)
debug_handler.setLevel(logging.DEBUG)
debug_handler.setFormatter(file_formatter)
debug_handler.addFilter(shared_filter)
root_logger.addHandler(debug_handler)
self.is_initialized = True
logger.info(f"✅ 日志系统初始化完成 - 会话ID: {session_id}")
logger.info(f"📝 运行时日志: logs/runtime/{runtime_log_file}")
logger.info(f"🐛 调试日志: logs/debug/{debug_log_file}")
except Exception as e:
logger.error(f"日志系统设置失败: {e}")
# 回退到基础配置
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
self.is_initialized = True
def _cleanup_old_log_files(self):
"""清理旧的日志文件,保持每个文件夹文件上限"""
try:
logger.debug("开始清理旧日志文件...")
# 清理 runtime 文件夹
runtime_dir = self.log_dir / "runtime"
if runtime_dir.exists():
runtime_files = list(runtime_dir.glob("*.log"))
self._remove_old_files(runtime_files, "runtime")
# 清理 debug 文件夹
debug_dir = self.log_dir / "debug"
if debug_dir.exists():
debug_files = list(debug_dir.glob("*.log"))
self._remove_old_files(debug_files, "debug")
except Exception as e:
logger.error(f"清理旧日志文件时出错: {e}")
def _remove_old_files(self, files: List[Path], folder_name: str):
"""删除最旧的文件,直到文件数量不超过限制"""
try:
if len(files) <= self.max_log_files_per_folder:
logger.debug(f"{folder_name} 文件夹文件数量正常: {len(files)}/{self.max_log_files_per_folder}")
return
# 按修改时间排序(最旧的在前)
files_sorted = sorted(files, key=lambda x: x.stat().st_mtime)
# 计算需要删除的文件数量
files_to_remove = len(files_sorted) - self.max_log_files_per_folder
if files_to_remove > 0:
logger.info(f"清理 {folder_name} 文件夹: 删除 {files_to_remove} 个旧日志文件")
for i in range(files_to_remove):
old_file = files_sorted[i]
try:
old_file.unlink()
logger.debug(f"删除旧日志文件: {old_file.name}")
except Exception as e:
logger.error(f"删除文件失败 {old_file}: {e}")
logger.info(f"{folder_name} 文件夹清理完成: {self.max_log_files_per_folder} 个文件")
except Exception as e:
logger.error(f"删除 {folder_name} 文件夹旧文件时出错: {e}")
def cleanup_log_files(self):
"""手动清理日志文件(可以定期调用)"""
try:
logger.info("开始手动清理日志文件...")
self._cleanup_old_log_files()
logger.info("日志文件清理完成")
except Exception as e:
logger.error(f"手动清理日志文件时出错: {e}")
def get_log_file_counts(self) -> Dict[str, int]:
"""获取当前日志文件数量统计"""
try:
runtime_count = len(list((self.log_dir / "runtime").glob("*.log")))
debug_count = len(list((self.log_dir / "debug").glob("*.log")))
return {
"runtime": runtime_count,
"debug": debug_count,
"max_limit": self.max_log_files_per_folder
}
except Exception as e:
logger.error(f"获取日志文件统计时出错: {e}")
return {"runtime": 0, "debug": 0, "max_limit": self.max_log_files_per_folder}
def get_current_session_info(self) -> Dict:
"""获取当前会话的日志信息"""
return getattr(self, 'current_session_logs', {})
def _parse_size(self, size_str: str) -> int:
"""解析文件大小字符串"""
try:
units = {'B': 1, 'KB': 1024, 'MB': 1024**2, 'GB': 1024**3}
number = ''.join(filter(str.isdigit, size_str))
unit = ''.join(filter(str.isalpha, size_str)).upper()
return int(number) * units.get(unit, 1)
except Exception:
return 10 * 1024 * 1024
def add_log_consumer(self, callback: Callable):
"""添加日志消费者"""
if callback not in self.log_consumers:
self.log_consumers.append(callback)
logger.debug(f"添加日志消费者,总数: {len(self.log_consumers)}")
def emit_log(self, log_record: Dict):
"""发射日志到消费者"""
if self._in_emit:
return
self._in_emit = True
try:
# 格式化日志记录
timestamp = log_record['timestamp']
level = log_record['level']
name = log_record['name']
message = log_record['message']
# 转换时间戳
if isinstance(timestamp, (int, float)):
timestamp_str = datetime.fromtimestamp(timestamp).strftime('%H:%M:%S')
else:
timestamp_str = str(timestamp)
formatted_record = {
'timestamp': timestamp,
'timestamp_str': timestamp_str,
'level': level,
'name': name,
'message': message,
'module': log_record.get('module', ''),
'line': log_record.get('line', 0),
'formatted_message': f"{timestamp_str} [{level:8}] {name}: {message}",
'simple_message': f"{timestamp_str} [{level:8}] {message}",
'original_message': message
}
# 添加到缓冲区
self.log_buffer.append(formatted_record)
if len(self.log_buffer) > self.buffer_size:
self.log_buffer.pop(0)
# 发送给消费者
for consumer in self.log_consumers:
try:
# 检查TUI级别过滤
if hasattr(consumer, '_tui_level_filter'):
tui_level = consumer._tui_level_filter
if self._should_display_for_tui(level, tui_level):
consumer(formatted_record)
else:
# 默认发送所有日志
consumer(formatted_record)
except Exception as e:
logger.error(f"日志消费者处理出错: {e}")
except Exception as e:
logger.error(f"发射日志时出错: {e}")
finally:
self._in_emit = False
def _should_display_for_tui(self, log_level: str, tui_level: str) -> bool:
"""检查日志是否应该显示在TUI中"""
level_priority = {
'DEBUG': 10, 'INFO': 20, 'WARNING': 30, 'ERROR': 40, 'CRITICAL': 50
}
log_priority = level_priority.get(log_level, 0)
tui_priority = level_priority.get(tui_level, 0)
return log_priority >= tui_priority
def add_tui_log_consumer(self, callback: Callable, tui_level: str = "INFO"):
"""专门为TUI添加日志消费者"""
callback._tui_level_filter = tui_level
if callback not in self.log_consumers:
self.log_consumers.append(callback)
logger.debug(f"添加TUI日志消费者,级别: {tui_level}")
def get_recent_logs(self, count: int = 50) -> List[Dict]:
"""获取最近的日志"""
return self.log_buffer[-count:]
def shutdown(self):
"""关闭日志服务"""
self.log_consumers.clear()
logging.shutdown()
logger.info("日志服务已关闭")
class SafeLogFilter(logging.Filter):
"""安全的日志过滤器"""
def __init__(self, log_service):
super().__init__()
self.log_service = log_service
self._in_filter = False
def filter(self, record):
"""过滤日志记录"""
if self._in_filter:
return True
self._in_filter = True
try:
# 创建日志记录
log_record = {
'timestamp': record.created,
'name': record.name,
'level': record.levelname,
'message': record.getMessage(),
'module': record.module,
'line': record.lineno
}
# 发射日志记录
self.log_service.emit_log(log_record)
except Exception:
pass
finally:
self._in_filter = False
return True