feat: refactor logging system into a global service

This commit is contained in:
wonfen 2025-03-02 04:20:38 +08:00
parent b3e4defc0f
commit ba0a291d97
4 changed files with 223 additions and 56 deletions

View File

@ -5,18 +5,19 @@ import { useClashInfo } from "./use-clash";
import dayjs from "dayjs";
import { create } from "zustand";
import { useVisibility } from "./use-visibility";
import {
useGlobalLogData,
clearGlobalLogs,
LogLevel,
ILogItem,
} from "@/services/global-log-service";
// 为了向后兼容,导出相同的类型
export type { LogLevel };
export type { ILogItem };
const MAX_LOG_NUM = 1000;
export type LogLevel = "warning" | "info" | "debug" | "error" | "all";
interface ILogItem {
time?: string;
type: string;
payload: string;
[key: string]: any;
}
const buildWSUrl = (server: string, secret: string, logLevel: LogLevel) => {
const baseUrl = `ws://${server}/logs`;
const params = new URLSearchParams();
@ -57,45 +58,6 @@ const useLogStore = create<LogStore>(
}),
);
export const useLogData = (logLevel: LogLevel) => {
const { clashInfo } = useClashInfo();
const [enableLog] = useEnableLog();
const { logs, appendLog } = useLogStore();
const pageVisible = useVisibility();
export const useLogData = useGlobalLogData;
useEffect(() => {
if (!enableLog || !clashInfo || !pageVisible) return;
const { server = "", secret = "" } = clashInfo;
const wsUrl = buildWSUrl(server, secret, logLevel);
let isActive = true;
const socket = createSockette(wsUrl, {
onmessage(event) {
if (!isActive) return;
const data = JSON.parse(event.data) as ILogItem;
const time = dayjs().format("MM-DD HH:mm:ss");
appendLog({ ...data, time });
},
onerror() {
if (!isActive) return;
socket.close();
},
});
return () => {
isActive = false;
socket.close();
};
}, [clashInfo, enableLog, logLevel]);
// 根据当前选择的日志等级过滤日志
return logLevel === "all"
? logs
: logs.filter((log) => log.type.toLowerCase() === logLevel);
};
// 导出清空日志的方法
export const clearLogs = () => {
useLogStore.getState().clearLogs();
};
export const clearLogs = clearGlobalLogs;

View File

