Skip to content

Commit

Permalink
feat: applaunchpad log previous (#5047)
Browse files Browse the repository at this point in the history
* feat: applaunchpad support previous and sincetime

* feat: since i18n

* fix: stream prefer flush header

* fix: previous cannot prefer flush header

* feat: no_logs_for_now
  • Loading branch information
zijiren233 committed Sep 6, 2024
1 parent 60c4065 commit bab49b7
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 31 deletions.
10 changes: 8 additions & 2 deletions frontend/providers/applaunchpad/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -259,5 +259,11 @@
"app_store": "App Store",
"sealaf": "sealaf",
"total_price_tip": "The estimated cost does not include port fees and traffic fees, and is subject to actual usage.",
"nodeports": "NodePorts"
}
"nodeports": "NodePorts",
"streaming_logs": "Streaming logs",
"within_5_minutes": "Within 5 minutes",
"within_1_hour": "Within 1 hour",
"within_1_day": "Within 1 day",
"terminated_logs": "Terminated logs",
"no_logs_for_now": "No logs for now"
}
Original file line number Diff line number Diff line change
Expand Up @@ -259,5 +259,11 @@
"sealaf": "云开发",
"app_store": "应用商店",
"total_price_tip": "预估费用不包括端口费用和流量费用,以实际使用为准",
"nodeports": "外网端口"
"nodeports": "外网端口",
"streaming_logs": "实时日志",
"within_5_minutes": "五分钟内",
"within_1_hour": "一小时内",
"within_1_day": "一天内",
"terminated_logs": "中断前",
"no_logs_for_now": "暂无日志"
}
2 changes: 2 additions & 0 deletions frontend/providers/applaunchpad/src/api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export const getPodLogs = (data: {
podName: string;
stream: boolean;
logSize?: number;
sinceTime?: number;
previous?: boolean;
}) => POST<string>(`/api/getPodLogs`, data);

export const getPodEvents = (podName: string) =>
Expand Down
33 changes: 29 additions & 4 deletions frontend/providers/applaunchpad/src/pages/api/getPodLogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
appName,
podName,
stream = false,
logSize
logSize,
previous,
sinceTime
} = req.body as {
appName: string;
podName: string;
stream: boolean;
logSize?: number;
previous?: boolean;
sinceTime?: number;
};

if (!podName) {
Expand All @@ -51,6 +55,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
});

if (!stream) {
const sinceSeconds =
sinceTime && !!!previous ? Math.floor((Date.now() - sinceTime) / 1000) : undefined;
// get pods
const { body: data } = await k8sCore.readNamespacedPodLog(
podName,
Expand All @@ -60,8 +66,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
undefined,
undefined,
undefined,
undefined,
undefined,
previous,
sinceSeconds,
logSize
);
return jsonRes(res, {
Expand All @@ -77,6 +83,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
res.setHeader('Cache-Control', 'no-cache, no-transform');
logStream.pipe(res);

const reqData = {
follow: true,
pretty: false,
timestamps: false,
tailLines: 1000,
previous: !!previous
} as any;
if (!reqData.previous && sinceTime) {
reqData.sinceTime = timestampToRFC3339(sinceTime);
}

if (!reqData.previous) {
res.flushHeaders();
}

streamResponse = await logs.log(
namespace,
podName,
Expand All @@ -86,7 +107,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
console.log('err', err);
destroyStream();
},
{ follow: true, pretty: false, timestamps: false, tailLines: 1000 }
reqData
);
} catch (err: any) {
jsonRes(res, {
Expand All @@ -95,3 +116,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
});
}
}

