You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

261 lines
12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""日志管理模块
该模块实现了应用程序的日志记录功能,包括日志的记录、保存、加载和查看。
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)