@ -13,7 +13,7 @@ import { useVerge } from "@/hooks/use-verge";
import LogoSvg from "@/assets/image/logo.svg?react";
import iconLight from "@/assets/image/icon_light.svg?react";
import iconDark from "@/assets/image/icon_dark.svg?react";
import { useThemeMode } from "@/services/states";
import { useThemeMode, useEnableLog } from "@/services/states";
import { Notice } from "@/components/base";
import { LayoutItem } from "@/components/layout/layout-item";
import { LayoutControl } from "@/components/layout/layout-control";
@ -28,6 +28,8 @@ import React from "react";
import { TransitionGroup, CSSTransition } from "react-transition-group";
import { useListen } from "@/hooks/use-listen";
import { listen } from "@tauri-apps/api/event";
import { useClashInfo } from "@/hooks/use-clash";
import { initGlobalLogService } from "@/services/global-log-service";
const appWindow = getCurrentWebviewWindow();
export let portableFlag = false;
@ -126,6 +128,8 @@ const Layout = () => {
const { t } = useTranslation();
const { theme } = useCustomTheme();
const { verge } = useVerge();
const { clashInfo } = useClashInfo();
const [enableLog] = useEnableLog();
const { language, start_page } = verge ?? {};
const navigate = useNavigate();
const location = useLocation();
@ -140,6 +144,15 @@ const Layout = () => {
[t, navigate],
);
// 初始化全局日志服务
useEffect(() => {
if (clashInfo) {
const { server = "", secret = "" } = clashInfo;
// 使用本地存储中的enableLog值初始化全局日志服务
initGlobalLogService(server, secret, enableLog, "info");
}
}, [clashInfo, enableLog]);
// 设置监听器
useEffect(() => {
const listeners = [

View File

@ -8,7 +8,8 @@ import {
PlayCircleOutlineRounded,
PauseCircleOutlineRounded,
} from "@mui/icons-material";
import { useLogData, LogLevel, clearLogs } from "@/hooks/use-log-data";
import { LogLevel, clearLogs } from "@/hooks/use-log-data";
import { useClashInfo } from "@/hooks/use-clash";
import { useEnableLog } from "@/services/states";
import { BaseEmpty, BasePage } from "@/components/base";
import LogItem from "@/components/log/log-item";
@ -16,10 +17,17 @@ import { useTheme } from "@mui/material/styles";
import { BaseSearchBox } from "@/components/base/base-search-box";
import { BaseStyledSelect } from "@/components/base/base-styled-select";
import { SearchState } from "@/components/base/base-search-box";
import {
useGlobalLogData,
clearGlobalLogs,
changeLogLevel,
toggleLogEnabled,
} from "@/services/global-log-service";
const LogPage = () => {
const { t } = useTranslation();
const [enableLog, setEnableLog] = useEnableLog();
const { clashInfo } = useClashInfo();
const theme = useTheme();
const isDark = theme.palette.mode === "dark";
const [logLevel, setLogLevel] = useLocalStorage<LogLevel>(
@ -27,7 +35,7 @@ const LogPage = () => {
"info",
);
const [match, setMatch] = useState(() => (_: string) => true);
const logData = useLogData(logLevel);
const logData = useGlobalLogData(logLevel);
const [searchState, setSearchState] = useState<SearchState>();
const filterLogs = useMemo(() => {
@ -44,6 +52,22 @@ const LogPage = () => {
: [];
}, [logData, logLevel, match]);
const handleLogLevelChange = (newLevel: LogLevel) => {
setLogLevel(newLevel);
if (clashInfo) {
const { server = "", secret = "" } = clashInfo;
changeLogLevel(newLevel, server, secret);
}
};
const handleToggleLog = () => {
if (clashInfo) {
const { server = "", secret = "" } = clashInfo;
toggleLogEnabled(server, secret);
setEnableLog(!enableLog);
}
};
return (
<BasePage
full
@ -60,7 +84,7 @@ const LogPage = () => {
title={t("Pause")}
size="small"
color="inherit"
onClick={() => setEnableLog((e) => !e)}
onClick={handleToggleLog}
>
{enableLog ? (
<PauseCircleOutlineRounded />
@ -74,7 +98,7 @@ const LogPage = () => {
size="small"
variant="contained"
onClick={() => {
clearLogs();
clearGlobalLogs();
}}
>
{t("Clear")}
@ -95,7 +119,7 @@ const LogPage = () => {
>
<BaseStyledSelect
value={logLevel}
onChange={(e) => setLogLevel(e.target.value as LogLevel)}
onChange={(e) => handleLogLevelChange(e.target.value as LogLevel)}
>
<MenuItem value="all">ALL</MenuItem>
<MenuItem value="info">INFO</MenuItem>

View File

@ -0,0 +1,168 @@
// 全局日志服务,使应用在任何页面都能收集日志
import { create } from "zustand";
import { createSockette } from "../utils/websocket";
import dayjs from "dayjs";
import { useState, useEffect } from "react";
// 最大日志数量
const MAX_LOG_NUM = 1000;
export type LogLevel = "warning" | "info" | "debug" | "error" | "all";
export interface ILogItem {
time?: string;
type: string;
payload: string;
[key: string]: any;
}
interface GlobalLogStore {
logs: ILogItem[];
enabled: boolean;
isConnected: boolean;
currentLevel: LogLevel;
setEnabled: (enabled: boolean) => void;
setCurrentLevel: (level: LogLevel) => void;
clearLogs: () => void;
appendLog: (log: ILogItem) => void;
}
// 创建全局状态存储
export const useGlobalLogStore = create<GlobalLogStore>((set) => ({
logs: [],
enabled: false,
isConnected: false,
currentLevel: "info",
setEnabled: (enabled) => set({ enabled }),
setCurrentLevel: (currentLevel) => set({ currentLevel }),
clearLogs: () => set({ logs: [] }),
appendLog: (log: ILogItem) =>
set((state) => {
const newLogs =
state.logs.length >= MAX_LOG_NUM
? [...state.logs.slice(1), log]
: [...state.logs, log];
return { logs: newLogs };
}),
}));
// 构建WebSocket URL
const buildWSUrl = (server: string, secret: string, logLevel: LogLevel) => {
const baseUrl = `ws://${server}/logs`;
const params = new URLSearchParams();
if (secret) {
params.append("token", secret);
}
if (logLevel === "all") {
params.append("level", "debug");
} else {
params.append("level", logLevel);
}
const queryString = params.toString();
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
};
// 初始化全局日志服务
let globalLogSocket: any = null;
export const initGlobalLogService = (
server: string,
secret: string,
enabled: boolean = false,
logLevel: LogLevel = "info",
) => {
const { appendLog, setEnabled } = useGlobalLogStore.getState();
// 更新启用状态
setEnabled(enabled);
// 如果不启用或没有服务器信息,则不初始化
if (!enabled || !server) {
closeGlobalLogConnection();
return;
}
// 关闭现有连接
closeGlobalLogConnection();
// 创建新的WebSocket连接
const wsUrl = buildWSUrl(server, secret, logLevel);
globalLogSocket = createSockette(wsUrl, {
onmessage(event) {
try {
const data = JSON.parse(event.data) as ILogItem;
const time = dayjs().format("MM-DD HH:mm:ss");
appendLog({ ...data, time });
} catch (error) {
console.error("Failed to parse log data:", error);
}
},
onerror() {
console.error("Log WebSocket connection error");
closeGlobalLogConnection();
},
onclose() {
console.log("Log WebSocket connection closed");
useGlobalLogStore.setState({ isConnected: false });
},
onopen() {
console.log("Log WebSocket connection opened");
useGlobalLogStore.setState({ isConnected: true });
},
});
};
// 关闭全局日志连接
export const closeGlobalLogConnection = () => {
if (globalLogSocket) {
globalLogSocket.close();
globalLogSocket = null;
useGlobalLogStore.setState({ isConnected: false });
}
};
// 切换日志级别
export const changeLogLevel = (
level: LogLevel,
server: string,
secret: string,
) => {
const { enabled } = useGlobalLogStore.getState();
useGlobalLogStore.setState({ currentLevel: level });
if (enabled && server) {
initGlobalLogService(server, secret, enabled, level);
}
};
// 切换启用状态
export const toggleLogEnabled = (server: string, secret: string) => {
const { enabled, currentLevel } = useGlobalLogStore.getState();
const newEnabled = !enabled;
useGlobalLogStore.setState({ enabled: newEnabled });
if (newEnabled && server) {
initGlobalLogService(server, secret, newEnabled, currentLevel);
} else {
closeGlobalLogConnection();
}
};
// 获取日志清理函数
export const clearGlobalLogs = () => {
useGlobalLogStore.getState().clearLogs();
};
// 自定义钩子,用于获取过滤后的日志数据
export const useGlobalLogData = (logLevel: LogLevel = "all") => {
const logs = useGlobalLogStore((state) => state.logs);
// 根据当前选择的日志等级过滤日志
if (logLevel === "all") {
return logs;
} else {
return logs.filter((log) => log.type.toLowerCase() === logLevel);
}
};