mirror of
https://github.com/clash-verge-rev/clash-verge-rev
synced 2025-05-05 05:33:45 +08:00
feat: refactor logging system into a global service
This commit is contained in:
parent
028e4012aa
commit
3ecf4bc238
@ -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;
|
||||
|
@ -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 = [
|
||||
|
@ -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>
|
||||
|
168
src/services/global-log-service.ts
Normal file
168
src/services/global-log-service.ts
Normal 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);
|
||||
}
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user