Browse Source

feat: 实验实例日志改为 websocket 连接

pull/101/head
cp3hnu 1 year ago
parent
commit
02a1f2ec8b
7 changed files with 266 additions and 182 deletions
  1. +16
    -8
      react-ui/src/pages/Experiment/Info/index.jsx
  2. +0
    -139
      react-ui/src/pages/Experiment/Info/props.tsx
  3. +0
    -0
      react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less
  4. +131
    -0
      react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx
  5. +2
    -2
      react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx
  6. +87
    -17
      react-ui/src/pages/Experiment/components/LogGroup/index.tsx
  7. +30
    -16
      react-ui/src/pages/Experiment/components/LogList/index.tsx

+ 16
- 8
react-ui/src/pages/Experiment/Info/index.jsx View File

@@ -10,10 +10,10 @@ import G6, { Util } from '@antv/g6';
import { Button } from 'antd';
import { useEffect, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import ExperimentDrawer from '../components/ExperimentDrawer';
import ParamsModal from '../components/ViewParamsModal';
import { experimentStatusInfo } from '../status';
import styles from './index.less';
import ExperimentDrawer from './props';

let graph = null;

@@ -51,6 +51,7 @@ function ExperimentText() {
}
if (evtSourceRef.current) {
evtSourceRef.current.close();
evtSourceRef.current = null;
}
};
}, []);
@@ -123,9 +124,10 @@ function ExperimentText() {

const setupSSE = (name, namespace) => {
const { origin } = location;
const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`);
const evtSource = new EventSource(
`${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=metadata.namespace%3D${namespace}%2Cmetadata.name%3D${name}`,
{ withCredentials: true },
`${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`,
{ withCredentials: false },
);
evtSource.onmessage = (event) => {
const data = event?.data;
@@ -162,6 +164,7 @@ function ExperimentText() {
}
if (phase !== ExperimentStatus.Pending && phase !== ExperimentStatus.Running) {
evtSource.close();
evtSourceRef.current = null;
}
} catch (error) {
console.log(error);
@@ -448,6 +451,10 @@ function ExperimentText() {
graph.on('node:mouseleave', (e) => {
graph.setItemState(e.item, 'hover', false);
});
graph.on('canvas:click', (e) => {
setExperimentNodeData(null);
closePropsDrawer();
});
};

return (
@@ -483,18 +490,19 @@ function ExperimentText() {
</Button>
</div>
<div className={styles['pipeline-container__graph']} ref={graphRef}></div>
{experimentNodeData ? (
{experimentIns && experimentNodeData ? (
<ExperimentDrawer
key={experimentNodeData.id}
open={propsDrawerOpen}
onClose={closePropsDrawer}
instanceId={experimentIns?.id}
instanceName={experimentIns?.argo_ins_name}
instanceNamespace={experimentIns?.argo_ins_ns}
instanceId={experimentIns.id}
instanceName={experimentIns.argo_ins_name}
instanceNamespace={experimentIns.argo_ins_ns}
instanceNodeData={experimentNodeData}
workflowId={experimentNodeData.workflowId}
instanceNodeStatus={experimentNodeData.experimentStatus}
instanceNodeStartTime={experimentNodeData.experimentStartTime}
instanceNodeEndTime={experimentIns.experimentEndTime}
instanceNodeEndTime={experimentNodeData.experimentEndTime}
></ExperimentDrawer>
) : null}
<ParamsModal


+ 0
- 139
react-ui/src/pages/Experiment/Info/props.tsx View File

@@ -1,139 +0,0 @@
import { ExperimentStatus } from '@/enums';
import { PipelineNodeModelSerialize } from '@/types';
import { elapsedTime, formatDate } from '@/utils/date';
import { DatabaseOutlined, ProfileOutlined } from '@ant-design/icons';
import { Drawer, Tabs } from 'antd';
import { forwardRef, useImperativeHandle, useMemo } from 'react';
import ExperimentParameter from '../components/ExperimentParameter';
import ExperimentResult from '../components/ExperimentResult';
import LogList from '../components/LogList';
import { experimentStatusInfo } from '../status';
import styles from './props.less';

type ExperimentDrawerProps = {
open: boolean;
onClose: () => void;
instanceId?: number; // 实验实例 id
instanceName?: string; // 实验实例 name
instanceNamespace?: string; // 实验实例 namespace
instanceNodeData: PipelineNodeModelSerialize; // 节点数据,在定时刷新实验实例状态中不会变化
workflowId?: string; // 实验实例工作流 id
instanceNodeStatus?: ExperimentStatus; // 实例节点状态
instanceNodeStartTime?: string; // 开始时间
instanceNodeEndTime?: string; // 在定时刷新实验实例状态中,会经常变化
};

const ExperimentDrawer = forwardRef(
(
{
open,
onClose,
instanceId,
instanceName,
instanceNamespace,
instanceNodeData,
workflowId,
instanceNodeStatus,
instanceNodeStartTime,
instanceNodeEndTime,
}: ExperimentDrawerProps,
ref,
) => {
useImperativeHandle(ref, () => ({}));

// 如果性能有问题,可以进一步拆解
const items = useMemo(
() => [
{
key: '1',
label: '日志详情',
children: (
<LogList
instanceName={instanceName}
instanceNamespace={instanceNamespace}
pipelineNodeId={instanceNodeData.id}
workflowId={workflowId}
instanceNodeStartTime={instanceNodeStartTime}
instanceNodeStatus={instanceNodeStatus}
></LogList>
),
icon: <ProfileOutlined />,
},
{
key: '2',
label: '配置参数',
icon: <DatabaseOutlined />,
children: <ExperimentParameter nodeData={instanceNodeData} />,
},
{
key: '3',
label: '输出结果',
children: (
<ExperimentResult
experimentInsId={instanceId}
pipelineNodeId={instanceNodeData.id}
></ExperimentResult>
),
icon: <ProfileOutlined />,
},
],
[
instanceNodeData,
instanceId,
instanceName,
instanceNamespace,
instanceNodeStatus,
workflowId,
instanceNodeStartTime,
],
);

return (
<Drawer
title="任务执行详情"
placement="right"
getContainer={false}
closeIcon={false}
onClose={onClose}
open={open}
width={520}
className={styles['experiment-drawer']}
destroyOnClose={true}
>
<div style={{ paddingTop: '15px' }}>
<div className={styles['experiment-drawer__info']}>
任务名称:{instanceNodeData.label}
</div>
<div className={styles['experiment-drawer__info']}>
执行状态:
{instanceNodeStatus ? (
<>
<div
className={styles['experiment-drawer__status-dot']}
style={{
backgroundColor: experimentStatusInfo[instanceNodeStatus]?.color,
}}
></div>
<span style={{ color: experimentStatusInfo[instanceNodeStatus]?.color }}>
{experimentStatusInfo[instanceNodeStatus]?.label}
</span>
</>
) : (
'--'
)}
</div>
<div className={styles['experiment-drawer__info']}>
启动时间:{formatDate(instanceNodeStartTime)}
</div>
<div className={styles['experiment-drawer__info']}>
耗时:
{elapsedTime(instanceNodeStartTime, instanceNodeEndTime)}
</div>
</div>
<Tabs defaultActiveKey="1" items={items} className={styles['experiment-drawer__tabs']} />
</Drawer>
);
},
);

export default ExperimentDrawer;

react-ui/src/pages/Experiment/Info/props.less → react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less View File


+ 131
- 0
react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx View File

@@ -0,0 +1,131 @@
import { ExperimentStatus } from '@/enums';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import { PipelineNodeModelSerialize } from '@/types';
import { elapsedTime, formatDate } from '@/utils/date';
import { DatabaseOutlined, ProfileOutlined } from '@ant-design/icons';
import { Drawer, Tabs } from 'antd';
import { useMemo } from 'react';
import ExperimentParameter from '../ExperimentParameter';
import ExperimentResult from '../ExperimentResult';
import LogList from '../LogList';
import styles from './index.less';

type ExperimentDrawerProps = {
open: boolean;
onClose: () => void;
instanceId: number; // 实验实例 id
instanceName: string; // 实验实例 name
instanceNamespace: string; // 实验实例 namespace
instanceNodeData: PipelineNodeModelSerialize; // 节点数据,在定时刷新实验实例状态中不会变化
workflowId?: string; // 实验实例工作流 id
instanceNodeStatus?: ExperimentStatus; // 实例节点状态
instanceNodeStartTime?: string; // 开始时间
instanceNodeEndTime?: string; // 在定时刷新实验实例状态中,会经常变化
};

const ExperimentDrawer = ({
open,
onClose,
instanceId,
instanceName,
instanceNamespace,
instanceNodeData,
workflowId,
instanceNodeStatus,
instanceNodeStartTime,
instanceNodeEndTime,
}: ExperimentDrawerProps) => {
// 如果性能有问题,可以进一步拆解
const items = useMemo(
() => [
{
key: '1',
label: '日志详情',
children: (
<LogList
instanceName={instanceName}
instanceNamespace={instanceNamespace}
pipelineNodeId={instanceNodeData.id}
workflowId={workflowId}
instanceNodeStartTime={instanceNodeStartTime}
instanceNodeStatus={instanceNodeStatus}
></LogList>
),
icon: <ProfileOutlined />,
},
{
key: '2',
label: '配置参数',
icon: <DatabaseOutlined />,
children: <ExperimentParameter nodeData={instanceNodeData} />,
},
{
key: '3',
label: '输出结果',
children: (
<ExperimentResult
experimentInsId={instanceId}
pipelineNodeId={instanceNodeData.id}
></ExperimentResult>
),
icon: <ProfileOutlined />,
},
],
[
instanceNodeData,
instanceId,
instanceName,
instanceNamespace,
instanceNodeStatus,
workflowId,
instanceNodeStartTime,
],
);

return (
<Drawer
title="任务执行详情"
placement="right"
getContainer={false}
closeIcon={false}
onClose={onClose}
open={open}
width={520}
className={styles['experiment-drawer']}
destroyOnClose={true}
mask={false}
>
<div style={{ paddingTop: '15px' }}>
<div className={styles['experiment-drawer__info']}>任务名称:{instanceNodeData.label}</div>
<div className={styles['experiment-drawer__info']}>
执行状态:
{instanceNodeStatus ? (
<>
<div
className={styles['experiment-drawer__status-dot']}
style={{
backgroundColor: experimentStatusInfo[instanceNodeStatus]?.color,
}}
></div>
<span style={{ color: experimentStatusInfo[instanceNodeStatus]?.color }}>
{experimentStatusInfo[instanceNodeStatus]?.label}
</span>
</>
) : (
'--'
)}
</div>
<div className={styles['experiment-drawer__info']}>
启动时间:{formatDate(instanceNodeStartTime)}
</div>
<div className={styles['experiment-drawer__info']}>
耗时:
{elapsedTime(instanceNodeStartTime, instanceNodeEndTime)}
</div>
</div>
<Tabs defaultActiveKey="1" items={items} className={styles['experiment-drawer__tabs']} />
</Drawer>
);
};

export default ExperimentDrawer;

+ 2
- 2
react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx View File

@@ -8,8 +8,8 @@ import ExportModelModal from '../ExportModelModal';
import styles from './index.less';

type ExperimentResultProps = {
experimentInsId?: number; // 实验实例 id
pipelineNodeId?: string; // 流水线节点 id
experimentInsId: number; // 实验实例 id
pipelineNodeId: string; // 流水线节点 id
};

type ExperimentResultData = {


+ 87
- 17
react-ui/src/pages/Experiment/components/LogGroup/index.tsx View File

@@ -10,6 +10,7 @@ import { getExperimentPodsLog } from '@/services/experiment/index.js';
import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import classNames from 'classnames';
import dayjs from 'dayjs';
import { useEffect, useRef, useState } from 'react';
import { ExperimentLog } from '../LogList';
import styles from './index.less';
@@ -21,6 +22,7 @@ export type LogGroupProps = ExperimentLog & {
type Log = {
start_time: string; // 日志开始时间
log_content: string; // 日志内容
pod_name: string; // pod名称
};

// 滚动到底部
@@ -49,44 +51,33 @@ function LogGroup({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false);
const preStatusRef = useRef<ExperimentStatus | undefined>(undefined);
const socketRef = useRef<WebSocket | undefined>(undefined);
const retryRef = useRef(2);

useEffect(() => {
scrollToBottom(false);
let timerId: NodeJS.Timeout | undefined;
if (status === ExperimentStatus.Running) {
timerId = setInterval(() => {
requestExperimentPodsLog();
}, 5 * 1000);
setupSockect();
} else if (preStatusRef.current === ExperimentStatus.Running) {
requestExperimentPodsLog();
setTimeout(() => {
requestExperimentPodsLog();
}, 5 * 1000);
setCompleted(true);
}
preStatusRef.current = status;
return () => {
if (timerId) {
clearInterval(timerId);
timerId = undefined;
}
};
}, [status]);

// 鼠标拖到中不滚动到底部
useEffect(() => {
const mouseDown = () => {
setIsMouseDown(true);
};

const mouseUp = () => {
setIsMouseDown(false);
};

document.addEventListener('mousedown', mouseDown);
document.addEventListener('mouseup', mouseUp);

return () => {
document.removeEventListener('mousedown', mouseDown);
document.removeEventListener('mouseup', mouseUp);
closeSocket();
};
}, []);

@@ -140,6 +131,85 @@ function LogGroup({
requestExperimentPodsLog();
};

// 建立 socket 连接
const setupSockect = () => {
let { host } = location;
console.log('setupSockect');

if (process.env.NODE_ENV === 'development') {
host = '172.20.32.181:31213';
}
const socket = new WebSocket(
`ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`,
);

socket.addEventListener('open', () => {
console.log('WebSocket is open now.');
});

socket.addEventListener('close', (event) => {
console.log('WebSocket is closed:', event);
if (event.code !== 1000 && retryRef.current > 0) {
retryRef.current -= 1;
setTimeout(() => {
setupSockect();
}, 2 * 1000);
}
});

socket.addEventListener('error', (event) => {
console.error('WebSocket error observed:', event);
});

socket.addEventListener('message', (event) => {
if (!event.data) {
return;
}
try {
const data = JSON.parse(event.data);
const streams = data.streams;
if (!streams || !Array.isArray(streams)) {
return;
}
let startTime = start_time;
const logContent = streams.reduce((result, item) => {
const values = item.values;
return (
result +
values.reduce((prev: string, cur: [string, string]) => {
const [time, value] = cur;
startTime = time;
const str = `[${dayjs(Number(time) / 1.0e6).format('YYYY-MM-DD HH:mm:ss')}] ${value}`;
return prev + str;
}, '')
);
}, '');
const logDetail: Log = {
start_time: startTime!,
log_content: logContent,
pod_name: pod_name,
};
setLogList((oldList) => oldList.concat(logDetail));
if (!isMouseDownRef.current && logContent) {
setTimeout(() => {
scrollToBottom();
}, 100);
}
} catch (error) {
console.log(error);
}
});

socketRef.current = socket;
};

const closeSocket = () => {
if (socketRef.current) {
socketRef.current.close(1000, 'completed');
socketRef.current = undefined;
}
};

const showLog = (log_type === 'resource' && !collapse) || log_type === 'normal';
const logText = log_content + logList.map((v) => v.log_content).join('');
const showMoreBtn =


+ 30
- 16
react-ui/src/pages/Experiment/components/LogList/index.tsx View File

@@ -2,7 +2,7 @@ import { ExperimentStatus } from '@/enums';
import { getQueryByExperimentLog } from '@/services/experiment/index.js';
import { to } from '@/utils/promise';
import dayjs from 'dayjs';
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import LogGroup from '../LogGroup';
import styles from './index.less';

@@ -14,9 +14,9 @@ export type ExperimentLog = {
};

type LogListProps = {
instanceName?: string; // 实验实例 name
instanceNamespace?: string; // 实验实例 namespace
pipelineNodeId?: string; // 流水线节点 id
instanceName: string; // 实验实例 name
instanceNamespace: string; // 实验实例 namespace
pipelineNodeId: string; // 流水线节点 id
workflowId?: string; // 实验实例工作流 id
instanceNodeStartTime?: string; // 实验实例节点开始运行时间
instanceNodeStatus?: ExperimentStatus;
@@ -31,23 +31,30 @@ function LogList({
instanceNodeStatus,
}: LogListProps) {
const [logList, setLogList] = useState<ExperimentLog[]>([]);
const preStatusRef = useRef<ExperimentStatus | undefined>(undefined);
const retryRef = useRef(3);

useEffect(() => {
if (workflowId) {
const start_time = dayjs(instanceNodeStartTime).valueOf() * 1.0e6;
const params = {
task_id: pipelineNodeId,
component_id: workflowId,
name: instanceName,
namespace: instanceNamespace,
start_time: start_time,
};
getExperimentLog(params, start_time);
if (
instanceNodeStatus &&
instanceNodeStatus !== ExperimentStatus.Pending &&
(!preStatusRef.current || preStatusRef.current === ExperimentStatus.Pending)
) {
getExperimentLog();
}
}, [workflowId, instanceNodeStartTime]);
preStatusRef.current = instanceNodeStatus;
}, [instanceNodeStatus]);

// 获取实验日志
const getExperimentLog = async (params: any, start_time: number) => {
const getExperimentLog = async () => {
const start_time = dayjs(instanceNodeStartTime).valueOf() * 1.0e6;
const params = {
task_id: pipelineNodeId,
component_id: workflowId,
name: instanceName,
namespace: instanceNamespace,
start_time: start_time,
};
const [res] = await to(getQueryByExperimentLog(params));
if (res && res.data) {
const { log_type, pods, log_detail } = res.data;
@@ -68,6 +75,13 @@ function LogList({
}));
setLogList(list);
}
} else {
if (retryRef.current > 0) {
retryRef.current -= 1;
setTimeout(() => {
getExperimentLog();
}, 2 * 1000);
}
}
};



Loading…
Cancel
Save