From 6e5da1a608b6cc718683a43cb6a9e43f1e9226fa Mon Sep 17 00:00:00 2001 From: limbo <43649186+HUAHUAI23@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:56:57 +0800 Subject: [PATCH] feat: add app startup logs (#1992) --- server/src/application/pod.service.ts | 35 ++++ server/src/log/log.controller.ts | 32 +++- web/src/layouts/Function.tsx | 16 +- .../app/functions/mods/HeadPanel/index.tsx | 2 +- .../app/mods/StatusBar/LogsModal/index.scss | 14 ++ .../app/mods/StatusBar/LogsModal/index.tsx | 50 ++++-- .../app/mods/StatusBar/LogsModal/initLog.scss | 45 ++++++ .../app/mods/StatusBar/LogsModal/initLog.tsx | 153 ++++++++++++++++++ .../setting/SysSetting/AppInfoList/index.tsx | 12 +- 9 files changed, 332 insertions(+), 27 deletions(-) create mode 100644 web/src/pages/app/mods/StatusBar/LogsModal/initLog.scss create mode 100644 web/src/pages/app/mods/StatusBar/LogsModal/initLog.tsx diff --git a/server/src/application/pod.service.ts b/server/src/application/pod.service.ts index 83e0c49b48..b1f8cef6cb 100644 --- a/server/src/application/pod.service.ts +++ b/server/src/application/pod.service.ts @@ -7,6 +7,14 @@ import http from 'http' import { PodNameListDto, ContainerNameListDto } from './dto/pod.dto' import { LABEL_KEY_APP_ID } from 'src/constants' +export type PodStatus = { + appid: string + podStatus: { + name: string + podStatus: string + initContainerId?: string + }[] +} @Injectable() export class PodService { private readonly logger = new Logger(PodService.name) @@ -53,4 +61,31 @@ export class PodService { return containerNames } + + async getPodStatusListByAppid(appid: string): Promise { + const region = await this.regionService.findByAppId(appid) + const namespaceOfApp = GetApplicationNamespace(region, appid) + const coreV1Api = this.cluster.makeCoreV1Api(region) + const res: { response: http.IncomingMessage; body: V1PodList } = + await coreV1Api.listNamespacedPod( + namespaceOfApp, + undefined, + undefined, + undefined, + undefined, + `${LABEL_KEY_APP_ID}=${appid}`, + ) + const podStatus: PodStatus = { + appid: appid, + podStatus: [], + } + for (const item of res.body.items) { + podStatus.podStatus.push({ + name: item.metadata.name, + podStatus: item.status.phase, + initContainerId: item.status.initContainerStatuses[0]?.containerID, + }) + } + return podStatus + } } diff --git a/server/src/log/log.controller.ts b/server/src/log/log.controller.ts index 86b9e81a4d..06a58cb63c 100644 --- a/server/src/log/log.controller.ts +++ b/server/src/log/log.controller.ts @@ -109,18 +109,36 @@ export class LogController { containerName = appid } - let podNameList: string[] = ( - await this.podService.getPodNameListByAppid(appid) - ).podNameList + const podStatus = await this.podService.getPodStatusListByAppid(appid) - if (!podNameList.includes(podName) && podName !== 'all') { + if (!podStatus.podStatus[0]) { return new Observable((subscriber) => { - subscriber.error(new Error('podName not exist')) + subscriber.error(new Error('pod not exist')) }) } + const podNameList = podStatus.podStatus.map((pod) => pod.name) + + const initContainerId = podStatus.podStatus.map( + (pod) => pod.initContainerId, + ) + + if (containerName === 'init') { + for (const containerId of initContainerId) { + if (!containerId) { + return new Observable((subscriber) => { + subscriber.error(new Error('init container not exist')) + }) + } + } + } + if (podName !== 'all') { - podNameList = undefined + if (!podNameList.includes(podName)) { + return new Observable((subscriber) => { + subscriber.error(new Error('podName not exist')) + }) + } } const region = await this.regionService.findByAppId(appid) @@ -223,7 +241,7 @@ export class LogController { } } - if (podNameList && podNameList.length > 0) { + if (podName === 'all' && podNameList.length > 0) { podNameList.forEach((podName) => { fetchLog(podName) }) diff --git a/web/src/layouts/Function.tsx b/web/src/layouts/Function.tsx index 12c95af57a..7bd4bfe5e8 100644 --- a/web/src/layouts/Function.tsx +++ b/web/src/layouts/Function.tsx @@ -4,9 +4,10 @@ import { Badge, Center, Spinner, useColorMode } from "@chakra-ui/react"; import { useQuery } from "@tanstack/react-query"; import clsx from "clsx"; -import { APP_PHASE_STATUS, COLOR_MODE, Pages } from "@/constants/index"; +import { APP_PHASE_STATUS, APP_STATUS, COLOR_MODE, Pages } from "@/constants/index"; import { ApplicationControllerFindOne } from "@/apis/v1/applications"; +import InitLog from "@/pages/app/mods/StatusBar/LogsModal/initLog"; import useGlobalStore from "@/pages/globalStore"; export default function FunctionLayout() { @@ -64,12 +65,17 @@ export default function FunctionLayout() { ) : ( <> - {currentApp?.phase !== APP_PHASE_STATUS.Started && - currentApp?.phase !== APP_PHASE_STATUS.Stopped && - currentApp?.phase !== APP_PHASE_STATUS.Deleted ? ( + {currentApp.phase === APP_PHASE_STATUS.Starting && + currentApp.state !== APP_STATUS.Restarting ? ( + + ) : [ + APP_PHASE_STATUS.Creating, + APP_PHASE_STATUS.Deleting, + APP_PHASE_STATUS.Stopping, + ].includes(currentApp.phase) || currentApp.state === APP_STATUS.Restarting ? (
diff --git a/web/src/pages/app/functions/mods/HeadPanel/index.tsx b/web/src/pages/app/functions/mods/HeadPanel/index.tsx index ec90d08c6a..22748c0bed 100644 --- a/web/src/pages/app/functions/mods/HeadPanel/index.tsx +++ b/web/src/pages/app/functions/mods/HeadPanel/index.tsx @@ -88,7 +88,7 @@ function HeadPanel() { setCurrentFunction(item); }} > -
+
([]); @@ -58,6 +58,23 @@ export default function LogsModal(props: { children: React.ReactElement }) { const darkMode = useColorMode().colorMode === "dark"; + const addOrUpdateLog = (newLog: Log) => { + setLogs((pre) => { + const existingLogIndex = pre.findIndex((existingLog) => existingLog.id === newLog.id); + + if (existingLogIndex !== -1) { + const updatedLogs = [...pre]; + updatedLogs[existingLogIndex] = { + ...updatedLogs[existingLogIndex], + data: newLog.data, + }; + return updatedLogs; + } else { + return [...pre, newLog]; + } + }); + }; + const { data: podData } = useQuery( ["GetPodQuery"], () => { @@ -115,9 +132,7 @@ export default function LogsModal(props: { children: React.ReactElement }) { } if (msg.event === "log") { - const newLineCount = (msg.data.match(/\n/g) || []).length; - setLogs((pre) => [...pre, msg]); - setRowNumber((prevRowNumber) => prevRowNumber + newLineCount); + addOrUpdateLog(msg); retryCountRef.current = 0; } }, @@ -126,8 +141,7 @@ export default function LogsModal(props: { children: React.ReactElement }) { // if the server closes the connection unexpectedly, retry: if (retryCountRef.current < MAX_RETRIES) { retryCountRef.current += 1; - setRefresh((pre) => !pre); - setPaused(false); + throw new Error("connect closed unexpectedly, retrying..."); } }, @@ -142,18 +156,23 @@ export default function LogsModal(props: { children: React.ReactElement }) { useEffect(() => { if (!isOpen) return; + setRowCount(0); setLogs([]); setIsLoading(true); + setPaused(false); const ctrl = fetchLogs(); + return () => { ctrl?.abort(); }; }, [podName, containerName, isOpen, refresh, fetchLogs]); useEffect(() => { - const sortedLogs = [...logs].sort((a, b) => parseInt(a.id) - parseInt(b.id)); + const sortedLogs = logs.sort((a, b) => parseInt(a.id) - parseInt(b.id)); const concatenatedLogs = sortedLogs.map((log) => log.data).join(""); setRenderLogs(concatenatedLogs); + const totalRows = concatenatedLogs.split("\n").length; + setRowCount(totalRows); }, [logs]); useEffect(() => { @@ -233,21 +252,26 @@ export default function LogsModal(props: { children: React.ReactElement }) { ) : (
{ - setPaused(true); - }} > { if (e.scrollOffsetToBottom <= 0) { setPaused(false); + return; } + if (!e.scrollUpdateWasRequested) { + setPaused(true); + return; + } + setPaused(false); }} toolbar={
diff --git a/web/src/pages/app/mods/StatusBar/LogsModal/initLog.scss b/web/src/pages/app/mods/StatusBar/LogsModal/initLog.scss new file mode 100644 index 0000000000..02e7189fcf --- /dev/null +++ b/web/src/pages/app/mods/StatusBar/LogsModal/initLog.scss @@ -0,0 +1,45 @@ +/* stylelint-disable selector-class-pattern */ +#log-viewer-cover-container { + .pf-v5-c-text-input-group__icon { + visibility: hidden; + } + + .pf-v5-c-text-input-group__text-input:focus { + outline: none !important; + color: #000; + } + + .pf-m-current { + background: #91ded9 !important; + } + + .pf-m-match { + background: #daf4f2 !important; + } + + [data-theme="dark"] & .pf-v5-c-text-input-group__text-input:focus { + outline: none !important; + color: #fff; + } + + [data-theme="dark"] & .pf-m-current { + background: #47c8bf !important; + } + + [data-theme="dark"] & .pf-m-match { + background: #2b7873 !important; + } +} + +.log-viewer-cover-container-hide-scrollbar { + &, + & * { + &::-webkit-scrollbar { + width: 0 !important; + height: 0 !important; + } + + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } +} diff --git a/web/src/pages/app/mods/StatusBar/LogsModal/initLog.tsx b/web/src/pages/app/mods/StatusBar/LogsModal/initLog.tsx new file mode 100644 index 0000000000..a506a69178 --- /dev/null +++ b/web/src/pages/app/mods/StatusBar/LogsModal/initLog.tsx @@ -0,0 +1,153 @@ +import { useCallback, useEffect, useState } from "react"; +import { Badge, Center, Spinner, useColorMode } from "@chakra-ui/react"; +import { EventStreamContentType, fetchEventSource } from "@microsoft/fetch-event-source"; +import { LogViewer } from "@patternfly/react-log-viewer"; +import clsx from "clsx"; + +import "./initLog.scss"; + +import useGlobalStore from "@/pages/globalStore"; + +type Log = { + data: string; + event: string; + id: string; + retry?: number; +}; + +export default function InitLog() { + const { currentApp } = useGlobalStore((state) => state); + const [isLoading, setIsLoading] = useState(true); + const [rowCount, setRowCount] = useState(0); + const [paused, setPaused] = useState(false); + + const [logs, setLogs] = useState([]); + const [renderLogs, setRenderLogs] = useState(""); + + const darkMode = useColorMode().colorMode === "dark"; + + const addOrUpdateLog = (newLog: Log) => { + setLogs((pre) => { + const existingLogIndex = pre.findIndex((existingLog) => existingLog.id === newLog.id); + + if (existingLogIndex !== -1) { + const updatedLogs = [...pre]; + updatedLogs[existingLogIndex] = { + ...updatedLogs[existingLogIndex], + data: newLog.data, + }; + return updatedLogs; + } else { + return [...pre, newLog]; + } + }); + }; + + const fetchLogs = useCallback(() => { + const ctrl = new AbortController(); + + fetchEventSource(`/v1/apps/${currentApp.appid}/logs/all?containerName=init`, { + method: "GET", + headers: { + Authorization: "Bearer " + localStorage.getItem("token"), + }, + signal: ctrl.signal, + async onopen(response) { + if (response.ok && response.headers.get("content-type") === EventStreamContentType) { + setIsLoading(false); + } else { + throw new Error(`Unexpected response: ${response.status} ${response.statusText}`); + } + }, + + onmessage(msg) { + if (msg.event === "error") { + throw new Error(msg.data); + } + + if (msg.event === "log") { + addOrUpdateLog(msg); + } + }, + + onclose() { + throw new Error("connect closed unexpectedly, retrying..."); + }, + + onerror(err) { + // auto retry fetch + }, + }); + return ctrl; + }, [currentApp.appid]); + + useEffect(() => { + setRowCount(0); + setLogs([]); + setIsLoading(true); + setPaused(false); + const ctrl = fetchLogs(); + + return () => { + ctrl?.abort(); + }; + }, [fetchLogs]); + + useEffect(() => { + const sortedLogs = logs.sort((a, b) => parseInt(a.id) - parseInt(b.id)); + const concatenatedLogs = sortedLogs.map((log) => log.data).join(""); + setRenderLogs(concatenatedLogs); + const totalRows = concatenatedLogs.split("\n").length; + setRowCount(totalRows); + }, [logs]); + + return ( + <> + {isLoading ? ( +
+ +
+ ) : ( + <> +
+ { + if (e.scrollOffsetToBottom <= 0) { + setPaused(false); + return; + } + if (!e.scrollUpdateWasRequested) { + setPaused(true); + return; + } + setPaused(false); + }} + /> +
+
+ + {currentApp.phase}... +
+ + )} + + ); +} diff --git a/web/src/pages/app/setting/SysSetting/AppInfoList/index.tsx b/web/src/pages/app/setting/SysSetting/AppInfoList/index.tsx index e7d5246160..3bde8a6aff 100644 --- a/web/src/pages/app/setting/SysSetting/AppInfoList/index.tsx +++ b/web/src/pages/app/setting/SysSetting/AppInfoList/index.tsx @@ -13,7 +13,13 @@ import InfoDetail from "./InfoDetail"; import useGlobalStore from "@/pages/globalStore"; import DeleteAppModal from "@/pages/home/mods/DeleteAppModal"; import StatusBadge from "@/pages/home/mods/StatusBadge"; -const AppEnvList = () => { + +interface AppEnvListProps { + onClose?: () => void; +} + +const AppEnvList: React.FC = (props = {}) => { + const { onClose } = props; const { t } = useTranslation(); const navigate = useNavigate(); @@ -67,6 +73,10 @@ const AppEnvList = () => { ? APP_STATUS.Running : APP_STATUS.Restarting, ); + // when start close modal window + if (currentApp?.phase === APP_PHASE_STATUS.Stopped && onClose) { + onClose(); + } }} > {currentApp?.phase === APP_PHASE_STATUS.Stopped ? (