feat : 新增日志查阅功能

This commit is contained in:
leimingsheng 2025-08-27 09:02:49 +08:00
parent a11865d6da
commit 8f0f80c644
10 changed files with 353 additions and 9 deletions

@ -151,6 +151,53 @@ p, li { white-space: pre-wrap; }
<string>事件时间线</string>
</attribute>
</widget>
<widget class="QWidget" name="tab_readlog">
<attribute name="title">
<string>日志查看</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QComboBox" name="comboBox_logSelect">
<property name="currentText">
<string>IDL日志</string>
</property>
<item>
<property name="text">
<string>IDL日志</string>
</property>
</item>
<item>
<property name="text">
<string>审计日志</string>
</property>
</item>
<item>
<property name="text">
<string>维护日志</string>
</property>
</item>
<item>
<property name="text">
<string>OS串口日志</string>
</property>
</item>
<item>
<property name="text">
<string>运行状态</string>
</property>
</item>
<item>
<property name="text">
<string>部件日志</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QTextBrowser" name="textBrowser_logView"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>

@ -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", "上传日志文件到程序中"))

58
src/ZiJin_mergeLog.py Normal file

@ -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

@ -37,4 +37,27 @@ class DiagnoseThread(QThread):
def stop(self):
"""停止线程"""
self.is_running = False
self.wait()
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()

@ -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):
"""拖入事件:判断是否为文件"""

@ -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"
}

65
src/search_shortcut.py Normal file

@ -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}'")

@ -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()

@ -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
)
)
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)

@ -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
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)}")