feat(web): add service log level controls (#2227)

- centralize gateway log level resolution and normalization
- propagate debug flags to spawned launcher and gateway processes
- add a log level selector to the logs page
- cover the new behavior with backend and config tests
This commit is contained in:
wenjie
2026-03-31 20:32:42 +08:00
committed by GitHub
parent 848f9dd2e9
commit 2bf842e460
18 changed files with 471 additions and 40 deletions
@@ -0,0 +1,102 @@
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { type AppConfig, getAppConfig, patchAppConfig } from "@/api/channels"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { refreshGatewayState } from "@/store/gateway"
const LOG_LEVEL_OPTIONS = ["debug", "info", "warn", "error", "fatal"] as const
type GatewayLogLevel = (typeof LOG_LEVEL_OPTIONS)[number]
const LOG_LEVEL_LABELS: Record<GatewayLogLevel, string> = {
debug: "Debug",
info: "Info",
warn: "Warn",
error: "Error",
fatal: "Fatal",
}
function getGatewayLogLevel(config: AppConfig | undefined): GatewayLogLevel {
const gateway = config?.gateway
if (typeof gateway === "object" && gateway !== null) {
const logLevel = (gateway as Record<string, unknown>).log_level
if (
typeof logLevel === "string" &&
LOG_LEVEL_OPTIONS.includes(logLevel as GatewayLogLevel)
) {
return logLevel as GatewayLogLevel
}
}
return "warn"
}
export function LogLevelSelect() {
const { t } = useTranslation()
const queryClient = useQueryClient()
const [logLevel, setLogLevel] = useState<GatewayLogLevel>("warn")
const [savingLogLevel, setSavingLogLevel] = useState(false)
const { data: configData } = useQuery({
queryKey: ["config"],
queryFn: getAppConfig,
})
useEffect(() => {
setLogLevel(getGatewayLogLevel(configData))
}, [configData])
const handleLogLevelChange = async (nextValue: string) => {
const nextLevel = nextValue as GatewayLogLevel
const previousLevel = logLevel
setLogLevel(nextLevel)
setSavingLogLevel(true)
try {
await patchAppConfig({
gateway: {
log_level: nextLevel,
},
})
await queryClient.invalidateQueries({ queryKey: ["config"] })
await refreshGatewayState({ force: true })
} catch (error) {
setLogLevel(previousLevel)
toast.error(
error instanceof Error
? error.message
: t("pages.logs.log_level_error"),
)
} finally {
setSavingLogLevel(false)
}
}
return (
<div className="flex items-center gap-2">
<Select
value={logLevel}
onValueChange={handleLogLevelChange}
disabled={savingLogLevel}
>
<SelectTrigger size="sm" className="w-28">
<SelectValue />
</SelectTrigger>
<SelectContent align="end">
{LOG_LEVEL_OPTIONS.map((level) => (
<SelectItem key={level} value={level}>
{LOG_LEVEL_LABELS[level]}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}
+14 -9
View File
@@ -1,6 +1,7 @@
import { IconTrash } from "@tabler/icons-react"
import { useTranslation } from "react-i18next"
import { LogLevelSelect } from "@/components/logs/log-level-select"
import { LogsPanel } from "@/components/logs/logs-panel"
import { PageHeader } from "@/components/page-header"
import { Button } from "@/components/ui/button"
@@ -17,15 +18,19 @@ export function LogsPage() {
<PageHeader
title={t("navigation.logs")}
children={
<Button
variant="outline"
size="sm"
onClick={clearLogs}
disabled={logs.length === 0 || clearing}
>
<IconTrash className="size-4" />
{t("pages.logs.clear")}
</Button>
<>
<LogLevelSelect />
<Button
variant="outline"
size="sm"
onClick={clearLogs}
disabled={logs.length === 0 || clearing}
>
<IconTrash className="size-4" />
{t("pages.logs.clear")}
</Button>
</>
}
/>
+1
View File
@@ -547,6 +547,7 @@
"unsaved_changes": "You have unsaved changes."
},
"logs": {
"log_level_error": "Failed to update log level.",
"clear": "Clear logs",
"empty": "Waiting for logs..."
}
+1
View File
@@ -547,6 +547,7 @@
"unsaved_changes": "您有未保存的更改。"
},
"logs": {
"log_level_error": "更新日志等级失败。",
"clear": "清空日志",
"empty": "等待日志中..."
}