Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: db log #5069

Merged
merged 4 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion frontend/providers/dbprovider/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -288,5 +288,11 @@
"upload_dump_file": "Upload Dump File",
"use_docs": "Documentation",
"version": "Version",
"yaml_file": "YAML"
"yaml_file": "YAML",
"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"
}
8 changes: 7 additions & 1 deletion frontend/providers/dbprovider/public/locales/zh/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -288,5 +288,11 @@
"upload_dump_file": "点击上传 Dump 文件",
"use_docs": "使用文档",
"version": "版本",
"yaml_file": "YAML 文件"
"yaml_file": "YAML 文件",
"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/dbprovider/src/api/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export const getPodLogs = (data: {
stream: boolean;
logSize?: number;
dbType: string;
sinceTime?: number;
previous?: boolean;
}) => POST<string>(`/api/pod/getPodLogs`, data);

export const getPodEvents = (podName: string) =>
Expand Down
136 changes: 82 additions & 54 deletions frontend/providers/dbprovider/src/pages/api/pod/getPodLogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,60 @@ import { DBBackupPolicyNameMap, DBTypeEnum } from '@/constants/db';

// get App Metrics By DeployName. compute average value
export default async function handler(req: NextApiRequest, res: NextApiResponse<ApiResp>) {
const { kc, k8sCore, namespace } = await getK8s({
kubeconfig: await authSession(req)
});

const {
dbType,
podName,
stream = false,
logSize,
previous,
sinceTime
} = req.body as {
dbType: `${DBTypeEnum}`;
podName: string;
stream: boolean;
logSize?: number;
previous?: boolean;
sinceTime?: number;
};

if (!podName) {
throw new Error('podName is empty');
}

const containerName = DBBackupPolicyNameMap[dbType];
console.log(containerName, 'containerName');

if (!stream) {
const sinceSeconds =
sinceTime && !!!previous ? Math.floor((Date.now() - sinceTime) / 1000) : undefined;
try {
const { body: data } = await k8sCore.readNamespacedPodLog(
podName,
namespace,
containerName,
undefined,
undefined,
undefined,
undefined,
previous,
sinceSeconds,
logSize
);
return jsonRes(res, {
data
});
} catch (error: any) {
jsonRes(res, {
code: 500,
error
});
}
}

let streamResponse: any;
const logStream = new PassThrough();

Expand All @@ -30,71 +84,45 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
destroyStream();
});

try {
const {
dbType,
podName,
stream = false,
logSize
} = req.body as {
dbType: `${DBTypeEnum}`;
podName: string;
stream: boolean;
logSize?: number;
};

if (!podName) {
throw new Error('podName is empty');
}
const { kc, k8sCore, namespace } = await getK8s({
kubeconfig: await authSession(req)
});
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('X-Accel-Buffering', 'no');
res.setHeader('Cache-Control', 'no-cache, no-transform');

const containerName = DBBackupPolicyNameMap[dbType];
console.log(containerName, 'containerName');
res.flushHeaders();

if (!stream) {
// get pods
const { body: data } = await k8sCore.readNamespacedPodLog(
podName,
namespace,
containerName,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
logSize
);
return jsonRes(res, {
data
});
}
logStream.pipe(res);

const logs = new Log(kc);

res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('X-Accel-Buffering', 'no');
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);
}

const logs = new Log(kc);
try {
streamResponse = await logs.log(
namespace,
podName,
containerName,
logStream,
(err) => {
console.log('pod log err', err);
if (err) {
console.log('err', err);
res.write(err.toString());
}
destroyStream();
},
{ follow: true, pretty: false, timestamps: false, tailLines: 1000 }
reqData
);
} catch (err: any) {
jsonRes(res, {
code: 500,
error: err
});
}
} catch (err: any) {}
}

function timestampToRFC3339(timestamp: number) {
return new Date(timestamp).toISOString();
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,46 @@ import {
import { SealosMenu } from '@sealos/ui';
import { default as AnsiUp } from 'ansi_up';
import { useTranslation } from 'next-i18next';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import styles from '../index.module.scss';
import MyIcon from '@/components/Icon';
import Empty from './empty';

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 = ({
dbName,
Expand All @@ -46,6 +83,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(() => {
// dbType is empty. pod may has been deleted
Expand All @@ -57,10 +108,13 @@ const LogsModal = ({
data: {
podName,
dbType,
stream: true
stream: true,
sinceTime,
previous
},
abortSignal: controller,
firstResponse() {
setLogs('');
setIsLoading(false);
// scroll bottom
setTimeout(() => {
Expand Down Expand Up @@ -95,24 +149,30 @@ const LogsModal = ({
}
});
return controller;
}, [closeFn, dbType, podName]);
}, [closeFn, dbType, podName, sinceTime, previous]);

useEffect(() => {
const controller = watchLogs();
return () => {
controller?.abort();
};
}, []);
}, [watchLogs]);

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

return (
<Modal isOpen={true} onClose={closeFn} isCentered={true} lockFocusAcrossFrames={false}>
Expand All @@ -123,7 +183,7 @@ const LogsModal = ({
<Box fontSize={'xl'} fontWeight={'bold'}>
Pod {t('Logs')}
</Box>
<Box px={3}>
<Box px={3} zIndex={10000}>
<SealosMenu
width={240}
Button={
Expand Down Expand Up @@ -151,6 +211,31 @@ 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 as sinceItem['key'])}</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
height={'32px'}
variant={'outline'}
Expand All @@ -163,16 +248,20 @@ const LogsModal = ({
</ModalHeader>
<ModalCloseButton top={'10px'} right={'10px'} />
<Box flex={'1 0 0'} h={0} position={'relative'} pl={'36px'} pr={'10px'} 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
Loading
Loading