feat : 新增日志查阅功能
This commit is contained in:
parent
a11865d6da
commit
8f0f80c644
@ -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
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
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)
|
||||
53
src/utils.py
53
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
|
||||
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)}")
|
||||
Loading…
Reference in New Issue
Block a user