mirror of
https://github.com/clash-verge-rev/clash-verge-rev
synced 2025-05-05 13:53:44 +08:00
feat: refactor logging system into a global service
This commit is contained in:
parent
b3e4defc0f
commit
ba0a291d97
@ -5,18 +5,19 @@ import { useClashInfo } from "./use-clash";
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { useVisibility } from "./use-visibility";
|
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;
|
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 buildWSUrl = (server: string, secret: string, logLevel: LogLevel) => {
|
||||||
const baseUrl = `ws://${server}/logs`;
|
const baseUrl = `ws://${server}/logs`;
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
@ -57,45 +58,6 @@ const useLogStore = create<LogStore>(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const useLogData = (logLevel: LogLevel) => {
|
export const useLogData = useGlobalLogData;
|
||||||
const { clashInfo } = useClashInfo();
|
|
||||||
const [enableLog] = useEnableLog();
|
|
||||||
const { logs, appendLog } = useLogStore();
|
|
||||||
const pageVisible = useVisibility();
|
|
||||||
|
|
||||||
useEffect(() => {
|
export const clearLogs = clearGlobalLogs;
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
@ -13,7 +13,7 @@ import { useVerge } from "@/hooks/use-verge";
|
|||||||
import LogoSvg from "@/assets/image/logo.svg?react";
|
import LogoSvg from "@/assets/image/logo.svg?react";
|
||||||
import iconLight from "@/assets/image/icon_light.svg?react";
|
import iconLight from "@/assets/image/icon_light.svg?react";
|
||||||
import iconDark from "@/assets/image/icon_dark.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 { Notice } from "@/components/base";
|
||||||
import { LayoutItem } from "@/components/layout/layout-item";
|
import { LayoutItem } from "@/components/layout/layout-item";
|
||||||
import { LayoutControl } from "@/components/layout/layout-control";
|
import { LayoutControl } from "@/components/layout/layout-control";
|
||||||
@ -28,6 +28,8 @@ import React from "react";
|
|||||||
import { TransitionGroup, CSSTransition } from "react-transition-group";
|
import { TransitionGroup, CSSTransition } from "react-transition-group";
|
||||||
import { useListen } from "@/hooks/use-listen";
|
import { useListen } from "@/hooks/use-listen";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import { useClashInfo } from "@/hooks/use-clash";
|
||||||
|
import { initGlobalLogService } from "@/services/global-log-service";
|
||||||
|
|
||||||
const appWindow = getCurrentWebviewWindow();
|
const appWindow = getCurrentWebviewWindow();
|
||||||
export let portableFlag = false;
|
export let portableFlag = false;
|
||||||
@ -126,6 +128,8 @@ const Layout = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { theme } = useCustomTheme();
|
const { theme } = useCustomTheme();
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
|
const { clashInfo } = useClashInfo();
|
||||||
|
const [enableLog] = useEnableLog();
|
||||||
const { language, start_page } = verge ?? {};
|
const { language, start_page } = verge ?? {};
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -140,6 +144,15 @@ const Layout = () => {
|
|||||||
[t, navigate],
|
[t, navigate],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 初始化全局日志服务
|
||||||
|
useEffect(() => {
|
||||||
|
if (clashInfo) {
|
||||||
|
const { server = "", secret = "" } = clashInfo;
|
||||||
|
// 使用本地存储中的enableLog值初始化全局日志服务
|
||||||
|
initGlobalLogService(server, secret, enableLog, "info");
|
||||||
|
}
|
||||||
|
}, [clashInfo, enableLog]);
|
||||||
|
|
||||||
// 设置监听器
|
// 设置监听器
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listeners = [
|
const listeners = [
|
||||||
|
@ -8,7 +8,8 @@ import {
|
|||||||
PlayCircleOutlineRounded,
|
PlayCircleOutlineRounded,
|
||||||
PauseCircleOutlineRounded,
|
PauseCircleOutlineRounded,
|
||||||
} from "@mui/icons-material";
|
} 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 { useEnableLog } from "@/services/states";
|
||||||
import { BaseEmpty, BasePage } from "@/components/base";
|
import { BaseEmpty, BasePage } from "@/components/base";
|
||||||
import LogItem from "@/components/log/log-item";
|
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 { BaseSearchBox } from "@/components/base/base-search-box";
|
||||||
import { BaseStyledSelect } from "@/components/base/base-styled-select";
|
import { BaseStyledSelect } from "@/components/base/base-styled-select";
|
||||||
import { SearchState } from "@/components/base/base-search-box";
|
import { SearchState } from "@/components/base/base-search-box";
|
||||||
|
import {
|
||||||
|
useGlobalLogData,
|
||||||
|
clearGlobalLogs,
|
||||||
|
changeLogLevel,
|
||||||
|
toggleLogEnabled,
|
||||||
|
} from "@/services/global-log-service";
|
||||||
|
|
||||||
const LogPage = () => {
|
const LogPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [enableLog, setEnableLog] = useEnableLog();
|
const [enableLog, setEnableLog] = useEnableLog();
|
||||||
|
const { clashInfo } = useClashInfo();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isDark = theme.palette.mode === "dark";
|
const isDark = theme.palette.mode === "dark";
|
||||||
const [logLevel, setLogLevel] = useLocalStorage<LogLevel>(
|
const [logLevel, setLogLevel] = useLocalStorage<LogLevel>(
|
||||||
@ -27,7 +35,7 @@ const LogPage = () => {
|
|||||||
"info",
|
"info",
|
||||||
);
|
);
|
||||||
const [match, setMatch] = useState(() => (_: string) => true);
|
const [match, setMatch] = useState(() => (_: string) => true);
|
||||||
const logData = useLogData(logLevel);
|
const logData = useGlobalLogData(logLevel);
|
||||||
const [searchState, setSearchState] = useState<SearchState>();
|
const [searchState, setSearchState] = useState<SearchState>();
|
||||||
|
|
||||||
const filterLogs = useMemo(() => {
|
const filterLogs = useMemo(() => {
|
||||||
@ -44,6 +52,22 @@ const LogPage = () => {
|
|||||||
: [];
|
: [];
|
||||||
}, [logData, logLevel, match]);
|
}, [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 (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
full
|
full
|
||||||
@ -60,7 +84,7 @@ const LogPage = () => {
|
|||||||
title={t("Pause")}
|
title={t("Pause")}
|
||||||
size="small"
|
size="small"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={() => setEnableLog((e) => !e)}
|
onClick={handleToggleLog}
|
||||||
>
|
>
|
||||||
{enableLog ? (
|
{enableLog ? (
|
||||||
<PauseCircleOutlineRounded />
|
<PauseCircleOutlineRounded />
|
||||||
@ -74,7 +98,7 @@ const LogPage = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clearLogs();
|
clearGlobalLogs();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("Clear")}
|
{t("Clear")}
|
||||||
@ -95,7 +119,7 @@ const LogPage = () => {
|
|||||||
>
|
>
|
||||||
<BaseStyledSelect
|
<BaseStyledSelect
|
||||||
value={logLevel}
|
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="all">ALL</MenuItem>
|
||||||
<MenuItem value="info">INFO</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