diff --git a/resource/MainWindow.ui b/resource/MainWindow.ui
index b85551d..81df7b2 100644
--- a/resource/MainWindow.ui
+++ b/resource/MainWindow.ui
@@ -151,6 +151,53 @@ p, li { white-space: pre-wrap; }
事件时间线
+
+
+ 日志查看
+
+
+ -
+
+
+ IDL日志
+
+
-
+
+ IDL日志
+
+
+ -
+
+ 审计日志
+
+
+ -
+
+ 维护日志
+
+
+ -
+
+ OS串口日志
+
+
+ -
+
+ 运行状态
+
+
+ -
+
+ 部件日志
+
+
+
+
+ -
+
+
+
+
diff --git a/src/MainWindow_ui.py b/src/MainWindow_ui.py
index e994bc2..ba33ffc 100644
--- a/src/MainWindow_ui.py
+++ b/src/MainWindow_ui.py
@@ -76,6 +76,23 @@ class Ui_MainWindow(object):
self.tab_timeline_event = QtWidgets.QWidget()
self.tab_timeline_event.setObjectName("tab_timeline_event")
self.tabWidget.addTab(self.tab_timeline_event, "")
+ self.tab_readlog = QtWidgets.QWidget()
+ self.tab_readlog.setObjectName("tab_readlog")
+ self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.tab_readlog)
+ self.verticalLayout_2.setObjectName("verticalLayout_2")
+ self.comboBox_logSelect = QtWidgets.QComboBox(self.tab_readlog)
+ self.comboBox_logSelect.setObjectName("comboBox_logSelect")
+ self.comboBox_logSelect.addItem("")
+ self.comboBox_logSelect.addItem("")
+ self.comboBox_logSelect.addItem("")
+ self.comboBox_logSelect.addItem("")
+ self.comboBox_logSelect.addItem("")
+ self.comboBox_logSelect.addItem("")
+ self.verticalLayout_2.addWidget(self.comboBox_logSelect)
+ self.textBrowser_logView = QtWidgets.QTextBrowser(self.tab_readlog)
+ self.textBrowser_logView.setObjectName("textBrowser_logView")
+ self.verticalLayout_2.addWidget(self.textBrowser_logView)
+ self.tabWidget.addTab(self.tab_readlog, "")
self.horizontalLayout_2.addWidget(self.tabWidget)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
@@ -120,6 +137,14 @@ class Ui_MainWindow(object):
self.comboBox.setItemText(10, _translate("MainWindow", "VR_FPGA_Temp"))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_temp_all), _translate("MainWindow", "温度信息"))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_timeline_event), _translate("MainWindow", "事件时间线"))
+ self.comboBox_logSelect.setCurrentText(_translate("MainWindow", "IDL日志"))
+ self.comboBox_logSelect.setItemText(0, _translate("MainWindow", "IDL日志"))
+ self.comboBox_logSelect.setItemText(1, _translate("MainWindow", "审计日志"))
+ self.comboBox_logSelect.setItemText(2, _translate("MainWindow", "维护日志"))
+ self.comboBox_logSelect.setItemText(3, _translate("MainWindow", "OS串口日志"))
+ self.comboBox_logSelect.setItemText(4, _translate("MainWindow", "运行状态"))
+ self.comboBox_logSelect.setItemText(5, _translate("MainWindow", "部件日志"))
+ self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_readlog), _translate("MainWindow", "日志查看"))
self.menu.setTitle(_translate("MainWindow", "文件"))
self.actionUpload_log.setText(_translate("MainWindow", "打开"))
self.actionUpload_log.setStatusTip(_translate("MainWindow", "上传日志文件到程序中"))
diff --git a/src/ZiJin_mergeLog.py b/src/ZiJin_mergeLog.py
new file mode 100644
index 0000000..c4d4ea3
--- /dev/null
+++ b/src/ZiJin_mergeLog.py
@@ -0,0 +1,58 @@
+import os
+import utils
+
+project_root = utils.get_project_root()
+cache_dir = os.path.join(project_root, "okd_tmp")
+onekeylog_dir = os.path.join(cache_dir, "onekeylog")
+extlog_dir = os.path.join(onekeylog_dir, "log")
+sollog_dir = os.path.join(extlog_dir, "sollog")
+running_dir = os.path.join(onekeylog_dir, "runningdata")
+comp_dir = os.path.join(onekeylog_dir, "component")
+
+# cache_{file}
+cache_idl = os.path.join(cache_dir, "merge_idl.log")
+cache_audit = os.path.join(cache_dir, "merge_audit.log")
+cache_maintenace = os.path.join(cache_dir, "merge_maintenance.log")
+cache_console = os.path.join(cache_dir, "os_sol.log")
+running_file = os.path.join(running_dir, "rundatainfo.log")
+compfile = os.path.join(comp_dir, "component.log")
+
+def merge_all_log_to_root():
+ # 1.融合处理IDL日志, 已在parse_idl中整合, pass
+
+ # 2.融合审计日志
+ audit_log = os.path.join(extlog_dir, "audit.log")
+ utils.merge_logrotate_files(audit_log, 2, cache_audit)
+
+ # 3.融合维护日志
+ # 暂时设定为最多解压四份,共合并五份日志
+ maintenance_log = os.path.join(extlog_dir, "maintenance.log")
+ utils.extract_maintenancelog_gz_files(extlog_dir, extlog_dir, 4)
+ utils.merge_logrotate_files(maintenance_log, 5, cache_maintenace)
+
+ # 4.融合OS串口日志
+ sol_log = os.path.join(sollog_dir, "SOLHostCapture.log")
+ utils.merge_logrotate_files(sol_log, 2, cache_console)
+
+def get_logfile_path_by_key(key):
+ match key:
+ case "log_idl":
+ logfile = cache_idl
+ case "log_audit":
+ logfile = cache_audit
+ case "log_maintenance":
+ logfile = cache_maintenace
+ case "log_osconsole":
+ logfile = cache_console
+ case "log_run":
+ logfile = running_file
+ case "log_comp":
+ logfile = compfile
+ case _ :
+ logfile = ""
+
+ return logfile
+
+def program_main():
+ merge_all_log_to_root()
+ return True
\ No newline at end of file
diff --git a/src/background_task.py b/src/background_task.py
index d7887e1..2f69042 100644
--- a/src/background_task.py
+++ b/src/background_task.py
@@ -37,4 +37,27 @@ class DiagnoseThread(QThread):
def stop(self):
"""停止线程"""
self.is_running = False
- self.wait()
\ No newline at end of file
+ self.wait()
+
+class FileReaderThread(QThread):
+ # 定义信号,用于传递读取到的文本块和完成状态
+ text_chunk_ready = pyqtSignal(str)
+ finished = pyqtSignal()
+
+ def __init__(self, file_path, chunk_size=4096):
+ super().__init__()
+ self.file_path = file_path
+ self.chunk_size = chunk_size # 每次读取的块大小
+
+ def run(self):
+ try:
+ with open(self.file_path, 'r', encoding='utf-8', errors='ignore') as f:
+ while True:
+ chunk = f.read(self.chunk_size) # 分块读取
+ if not chunk:
+ break
+ self.text_chunk_ready.emit(chunk) # 发送文本块到主线程
+ self.finished.emit()
+ except Exception as e:
+ self.text_chunk_ready.emit(f"读取文件错误: {str(e)}")
+ self.finished.emit()
\ No newline at end of file
diff --git a/src/event_handler.py b/src/event_handler.py
index d82e698..b2357cc 100644
--- a/src/event_handler.py
+++ b/src/event_handler.py
@@ -1,7 +1,9 @@
import os
+import service
from PyQt5.QtWidgets import QFileDialog
from PyQt5.QtCore import Qt
-from PyQt5.QtGui import QDragEnterEvent, QDropEvent
+from PyQt5.QtGui import QDragEnterEvent, QDropEvent, QTextCharFormat
+from background_task import FileReaderThread
class EventHandler:
def __init__(self, main_window):
@@ -19,6 +21,8 @@ class EventHandler:
self.main_window.graphicsView.wheelEvent = self.zoom_with_mouse_wheel
# 窗口 resize 事件
self.main_window.resizeEvent = self.resize_event
+ # 日志下拉框变化事件
+ self.main_window.comboBox_logSelect.currentTextChanged.connect(self.logSelect_on_combo_changed)
def upload_file(self):
"""打开文件选择对话框"""
@@ -40,6 +44,27 @@ class EventHandler:
self.main_window.file_processor.get_sensorhistory_path(key),
self.main_window.parse_status.diag_complete_status
)
+
+ def logview_append_text(self, chunk):
+ # 在主线程中追加文本
+ self.main_window.textBrowser_logView.insertPlainText(chunk)
+ # 自动滚动到底部
+ self.main_window.textBrowser_logView.moveCursor(
+ self.main_window.textBrowser_logView.textCursor().End)
+
+ def logSelect_on_combo_changed(self, selected_text):
+ key = self.main_window.comboBox_logSelect_text_dict.get(selected_text)
+ if key:
+ filepath = service.get_logfile_path(key)
+ if not os.path.exists(filepath):
+ self.main_window.textBrowser_logView.setText("文件不存在/未上传日志")
+ return
+ self.main_window.textBrowser_logView.clear()
+
+ self.main_window.reader_thread = FileReaderThread(filepath)
+ self.main_window.reader_thread.text_chunk_ready.connect(self.logview_append_text)
+ # self.main_window.reader_thread.finished.connect(self.on_read_finished)
+ self.main_window.reader_thread.start()
def dragEnterEvent(self, event: QDragEnterEvent):
"""拖入事件:判断是否为文件"""
diff --git a/src/main_window.py b/src/main_window.py
index 7ccec28..7d0cffd 100644
--- a/src/main_window.py
+++ b/src/main_window.py
@@ -30,10 +30,11 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.ui_init.init_textbrowser_style()
self.ui_init.init_tableView_alert_model()
self.ui_init.re_init_time_line_tab()
+ self.ui_init.init_textbrowser_logview_style()
# 事件绑定(按钮、拖放、下拉框)
self.event_handler.bind_events()
self.event_handler.patch_drag_events() # 将拖放事件绑定到主窗口
- # 初始化下拉框映射字典(跨模块使用,放在主窗口)
+ # 初始化温度图下拉框映射字典(跨模块使用,放在主窗口)
self.combo_box_text_dict = {
"ALL_Temp": "all",
"CPU_Temp": "cpu",
@@ -50,4 +51,14 @@ class MainWindow(QMainWindow, Ui_MainWindow):
"OPT1_Temp": "opt1",
"OPT2_Temp": "opt2",
"OPT3_Temp": "opt3",
+ }
+ # 初始化日志窗格下拉框映射字典
+ self.reader_thread = None
+ self.comboBox_logSelect_text_dict = {
+ "IDL日志": "log_idl",
+ "审计日志": "log_audit",
+ "维护日志": "log_maintenance",
+ "OS串口日志": "log_osconsole",
+ "运行状态": "log_run",
+ "部件日志": "log_comp"
}
\ No newline at end of file
diff --git a/src/search_shortcut.py b/src/search_shortcut.py
new file mode 100644
index 0000000..bb739dc
--- /dev/null
+++ b/src/search_shortcut.py
@@ -0,0 +1,65 @@
+from PyQt5.QtWidgets import QDialog, QHBoxLayout, QLineEdit, QLabel, QPushButton, QMessageBox
+from PyQt5.QtGui import QTextCursor
+
+class FindDialog(QDialog):
+ """独立的查找对话框类,用于在文本中查找指定内容"""
+ def __init__(self, parent=None, text_browser=None):
+ super().__init__(parent)
+ self.text_browser = text_browser
+ self.init_ui()
+
+ def init_ui(self):
+ self.setWindowTitle("查找")
+ self.setFixedSize(300, 80)
+ self.setModal(True) # 模态对话框,阻止对父窗口的操作
+
+ # 创建布局
+ layout = QHBoxLayout()
+
+ # 查找输入框
+ self.find_input = QLineEdit()
+ self.find_input.setPlaceholderText("输入要查找的内容...")
+ self.find_input.returnPressed.connect(self.find_next) # 回车触发查找
+
+ # 查找按钮
+ self.find_btn = QPushButton("查找下一个")
+ self.find_btn.clicked.connect(self.find_next)
+
+ # 添加到布局
+ layout.addWidget(QLabel("查找:"))
+ layout.addWidget(self.find_input)
+ layout.addWidget(self.find_btn)
+
+ self.setLayout(layout)
+
+ # 让输入框获取焦点
+ self.find_input.setFocus()
+
+ def find_next(self):
+ """查找下一个匹配项"""
+ if not self.text_browser:
+ return
+
+ # 获取要查找的文本
+ find_text = self.find_input.text()
+ if not find_text:
+ return
+
+ # 获取当前文本和光标
+ document = self.text_browser.document()
+ cursor = self.text_browser.textCursor()
+
+ # 从当前位置之后查找
+ cursor = document.find(find_text, cursor)
+
+ # 如果没找到,从头开始查找
+ if cursor.isNull():
+ cursor = QTextCursor(document)
+ cursor = document.find(find_text, cursor)
+
+ # 如果找到匹配项
+ if not cursor.isNull():
+ self.text_browser.setTextCursor(cursor)
+ self.text_browser.ensureCursorVisible() # 滚动到可见位置
+ else:
+ QMessageBox.information(self, "查找完成", f"找不到 '{find_text}'")
diff --git a/src/service.py b/src/service.py
index 6ae4717..707ad2f 100644
--- a/src/service.py
+++ b/src/service.py
@@ -5,6 +5,7 @@ import ZiJin_parse_sensorhistory as sensorparse
import ZiJin_parse_baseinfo as baseinfo
import ZiJin_parse_idl as parseidl
import ZiJin_parse_event as zijin_event
+import ZiJin_mergeLog as mergelog
class ServiceStatus():
def __init__(self):
@@ -12,6 +13,7 @@ class ServiceStatus():
self.sensorhistory_status = False
self.baseinfo_status = False
self.parseidl_status = False
+ self.mergelog_status = False
self.eventline_status = False
def set_sensorhistory_status(self, status):
@@ -37,6 +39,12 @@ class ServiceStatus():
def get_eventline_status(self):
return self.eventline_status
+
+ def set_mergelog_status(self, status):
+ self.mergelog_status = status
+
+ def get_mergelog_status(self):
+ return self.mergelog_status
def app_cache_init():
project_root = utils.get_project_root()
@@ -76,6 +84,9 @@ def is_idl_alert_empty():
def get_timeline_event_json():
return zijin_event.get_event_json()
+def get_logfile_path(key):
+ return mergelog.get_logfile_path_by_key(key)
+
def start_diagnose(parseStatus, filepath, progress_callback=None):
"""
执行诊断任务,添加进度回调
@@ -101,10 +112,15 @@ def start_diagnose(parseStatus, filepath, progress_callback=None):
parseStatus.set_baseinfo_status(result_baseinfo)
if progress_callback:
- progress_callback(80, "正在解析告警信息...")
+ progress_callback(60, "正在解析告警信息...")
result_parseidl = parseidl.program_main()
parseStatus.set_parseidl_status(result_parseidl)
+ if progress_callback:
+ progress_callback(80, "正在整合日志文件...")
+ result_mergelog = mergelog.program_main()
+ parseStatus.set_mergelog_status(result_mergelog)
+
if progress_callback:
progress_callback(95, "正在生成时间线...")
result_eventline = zijin_event.program_main()
diff --git a/src/ui_initializer.py b/src/ui_initializer.py
index 161bd6d..6d1ff77 100644
--- a/src/ui_initializer.py
+++ b/src/ui_initializer.py
@@ -1,8 +1,9 @@
-from PyQt5.QtWidgets import QVBoxLayout, QGraphicsScene
-from PyQt5.QtGui import QPainter, QStandardItemModel
+from PyQt5.QtWidgets import QVBoxLayout, QGraphicsScene, QShortcut
+from PyQt5.QtGui import (QPainter, QStandardItemModel, QTextOption, QKeySequence,
+ QTextCharFormat, QBrush, QColor)
from PyQt5.QtCore import Qt
import timelineEvent
-
+from search_shortcut import FindDialog
class UIInitializer:
def __init__(self, main_window):
self.main_window = main_window # 持有主窗口引用
@@ -79,4 +80,26 @@ class UIInitializer:
# 列宽自适应
self.main_window.tableView_alert.horizontalHeader().setSectionResizeMode(
self.main_window.tableView_alert.horizontalHeader().ResizeToContents
- )
\ No newline at end of file
+ )
+
+ def show_find_dialog(self):
+ """显示查找对话框"""
+ # 如果文本浏览器有内容才显示查找对话框
+ if self.main_window.textBrowser_logView.toPlainText():
+ dialog = FindDialog(self.main_window, self.main_window.textBrowser_logView)
+ dialog.exec_()
+
+ def init_textbrowser_logview_style(self):
+ # 关键设置:禁用自动换行,启用横向滚动条
+ self.main_window.textBrowser_logView.setWordWrapMode(QTextOption.NoWrap)
+
+ # 设置字体为等宽字体,更适合查看日志
+ font = self.main_window.textBrowser_logView.font()
+ font.setFamily("Consolas") # Windows系统
+ # font.setFamily("Monaco") # macOS系统
+ # font.setFamily("Monospace") # Linux系统
+ self.main_window.textBrowser_logView.setFont(font)
+
+ # 设置Ctrl+F快捷键
+ self.main_window.textBrowser_logView.find_shortcut = QShortcut(QKeySequence("Ctrl+F"), self.main_window)
+ self.main_window.textBrowser_logView.find_shortcut.activated.connect(self.show_find_dialog)
\ No newline at end of file
diff --git a/src/utils.py b/src/utils.py
index 5971cb7..b83c718 100644
--- a/src/utils.py
+++ b/src/utils.py
@@ -3,7 +3,9 @@ import tarfile
import shutil
import stat
import re
+import gzip
from datetime import datetime
+from pathlib import Path
from PyQt5.QtWidgets import QMessageBox
def show_error_message(parent, file_path):
@@ -397,4 +399,53 @@ def parse_idllog_line(log_line):
except Exception as e:
print(f"解析日志行时出错: {str(e)}")
- return None
\ No newline at end of file
+ return None
+
+def extract_maintenancelog_gz_files(input_dir, output_dir, max_files=10):
+ """
+ 解压指定目录下的maintenancelog.1.gz到log.max_files.gz文件到目标目录
+
+ 参数:
+ input_dir: 压缩文件所在的目录路径
+ output_dir: 解压后文件的保存目录路径
+ max_files: 最大文件编号,默认为10
+ """
+ # 确保输入输出目录存在
+ input_path = Path(input_dir)
+ output_path = Path(output_dir)
+
+ # 创建输出目录(如果不存在)
+ output_path.mkdir(parents=True, exist_ok=True)
+
+ # 检查输入目录是否存在
+ if not input_path.exists() or not input_path.is_dir():
+ print(f"错误:输入目录 '{input_path}' 不存在或不是一个目录")
+ return
+
+ for i in range(1, max_files + 1):
+ # 压缩文件路径
+ gz_filename = input_path / f"maintenance.log.{i}.gz"
+
+ # 检查文件是否存在
+ if not gz_filename.exists() or not gz_filename.is_file():
+ print(f"文件 {gz_filename} 不存在,跳过")
+ continue
+
+ # 解压后的文件路径
+ output_filename = output_path / f"maintenance.log.{i}"
+
+ try:
+ # 打开压缩文件并解压
+ with gzip.open(gz_filename, 'rb') as f_in:
+ with open(output_filename, 'wb') as f_out:
+ # 分块读取写入,处理大文件更高效
+ while True:
+ chunk = f_in.read(1024 * 1024) # 1MB块
+ if not chunk:
+ break
+ f_out.write(chunk)
+
+ print(f"成功解压: {gz_filename} -> {output_filename}")
+
+ except Exception as e:
+ print(f"解压 {gz_filename} 时出错: {str(e)}")
\ No newline at end of file