function timestampToRFC3339(timestamp: number) {
return new Date(timestamp).toISOString();
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { getPodLogs } from '@/api/app';
import {
Modal,
Expand All @@ -16,12 +16,48 @@ import { useLoading } from '@/hooks/useLoading';
import { downLoadBold } from '@/utils/tools';
import styles from '../index.module.scss';
import { SealosMenu } from '@sealos/ui';

import Empty from './empty';
import { ChevronDownIcon } from '@chakra-ui/icons';
import { streamFetch } from '@/services/streamFetch';
import { default as AnsiUp } from 'ansi_up';
import { useTranslation } from 'next-i18next';

interface sinceItem {
key: 'streaming_logs' | 'within_5_minutes' | 'within_1_hour' | 'within_1_day' | 'terminated_logs';
since: number;
previous: boolean;
}

const newSinceItems = (baseTimestamp: number): sinceItem[] => {
return [
{
key: 'streaming_logs',
since: 0,
previous: false
},
{
key: 'within_5_minutes',
since: baseTimestamp - 5 * 60 * 1000,
previous: false
},
{
key: 'within_1_hour',
since: baseTimestamp - 60 * 60 * 1000,
previous: false
},
{
key: 'within_1_day',
since: baseTimestamp - 24 * 60 * 60 * 1000,
previous: false
},
{
key: 'terminated_logs',
since: 0,
previous: true
}
];
};

const LogsModal = ({
appName,
podName,
Expand All @@ -44,6 +80,20 @@ const LogsModal = ({
const [isLoading, setIsLoading] = useState(true);
const LogBox = useRef<HTMLDivElement>(null);
const ansi_up = useRef(new AnsiUp());
const [sinceKey, setSinceKey] = useState('streaming_logs');
const [sinceTime, setSinceTime] = useState(0);
const [previous, setPrevious] = useState(false);

const switchSince = useCallback(
(item: sinceItem) => {
setSinceKey(item.key);
setPrevious(item.previous);
setSinceTime(item.since);
},
[setSinceKey, setPrevious, setSinceTime]
);

const sinceItems = useMemo(() => newSinceItems(Date.now()), []);

const watchLogs = useCallback(() => {
// podName is empty. pod may has been deleted
Expand All @@ -56,12 +106,14 @@ const LogsModal = ({
data: {
appName,
podName,
stream: true
stream: true,
sinceTime,
previous
},
abortSignal: controller,
firstResponse() {
setIsLoading(false);
setLogs('');
setIsLoading(false);
setTimeout(() => {
if (!LogBox.current) return;

Expand Down Expand Up @@ -91,7 +143,7 @@ const LogsModal = ({
}
});
return controller;
}, [appName, closeFn, podName]);
}, [appName, closeFn, podName, sinceTime, previous]);

useEffect(() => {
const controller = watchLogs();
Expand All @@ -101,13 +153,19 @@ const LogsModal = ({
}, [watchLogs]);

const exportLogs = useCallback(async () => {
const allLogs = await getPodLogs({
appName,
podName,
stream: false
});
downLoadBold(allLogs, 'text/plain', 'log.txt');
}, [appName, podName]);
try {
const allLogs = await getPodLogs({
appName,
podName,
stream: false,
sinceTime,
previous
});
downLoadBold(allLogs, 'text/plain', 'log.txt');
} catch (e) {
console.log('download log error:', e);
}
}, [appName, podName, sinceTime, previous]);

return (
<Modal isOpen={true} onClose={closeFn} isCentered={true} lockFocusAcrossFrames={false}>
Expand Down Expand Up @@ -143,23 +201,53 @@ const LogsModal = ({
}))}
/>
</Box>
<Box px={3} zIndex={10000}>
<SealosMenu
width={200}
Button={
<MenuButton
minW={'200px'}
h={'32px'}
textAlign={'start'}
bg={'grayModern.100'}
border={theme.borders.base}
borderRadius={'md'}
>
<Flex px={4} alignItems={'center'}>
<Box flex={1}>{t(sinceKey)}</Box>
<ChevronDownIcon ml={2} />
</Flex>
</MenuButton>
}
menuList={sinceItems.map((item) => ({
isActive: item.key === sinceKey,
child: <Box>{t(item.key)}</Box>,
onClick: () => switchSince(item)
}))}
/>
</Box>
<Button size={'sm'} onClick={exportLogs}>
{t('Export')}
</Button>
</Flex>
<ModalCloseButton />
</ModalHeader>
<Box flex={'1 0 0'} h={0} position={'relative'} px={'36px'} mt={'24px'}>
<Box
ref={LogBox}
h={'100%'}
whiteSpace={'pre'}
pb={2}
overflow={'auto'}
fontWeight={400}
fontFamily={'SFMono-Regular,Menlo,Monaco,Consolas,monospace'}
dangerouslySetInnerHTML={{ __html: logs }}
></Box>
{logs === '' ? (
<Empty />
) : (
<Box
ref={LogBox}
h={'100%'}
whiteSpace={'pre'}
pb={2}
overflow={'auto'}
fontWeight={400}
fontFamily={'SFMono-Regular,Menlo,Monaco,Consolas,monospace'}
dangerouslySetInnerHTML={{ __html: logs }}
></Box>
)}

<Loading loading={isLoading} fixed={false} />
</Box>
</ModalContent>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.empty {
width: 100%;
height: 100%;
background-color: #fff;
user-select: none;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import { Box } from '@chakra-ui/react';
import styles from './empty.module.scss';
import MyIcon from '@/components/Icon';
import { useTranslation } from 'next-i18next';

const Empty = () => {
const { t } = useTranslation();
return (
<Box
className={styles.empty}
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="center"
>
<MyIcon name={'noEvents'} color={'transparent'} width={'80px'} height={'80px'} />
<Box py={8}>{t('no_logs_for_now')}</Box>
</Box>
);
};

export default Empty;
8 changes: 6 additions & 2 deletions frontend/providers/applaunchpad/src/services/streamFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ interface StreamFetchProps {
url: string;
data: any;
onMessage: (text: string) => void;
firstResponse?: (text: string) => void;
firstResponse?: () => void;
abortSignal: AbortController;
}
export const streamFetch = ({
Expand All @@ -25,6 +25,11 @@ export const streamFetch = ({
body: JSON.stringify(data),
signal: abortSignal.signal
});
if (res.status === 200) {
firstResponse && firstResponse();
} else {
reject('请求异常');
}
const reader = res.body?.getReader();
if (!reader) return;
abortSignal.signal.addEventListener('abort', () => reader.cancel(), { once: true });
Expand All @@ -49,7 +54,6 @@ export const streamFetch = ({
}
const text = decoder.decode(value).replace(/<br\/>/g, '\n');
if (res.status === 200) {
responseText === '' && firstResponse && firstResponse(text);
onMessage(text);
responseText += text;
}
Expand Down

0 comments on commit bab49b7

Please sign in to comment.