|
|
"""日志管理模块
|
|
|
|
|
|
该模块实现了应用程序的日志记录功能,包括日志的记录、保存、加载和查看。
|
|
|
LoggerManager类提供了完整的日志管理接口,支持多种日志级别和日志过滤功能。
|
|
|
"""
|
|
|
|
|
|
import tkinter as tk
|
|
|
import customtkinter as ctk
|
|
|
from tkinter import messagebox
|
|
|
from datetime import datetime
|
|
|
import json
|
|
|
import os
|
|
|
|
|
|
class LoggerManager:
|
|
|
"""日志管理器类
|
|
|
|
|
|
负责应用程序的日志记录、存储和查看功能。支持多种日志级别,
|
|
|
可以记录用户操作、系统事件和错误信息,并提供日志过滤和显示功能。
|
|
|
|
|
|
属性:
|
|
|
app: 应用程序主对象
|
|
|
root: 主窗口对象
|
|
|
logs: 日志列表,存储所有日志条目
|
|
|
log_file: 日志文件路径
|
|
|
|
|
|
方法:
|
|
|
ensure_log_file_exists: 确保日志文件存在
|
|
|
load_logs: 从文件加载日志数据
|
|
|
save_logs: 将日志数据保存到文件
|
|
|
log: 记录新的日志条目
|
|
|
show_logs: 显示日志查看窗口
|
|
|
"""
|
|
|
def __init__(self, app):
|
|
|
"""初始化日志管理器
|
|
|
|
|
|
参数:
|
|
|
app: 应用程序主对象,包含根窗口和其他应用程序组件
|
|
|
"""
|
|
|
self.app = app # 应用程序主对象
|
|
|
self.root = app.root if app else None # 主窗口对象
|
|
|
self.logs = [] # 日志列表,存储所有日志条目
|
|
|
self.log_file = "data/app_logs.json" # 日志文件路径
|
|
|
self.ensure_log_file_exists() # 确保日志文件存在
|
|
|
self.load_logs() # 从文件加载现有日志
|
|
|
|
|
|
def ensure_log_file_exists(self):
|
|
|
"""确保日志文件存在
|
|
|
|
|
|
创建data目录(如果不存在),并初始化空的日志文件。
|
|
|
"""
|
|
|
os.makedirs("data", exist_ok=True) # 创建data目录,exist_ok=True表示目录已存在也不报错
|
|
|
if not os.path.exists(self.log_file): # 检查日志文件是否存在
|
|
|
with open(self.log_file, "w", encoding="utf-8") as f:
|
|
|
json.dump([], f, ensure_ascii=False, indent=2) # 创建空的日志文件
|
|
|
|
|
|
def load_logs(self):
|
|
|
"""从文件加载日志数据
|
|
|
|
|
|
从日志文件中读取JSON格式的日志数据并存储到内存中。
|
|
|
如果加载失败,初始化空的日志列表。
|
|
|
"""
|
|
|
try:
|
|
|
with open(self.log_file, "r", encoding="utf-8") as f:
|
|
|
self.logs = json.load(f) # 读取并解析日志文件
|
|
|
except Exception as e: # 捕获所有可能的异常
|
|
|
print(f"加载日志失败: {e}") # 打印错误信息
|
|
|
self.logs = [] # 初始化空日志列表
|
|
|
|
|
|
def save_logs(self):
|
|
|
"""将日志数据保存到文件
|
|
|
|
|
|
将内存中的日志数据以JSON格式保存到日志文件中。
|
|
|
|
|
|
返回:
|
|
|
bool: 保存成功返回True,失败返回False
|
|
|
"""
|
|
|
try:
|
|
|
with open(self.log_file, "w", encoding="utf-8") as f:
|
|
|
json.dump(self.logs, f, ensure_ascii=False, indent=2) # 保存日志数据到文件
|
|
|
return True # 保存成功
|
|
|
except Exception as e: # 捕获所有可能的异常
|
|
|
print(f"保存日志失败: {e}") # 打印错误信息
|
|
|
return False # 保存失败
|
|
|
|
|
|
def log(self, level, message, username=None):
|
|
|
"""记录日志
|
|
|
|
|
|
创建新的日志条目并保存到日志文件中。
|
|
|
|
|
|
参数:
|
|
|
level: 日志级别 (debug, info, warning, error)
|
|
|
message: 日志消息内容
|
|
|
username: 用户名(可选,默认为None)
|
|
|
"""
|
|
|
# 创建日志条目
|
|
|
log_entry = {
|
|
|
"id": str(datetime.now().timestamp()), # 使用时间戳作为唯一ID
|
|
|
"level": level, # 日志级别
|
|
|
"message": message, # 日志消息
|
|
|
"timestamp": datetime.now().isoformat(), # 时间戳(ISO格式)
|
|
|
"username": username # 用户名(可选)
|
|
|
}
|
|
|
|
|
|
self.logs.append(log_entry) # 添加到日志列表
|
|
|
|
|
|
# 只保留最近1000条日志,防止日志文件过大
|
|
|
if len(self.logs) > 1000:
|
|
|
self.logs = self.logs[-1000:] # 保留最后1000条日志
|
|
|
|
|
|
self.save_logs() # 保存日志到文件
|
|
|
|
|
|
def show_logs(self, parent_frame=None):
|
|
|
"""显示日志
|
|
|
|
|
|
在指定的父框架中显示日志查看界面,包含日志过滤、搜索和显示功能。
|
|
|
支持按日志级别过滤和按关键词搜索日志内容。
|
|
|
|
|
|
Args:
|
|
|
parent_frame (CTkFrame, optional): 要显示日志的父框架。如果为None,则创建新窗口(兼容旧代码)。
|
|
|
"""
|
|
|
# 只有在有root窗口的情况下才支持显示日志界面
|
|
|
if not self.root and not parent_frame:
|
|
|
print("无法显示日志界面:未提供父窗口或根窗口")
|
|
|
return
|
|
|
|
|
|
# 决定使用哪个框架作为父容器
|
|
|
if parent_frame:
|
|
|
# 清空父框架中的所有组件
|
|
|
for widget in parent_frame.winfo_children():
|
|
|
widget.destroy()
|
|
|
container = parent_frame
|
|
|
else:
|
|
|
# 兼容旧代码:创建新窗口
|
|
|
container = ctk.CTkToplevel(self.root)
|
|
|
container.title("系统日志")
|
|
|
container.geometry("900x700")
|
|
|
container.configure(fg_color="#1e1e1e")
|
|
|
# 保持窗口在最前面(仅当是新窗口时)
|
|
|
container.attributes("-topmost", True)
|
|
|
|
|
|
# 日志过滤器框架
|
|
|
filter_frame = ctk.CTkFrame(container, fg_color="#2a2a2a") # 创建过滤器框架
|
|
|
filter_frame.pack(fill="x", padx=10, pady=10) # 放置过滤器框架
|
|
|
|
|
|
# 日志级别过滤器
|
|
|
level_label = ctk.CTkLabel(filter_frame, text="日志级别:", text_color="#ffffff") # 级别标签
|
|
|
level_label.pack(side="left", padx=10, pady=5) # 放置级别标签
|
|
|
|
|
|
level_filter = tk.StringVar(value="all") # 级别过滤变量,默认显示所有
|
|
|
level_options = ["all", "debug", "info", "warning", "error"] # 可用的日志级别选项
|
|
|
level_combobox = ctk.CTkComboBox(filter_frame, values=level_options, variable=level_filter, width=120) # 级别选择框
|
|
|
level_combobox.pack(side="left", padx=10, pady=5) # 放置级别选择框
|
|
|
|
|
|
# 搜索框
|
|
|
search_label = ctk.CTkLabel(filter_frame, text="搜索:", text_color="#ffffff") # 搜索标签
|
|
|
search_label.pack(side="left", padx=10, pady=5) # 放置搜索标签
|
|
|
|
|
|
search_var = tk.StringVar() # 搜索文本变量
|
|
|
search_entry = ctk.CTkEntry(filter_frame, textvariable=search_var, width=200) # 搜索输入框
|
|
|
search_entry.pack(side="left", padx=10, pady=5) # 放置搜索输入框
|
|
|
|
|
|
# 日志显示区域
|
|
|
log_text = ctk.CTkTextbox(container, wrap="word", fg_color="#1e1e1e", text_color="#ffffff", border_color="#3a3a3a") # 日志文本框
|
|
|
log_text.pack(fill="both", expand=True, padx=10, pady=10) # 放置日志文本框
|
|
|
|
|
|
# 日志级别颜色映射 - 为不同级别设置不同颜色以提高可读性
|
|
|
level_colors = {
|
|
|
"debug": "#4a90e2", # 调试信息 - 深蓝色
|
|
|
"info": "#4caf50", # 普通信息 - 深绿色
|
|
|
"warning": "#ffa000", # 警告信息 - 深橙色
|
|
|
"error": "#f44336" # 错误信息 - 红色
|
|
|
}
|
|
|
|
|
|
def display_logs(level_filter_val, search_term):
|
|
|
"""显示过滤后的日志
|
|
|
|
|
|
根据指定的日志级别和搜索词过滤日志,并在文本框中显示。
|
|
|
每条日志按时间戳、级别、用户名和消息的格式显示,并为不同级别设置颜色。
|
|
|
|
|
|
参数:
|
|
|
level_filter_val: 日志级别过滤值 ("all"表示显示所有级别)
|
|
|
search_term: 搜索关键词
|
|
|
"""
|
|
|
log_text.delete(1.0, tk.END) # 清空文本框
|
|
|
|
|
|
# 过滤日志
|
|
|
filtered_logs = self.logs.copy() # 复制日志列表,避免修改原始数据
|
|
|
|
|
|
# 按级别过滤
|
|
|
if level_filter_val != "all": # 如果不是显示所有级别
|
|
|
filtered_logs = [log for log in filtered_logs if log["level"] == level_filter_val] # 按级别筛选
|
|
|
|
|
|
# 按搜索词过滤
|
|
|
if search_term: # 如果有搜索词
|
|
|
search_term = search_term.lower() # 转换为小写以实现大小写不敏感搜索
|
|
|
filtered_logs = [log for log in filtered_logs if
|
|
|
search_term in log["message"].lower() or # 搜索消息内容
|
|
|
(log["username"] and search_term in log["username"].lower())] # 搜索用户名
|
|
|
|
|
|
# 按时间排序(最新的在最前面)
|
|
|
filtered_logs.sort(key=lambda x: x["timestamp"], reverse=True)
|
|
|
|
|
|
for log in filtered_logs:
|
|
|
# 构建日志显示文本
|
|
|
timestamp = log["timestamp"] # 时间戳
|
|
|
level = log["level"].upper() # 日志级别(转为大写显示)
|
|
|
username = log["username"] if log["username"] else "-" # 用户名,为空时显示"-"
|
|
|
message = log["message"] # 日志消息
|
|
|
|
|
|
log_line = f"[{timestamp}] [{level}] [{username}] {message}\n" # 格式化日志行
|
|
|
|
|
|
# 设置日志级别颜色
|
|
|
color = level_colors.get(log["level"], "#ffffff") # 获取对应级别的颜色,默认白色
|
|
|
|
|
|
# 插入日志行
|
|
|
log_text.insert(tk.END, log_line) # 在文本框末尾插入日志行
|
|
|
|
|
|
# 为日志级别部分设置颜色
|
|
|
line_start = log_text.index(f"end-1c linestart") # 获取当前行的起始位置
|
|
|
level_start = log_text.search(f"[{level}]", line_start) # 查找日志级别在当前行的位置
|
|
|
if level_start: # 如果找到
|
|
|
level_end = f"{level_start}+{len(f'[{level}]')}c" # 计算日志级别部分的结束位置
|
|
|
log_text.tag_add(level, level_start, level_end) # 为该部分添加标签
|
|
|
log_text.tag_config(level, foreground=color) # 设置标签颜色
|
|
|
|
|
|
# 刷新按钮
|
|
|
def refresh_logs():
|
|
|
"""刷新日志显示"""
|
|
|
display_logs(level_filter.get(), search_var.get()) # 调用display_logs函数显示过滤后的日志
|
|
|
|
|
|
refresh_button = ctk.CTkButton(filter_frame, text="刷新", command=refresh_logs) # 创建刷新按钮
|
|
|
refresh_button.pack(side="left", padx=10, pady=5) # 放置刷新按钮
|
|
|
|
|
|
# 清除日志按钮
|
|
|
def clear_logs():
|
|
|
"""清除所有日志
|
|
|
|
|
|
显示确认对话框,用户确认后清除所有日志并保存更改。
|
|
|
"""
|
|
|
if messagebox.askyesno("确认", "确定要清除所有日志吗?"): # 显示确认对话框
|
|
|
self.logs = [] # 清空日志列表
|
|
|
self.save_logs() # 保存更改到文件
|
|
|
display_logs(level_filter.get(), search_var.get()) # 重新显示日志(此时为空)
|
|
|
|
|
|
clear_button = ctk.CTkButton(filter_frame, text="清除日志", command=clear_logs, fg_color="#d32f2f") # 创建清除按钮,红色背景
|
|
|
clear_button.pack(side="left", padx=10, pady=5) # 放置清除按钮
|
|
|
|
|
|
# 初始显示日志
|
|
|
display_logs(level_filter.get(), search_var.get()) # 应用程序启动时显示所有日志
|
|
|
|
|
|
# 绑定搜索按键事件 - 按回车键执行搜索
|
|
|
def on_search(event):
|
|
|
"""处理搜索按键事件"""
|
|
|
display_logs(level_filter.get(), search_var.get()) # 执行搜索
|
|
|
|
|
|
search_entry.bind("<Return>", on_search) # 绑定回车键到搜索函数
|
|
|
|
|
|
# 保持窗口在最前面 - 确保日志窗口始终可见
|
|
|
if not parent_frame: # 仅当是新窗口时
|
|
|
container.attributes("-topmost", True)
|