Update Report Engine Log Display Method
This commit is contained in:
+43
-3
@@ -233,14 +233,54 @@ class ReportAgent:
|
|||||||
- 确保日志目录存在;
|
- 确保日志目录存在;
|
||||||
- 使用独立的 loguru sink 写入 Report Engine 专属 log 文件,
|
- 使用独立的 loguru sink 写入 Report Engine 专属 log 文件,
|
||||||
避免与其他子系统混淆。
|
避免与其他子系统混淆。
|
||||||
|
- 【修复】配置实时日志写入,禁用缓冲,确保前端实时看到日志
|
||||||
|
- 【修复】防止重复添加handler
|
||||||
"""
|
"""
|
||||||
# 确保日志目录存在
|
# 确保日志目录存在
|
||||||
log_dir = os.path.dirname(self.config.LOG_FILE)
|
log_dir = os.path.dirname(self.config.LOG_FILE)
|
||||||
os.makedirs(log_dir, exist_ok=True)
|
os.makedirs(log_dir, exist_ok=True)
|
||||||
|
|
||||||
# 创建专用的logger,避免与其他模块冲突
|
# 【修复】检查是否已经添加过这个文件的handler,避免重复
|
||||||
# 修改日志级别为DEBUG,确保DEBUG、INFO、WARNING、ERROR级别的日志都能被记录
|
# loguru会自动去重,但显式检查更安全
|
||||||
logger.add(self.config.LOG_FILE, level="DEBUG")
|
log_file_path = str(Path(self.config.LOG_FILE).resolve())
|
||||||
|
|
||||||
|
# 检查现有的handlers
|
||||||
|
handler_exists = False
|
||||||
|
for handler_id, handler_config in logger._core.handlers.items():
|
||||||
|
if hasattr(handler_config, 'sink'):
|
||||||
|
sink = handler_config.sink
|
||||||
|
# 检查是否是文件sink且路径相同
|
||||||
|
if hasattr(sink, '_name') and sink._name == log_file_path:
|
||||||
|
handler_exists = True
|
||||||
|
logger.debug(f"日志handler已存在,跳过添加: {log_file_path}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not handler_exists:
|
||||||
|
# 【修复】创建专用的logger,配置实时写入
|
||||||
|
# - enqueue=False: 禁用异步队列,立即写入
|
||||||
|
# - buffering=1: 行缓冲,每条日志立即刷新到文件
|
||||||
|
# - level="DEBUG": 记录所有级别的日志
|
||||||
|
# - encoding="utf-8": 明确指定UTF-8编码
|
||||||
|
# - mode="a": 追加模式,保留历史日志
|
||||||
|
handler_id = logger.add(
|
||||||
|
self.config.LOG_FILE,
|
||||||
|
level="DEBUG",
|
||||||
|
enqueue=False, # 禁用异步队列,同步写入
|
||||||
|
buffering=1, # 行缓冲,每行立即写入
|
||||||
|
serialize=False, # 普通文本格式,不序列化为JSON
|
||||||
|
encoding="utf-8", # 明确UTF-8编码
|
||||||
|
mode="a" # 追加模式
|
||||||
|
)
|
||||||
|
logger.debug(f"已添加日志handler (ID: {handler_id}): {self.config.LOG_FILE}")
|
||||||
|
|
||||||
|
# 【修复】验证日志文件可写
|
||||||
|
try:
|
||||||
|
with open(self.config.LOG_FILE, 'a', encoding='utf-8') as f:
|
||||||
|
f.write('') # 尝试写入空字符串验证权限
|
||||||
|
f.flush() # 立即刷新
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"日志文件无法写入: {self.config.LOG_FILE}, 错误: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
def _initialize_file_baseline(self):
|
def _initialize_file_baseline(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -957,9 +957,22 @@ def clear_report_log():
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
log_file = settings.LOG_FILE
|
log_file = settings.LOG_FILE
|
||||||
with open(log_file, 'w', encoding='utf-8') as f:
|
|
||||||
f.write('')
|
# 【修复】使用truncate而非重新打开,避免与logger的文件句柄冲突
|
||||||
|
# 追加模式打开,然后truncate,保持文件句柄有效
|
||||||
|
with open(log_file, 'r+', encoding='utf-8') as f:
|
||||||
|
f.truncate(0) # 清空文件内容但不关闭文件
|
||||||
|
f.flush() # 立即刷新
|
||||||
|
|
||||||
logger.info(f"已清空日志文件: {log_file}")
|
logger.info(f"已清空日志文件: {log_file}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
# 文件不存在,创建空文件
|
||||||
|
try:
|
||||||
|
with open(log_file, 'w', encoding='utf-8') as f:
|
||||||
|
f.write('')
|
||||||
|
logger.info(f"创建日志文件: {log_file}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"创建日志文件失败: {str(e)}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"清空日志文件失败: {str(e)}")
|
logger.exception(f"清空日志文件失败: {str(e)}")
|
||||||
|
|
||||||
@@ -969,29 +982,58 @@ def get_report_log():
|
|||||||
"""
|
"""
|
||||||
获取report.log内容,并按行去除空白返回。
|
获取report.log内容,并按行去除空白返回。
|
||||||
|
|
||||||
|
【修复】优化大文件读取,添加错误处理和文件锁
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
Response: JSON,包含最新日志行数组。
|
Response: JSON,包含最新日志行数组。
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
log_file = settings.LOG_FILE
|
log_file = settings.LOG_FILE
|
||||||
|
|
||||||
if not os.path.exists(log_file):
|
if not os.path.exists(log_file):
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'log_lines': []
|
'log_lines': []
|
||||||
})
|
})
|
||||||
|
|
||||||
with open(log_file, 'r', encoding='utf-8') as f:
|
# 【修复】检查文件大小,避免读取过大文件导致内存问题
|
||||||
lines = f.readlines()
|
file_size = os.path.getsize(log_file)
|
||||||
|
max_size = 10 * 1024 * 1024 # 10MB限制
|
||||||
# 清理行尾的换行符
|
|
||||||
|
if file_size > max_size:
|
||||||
|
# 文件过大,只读取最后10MB
|
||||||
|
with open(log_file, 'rb') as f:
|
||||||
|
f.seek(-max_size, 2) # 从文件末尾往前10MB
|
||||||
|
# 跳过可能不完整的第一行
|
||||||
|
f.readline()
|
||||||
|
content = f.read().decode('utf-8', errors='replace')
|
||||||
|
lines = content.splitlines()
|
||||||
|
logger.warning(f"日志文件过大 ({file_size} bytes),仅返回最后 {max_size} bytes")
|
||||||
|
else:
|
||||||
|
# 正常大小,完整读取
|
||||||
|
with open(log_file, 'r', encoding='utf-8', errors='replace') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
# 清理行尾的换行符和空行
|
||||||
log_lines = [line.rstrip('\n\r') for line in lines if line.strip()]
|
log_lines = [line.rstrip('\n\r') for line in lines if line.strip()]
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'log_lines': log_lines
|
'log_lines': log_lines
|
||||||
})
|
})
|
||||||
|
|
||||||
|
except PermissionError as e:
|
||||||
|
logger.error(f"读取日志权限不足: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': '读取日志权限不足'
|
||||||
|
}), 403
|
||||||
|
except UnicodeDecodeError as e:
|
||||||
|
logger.error(f"日志文件编码错误: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': '日志文件编码错误'
|
||||||
|
}), 500
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"读取日志失败: {str(e)}")
|
logger.exception(f"读取日志失败: {str(e)}")
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ Flask主应用 - 统一管理三个Streamlit应用
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
# 【修复】尽早设置环境变量,确保所有模块都使用无缓冲模式
|
||||||
|
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
||||||
|
os.environ['PYTHONUTF8'] = '1'
|
||||||
|
os.environ['PYTHONUNBUFFERED'] = '1' # 禁用Python输出缓冲,确保日志实时输出
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
@@ -37,10 +43,6 @@ if REPORT_ENGINE_AVAILABLE:
|
|||||||
else:
|
else:
|
||||||
logger.info("ReportEngine不可用,跳过接口注册")
|
logger.info("ReportEngine不可用,跳过接口注册")
|
||||||
|
|
||||||
# 设置UTF-8编码环境
|
|
||||||
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
|
||||||
os.environ['PYTHONUTF8'] = '1'
|
|
||||||
|
|
||||||
# 创建日志目录
|
# 创建日志目录
|
||||||
LOG_DIR = Path('logs')
|
LOG_DIR = Path('logs')
|
||||||
LOG_DIR.mkdir(exist_ok=True)
|
LOG_DIR.mkdir(exist_ok=True)
|
||||||
|
|||||||
+69
-22
@@ -4467,21 +4467,53 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refreshReportLog() {
|
function refreshReportLog() {
|
||||||
fetch('/api/report/log')
|
// 【修复】添加超时控制和完整错误处理
|
||||||
.then(response => response.json())
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时
|
||||||
|
|
||||||
|
fetch('/api/report/log', { signal: controller.signal })
|
||||||
|
.then(response => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
// 【修复】检查HTTP状态
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success && data.log_lines.length > reportLogLineCount) {
|
// 【修复】检查返回的成功状态
|
||||||
|
if (!data.success) {
|
||||||
|
console.error('[Report日志] 刷新失败:', data.error || '未知错误');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.log_lines && data.log_lines.length > reportLogLineCount) {
|
||||||
// 只添加新的行
|
// 只添加新的行
|
||||||
const newLines = data.log_lines.slice(reportLogLineCount);
|
const newLines = data.log_lines.slice(reportLogLineCount);
|
||||||
|
|
||||||
|
// 【调试日志】记录实时读取的日志数量
|
||||||
|
if (newLines.length > 0) {
|
||||||
|
console.log(`[Report日志] 读取 ${newLines.length} 条新日志(总计 ${data.log_lines.length})`);
|
||||||
|
}
|
||||||
|
|
||||||
newLines.forEach(line => {
|
newLines.forEach(line => {
|
||||||
|
// 直接添加,使用LogVirtualList的默认批处理机制
|
||||||
appendConsoleTextLine('report', line);
|
appendConsoleTextLine('report', line);
|
||||||
});
|
});
|
||||||
|
|
||||||
reportLogLineCount = data.log_lines.length;
|
reportLogLineCount = data.log_lines.length;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('刷新Report日志失败:', error);
|
clearTimeout(timeoutId);
|
||||||
|
// 【修复】区分错误类型
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
console.warn('[Report日志] 刷新超时(5秒)');
|
||||||
|
} else if (error.message.includes('Failed to fetch')) {
|
||||||
|
console.error('[Report日志] 刷新失败: 网络连接错误');
|
||||||
|
} else {
|
||||||
|
console.error('[Report日志] 刷新失败:', error.message || error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5113,9 +5145,6 @@
|
|||||||
reportTaskId = data.task_id;
|
reportTaskId = data.task_id;
|
||||||
showMessage('报告生成已启动', 'success');
|
showMessage('报告生成已启动', 'success');
|
||||||
|
|
||||||
// 【修复】立即启动日志实时刷新,确保日志实时显示
|
|
||||||
startReportLogRefresh();
|
|
||||||
|
|
||||||
// 更新任务状态显示
|
// 更新任务状态显示
|
||||||
updateTaskProgressStatus({
|
updateTaskProgressStatus({
|
||||||
task_id: data.task_id,
|
task_id: data.task_id,
|
||||||
@@ -5126,12 +5155,13 @@
|
|||||||
updated_at: new Date().toISOString()
|
updated_at: new Date().toISOString()
|
||||||
});
|
});
|
||||||
|
|
||||||
// 立即刷新一次日志以确保同步
|
|
||||||
setTimeout(() => {
|
|
||||||
refreshReportLog();
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
appendReportStreamLine('任务创建成功,正在建立流式连接...', 'info', { force: true });
|
appendReportStreamLine('任务创建成功,正在建立流式连接...', 'info', { force: true });
|
||||||
|
|
||||||
|
// 【优化】先启动日志轮询,再建立SSE连接
|
||||||
|
// 确保从任务开始就能读取日志
|
||||||
|
startReportLogRefresh();
|
||||||
|
console.log('[Report日志] 任务创建成功,启动日志轮询');
|
||||||
|
|
||||||
if (window.EventSource) {
|
if (window.EventSource) {
|
||||||
openReportStream(reportTaskId);
|
openReportStream(reportTaskId);
|
||||||
} else {
|
} else {
|
||||||
@@ -5163,18 +5193,25 @@
|
|||||||
|
|
||||||
// 【修复】启动Report Engine日志实时刷新
|
// 【修复】启动Report Engine日志实时刷新
|
||||||
function startReportLogRefresh() {
|
function startReportLogRefresh() {
|
||||||
// 清除旧的定时器
|
// 防重复:如果已经在运行,直接返回
|
||||||
if (reportLogRefreshInterval) {
|
if (reportLogRefreshInterval) {
|
||||||
clearInterval(reportLogRefreshInterval);
|
console.log('[Report日志] 日志轮询已在运行,跳过重复启动');
|
||||||
reportLogRefreshInterval = null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动新的日志刷新定时器(1秒刷新一次,保证实时性)
|
// 立即刷新一次,获取当前所有日志
|
||||||
|
refreshReportLog();
|
||||||
|
|
||||||
|
// 启动定时器,每秒刷新一次
|
||||||
reportLogRefreshInterval = setInterval(() => {
|
reportLogRefreshInterval = setInterval(() => {
|
||||||
if (currentApp === 'report') {
|
if (currentApp === 'report') {
|
||||||
refreshReportLog();
|
refreshReportLog();
|
||||||
|
} else {
|
||||||
|
// 如果切换到其他应用,自动停止轮询
|
||||||
|
console.log('[Report日志] 检测到应用切换,停止日志轮询');
|
||||||
|
stopReportLogRefresh();
|
||||||
}
|
}
|
||||||
}, 1000); // 1秒刷新,确保日志实时显示
|
}, 1000); // 每秒刷新一次
|
||||||
|
|
||||||
console.log('[Report日志] 启动实时日志刷新,频率: 1秒');
|
console.log('[Report日志] 启动实时日志刷新,频率: 1秒');
|
||||||
}
|
}
|
||||||
@@ -5421,9 +5458,10 @@
|
|||||||
appendReportStreamLine(isRetry ? 'SSE重连成功' : 'Report Engine流式连接已建立', 'success', { badge: 'SSE' });
|
appendReportStreamLine(isRetry ? 'SSE重连成功' : 'Report Engine流式连接已建立', 'success', { badge: 'SSE' });
|
||||||
startStreamHeartbeat();
|
startStreamHeartbeat();
|
||||||
|
|
||||||
// 【优化Phase 1.1】删除定时刷新日志的轮询
|
// 【修复】启动日志轮询,读取logger.info/debug/warning/error
|
||||||
// SSE事件流已提供实时推送,无需1秒轮询
|
// SSE只推送显式事件(stage/chapter_status等),logger日志需要轮询读取
|
||||||
// 这减少了大量不必要的HTTP请求
|
startReportLogRefresh();
|
||||||
|
console.log('[Report日志] SSE连接建立,同步启动日志轮询');
|
||||||
};
|
};
|
||||||
reportEventSource.onerror = () => {
|
reportEventSource.onerror = () => {
|
||||||
appendReportStreamLine('检测到网络抖动,SSE正在等待自动重连...', 'warn', { badge: 'SSE' });
|
appendReportStreamLine('检测到网络抖动,SSE正在等待自动重连...', 'warn', { badge: 'SSE' });
|
||||||
@@ -5454,6 +5492,7 @@
|
|||||||
if (reportLogRefreshInterval) {
|
if (reportLogRefreshInterval) {
|
||||||
clearInterval(reportLogRefreshInterval);
|
clearInterval(reportLogRefreshInterval);
|
||||||
reportLogRefreshInterval = null;
|
reportLogRefreshInterval = null;
|
||||||
|
console.log('[Report日志] SSE连接关闭,停止日志轮询');
|
||||||
}
|
}
|
||||||
clearStreamHeartbeat();
|
clearStreamHeartbeat();
|
||||||
if (!keepIndicator) {
|
if (!keepIndicator) {
|
||||||
@@ -5560,7 +5599,15 @@
|
|||||||
break;
|
break;
|
||||||
case 'completed':
|
case 'completed':
|
||||||
appendReportStreamLine(payload.message || '任务完成', 'success');
|
appendReportStreamLine(payload.message || '任务完成', 'success');
|
||||||
safeCloseReportStream();
|
|
||||||
|
// 【修复】任务完成前最后一次刷新日志,确保所有日志都被读取
|
||||||
|
refreshReportLog();
|
||||||
|
|
||||||
|
// 延迟500ms后关闭SSE,确保最后一次日志刷新完成
|
||||||
|
setTimeout(() => {
|
||||||
|
safeCloseReportStream();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
reportTaskId = null;
|
reportTaskId = null;
|
||||||
setGenerateButtonState(false);
|
setGenerateButtonState(false);
|
||||||
if (task) {
|
if (task) {
|
||||||
|
|||||||
Reference in New Issue
Block a user