Skip to content

Commit ac38700

Browse files
feat: 添加实时获取日志 (#2344)
1 parent 279e8ce commit ac38700

3 files changed

Lines changed: 209 additions & 15 deletions

File tree

webfe/package_vue/src/common/tools.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,3 +635,97 @@ export function normalizeOrgSelectionData(data, options = {}) {
635635
name: item.name,
636636
}));
637637
}
638+
639+
/**
640+
* 初始化一个 EventSource,以连接到服务器发送事件(SSE)流。
641+
* @param {string} url - 服务器端 SSE 流的 URL。
642+
* @param {Object} options - 事件流的配置选项。
643+
* @param {boolean} [options.withCredentials=false] - 是否在请求中包含凭据(如 Cookies、授权报头)。
644+
* @param {function} [options.onOpen=() => {}] - 连接成功建立时的回调函数。
645+
* @param {function} [options.onMessage=() => {}] - 处理从服务器接收消息的回调函数。
646+
* @param {function} [options.onError=() => {}] - 处理流期间发生错误的回调函数。
647+
* @param {function} [options.onEOF=() => {}] - 处理'EOF'事件的回调函数,指示流的结束。
648+
* @param {number} [options.reconnectInterval=5000] - 在断开连接或出错后尝试重新连接之前等待的毫秒数。
649+
* @param {number} [options.maxRetries=Infinity] - 最大重试次数,默认无限重试。
650+
* @returns {{ close: function, reconnect: function }} 返回一个包含关闭方法、重连方法的对象。
651+
*/
652+
export function createSSE(url, options = {}) {
653+
const {
654+
withCredentials = false,
655+
onOpen = () => {},
656+
onMessage = () => {},
657+
onError = () => {},
658+
onEOF = () => {},
659+
reconnectInterval = 5000,
660+
maxRetries = Infinity,
661+
} = options;
662+
663+
let eventSource;
664+
let retryCount = 0;
665+
let isManuallyClosed = false;
666+
let reconnectTimer;
667+
668+
const connect = () => {
669+
if (isManuallyClosed) return;
670+
671+
// 清除之前的连接(如果有)
672+
if (eventSource) {
673+
eventSource.close();
674+
}
675+
676+
eventSource = new EventSource(url, { withCredentials });
677+
678+
// 连接成功建立
679+
eventSource.onopen = () => {
680+
retryCount = 0; // 重置重试计数器
681+
onOpen();
682+
};
683+
684+
// 处理接收到的消息
685+
eventSource.onmessage = (event) => {
686+
onMessage(event);
687+
};
688+
689+
// 处理错误事件
690+
eventSource.onerror = (error) => {
691+
// 仅在连接关闭时处理错误(EventSource 在重连时也会触发 error 事件)
692+
if (eventSource.readyState === EventSource.CLOSED) {
693+
onError(error, retryCount);
694+
if (retryCount < maxRetries) {
695+
retryCount += 1;
696+
reconnectTimer = setTimeout(connect, reconnectInterval);
697+
}
698+
}
699+
};
700+
701+
// 处理流结束事件
702+
eventSource.addEventListener('EOF', () => {
703+
onEOF();
704+
close();
705+
});
706+
};
707+
708+
// 开始连接
709+
connect();
710+
711+
// 关闭连接的方法
712+
const close = () => {
713+
isManuallyClosed = true;
714+
clearTimeout(reconnectTimer);
715+
if (eventSource) {
716+
eventSource.close();
717+
}
718+
};
719+
720+
// 主动重连的方法
721+
const reconnect = () => {
722+
isManuallyClosed = false;
723+
connect();
724+
};
725+
726+
return {
727+
close,
728+
reconnect,
729+
};
730+
};
731+

webfe/package_vue/src/components/process-log-dialog/log.vue

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,23 @@
4949
:name="$t('最近 400 条')"
5050
/>
5151
</bk-select>
52+
<template v-if="isRealTimeLog && isCloudNative">
53+
<i
54+
v-bk-tooltips="isShowTime ? $t('隐藏时间') : $t('显示时间')"
55+
class="paasng-icon paasng-clock ml20"
56+
@click="handleChangeShowTime"
57+
></i>
58+
<!-- 云原生-实时日志: 建立sse链接,获取日志 -->
59+
<i
60+
v-bk-tooltips="isOpen ? $t('停止刷新') : $t('实时刷新')"
61+
class="paasng-icon ml20"
62+
:class="isOpen ? 'paasng-pause' : 'paasng-play2'"
63+
@click="getStreamRealTimeLog"
64+
></i>
65+
</template>
5266
<!-- 刷新 -->
5367
<i
68+
v-bk-tooltips="$t('刷新')"
5469
class="paasng-icon paasng-refresh ml20"
5570
@click="refresh"
5671
></i>
@@ -74,7 +89,6 @@
7489
v-if="logs.length"
7590
class="bk-log"
7691
ref="bkLog"
77-
:key="logIndex"
7892
></bk-log>
7993
<div
8094
v-else
@@ -126,8 +140,8 @@ export default {
126140
type: Array | String,
127141
default: () => [],
128142
},
129-
// 直接展示当前行日志
130-
isDirect: {
143+
// 是否为云原生应用
144+
isCloudNative: {
131145
type: Boolean,
132146
default: false,
133147
},
@@ -137,11 +151,12 @@ export default {
137151
internalVisible: this.value,
138152
selectValue: this.defaultCondition,
139153
logType: 'realtime',
140-
logIndex: 0,
141154
logTypeOption: [
142155
{ id: 'realtime', text: this.$t('实时日志') },
143156
{ id: 'restart', text: this.$t('最近一次重启日志') },
144157
],
158+
isOpen: false,
159+
isShowTime: false,
145160
};
146161
},
147162
computed: {
@@ -171,9 +186,9 @@ export default {
171186
},
172187
logs: {
173188
handler(newLogs) {
174-
this.logIndex += 1;
175189
this.$nextTick(() => {
176-
this.$refs.bkLog.addLogData(newLogs);
190+
this.$refs.bkLog?.changeExecute();
191+
this.$refs.bkLog?.addLogData(newLogs);
177192
});
178193
},
179194
deep: true,
@@ -190,7 +205,8 @@ export default {
190205
},
191206
// 切换日志类型
192207
changeLogType(type) {
193-
if (this.loading) return;
208+
if (this.loading || this.logType === type) return;
209+
this.rest();
194210
this.logType = type;
195211
this.selectValue = this.isDisabledRestartLog ? 'restart' : this.defaultCondition;
196212
this.$emit('change', {
@@ -204,11 +220,37 @@ export default {
204220
type: this.logType,
205221
value: this.selectValue,
206222
});
223+
this.closeStreamLog();
207224
},
208225
// 下载重启日志
209226
async download() {
210227
this.$emit('download', this.logType);
211228
},
229+
rest() {
230+
// 清空当前日志
231+
this.$refs.bkLog?.changeExecute();
232+
this.closeStreamLog();
233+
},
234+
// 关闭 sse 连接
235+
closeStreamLog() {
236+
this.$emit('get-stream-log', 'close');
237+
this.isOpen = false;
238+
},
239+
// 获取实时日志(sse)
240+
getStreamRealTimeLog() {
241+
const type = this.isOpen ? 'close' : 'open';
242+
this.$emit('get-stream-log', type);
243+
this.isOpen = !this.isOpen;
244+
},
245+
// 流式获取的 push 进组件,不进行全量替换
246+
addLogData(log) {
247+
this.$refs.bkLog?.addLogData(log);
248+
},
249+
// 日志时间
250+
handleChangeShowTime() {
251+
this.isShowTime = !this.isShowTime;
252+
this.$refs.bkLog?.changeShowTime(this.isShowTime);
253+
},
212254
},
213255
};
214256
</script>
@@ -299,13 +341,17 @@ export default {
299341
300342
.log-content {
301343
height: calc(100% - 48px);
344+
min-height: 0;
302345
.bk-log {
303346
height: 100%;
304-
transform: translateY(-5px);
347+
margin-top: 0 !important;
305348
/deep/ .scroll-home .scroll-main .scroll-item {
306349
font-family: Consolas, Courier New, monospace;
307350
color: #fff !important;
308351
}
352+
/deep/ .log-loading {
353+
display: none;
354+
}
309355
}
310356
.empty {
311357
padding: 16px;

webfe/package_vue/src/views/dev-center/app/engine/cloud-deploy-manage/comps/deploy-detail.vue

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -500,18 +500,20 @@
500500
<!-- 日志弹窗 -->
501501
<process-log
502502
v-if="logConfig.visiable"
503+
ref="logRef"
503504
v-model="logConfig.visiable"
504505
:title="logConfig.title"
505506
:logs="logConfig.logs"
506507
:loading="logConfig.isLoading"
507508
:selection-list="logSelectionList"
508509
:params="logConfig.params"
509-
:is-direct="true"
510510
:default-condition="'400'"
511+
:is-cloud-native="isCloudNativeApp"
511512
@close="handleClose"
512513
@change="refreshLogs"
513514
@refresh="refreshLogs"
514515
@download="downloadInstanceLog"
516+
@get-stream-log="getStreamLog"
515517
></process-log>
516518

517519
<!-- 功能依赖项展示 -->
@@ -539,7 +541,7 @@ import scaleDialog from './scale-dialog';
539541
import i18n from '@/language/i18n.js';
540542
import { bus } from '@/common/bus';
541543
import eventDetail from './event-detail.vue';
542-
import { downloadTxt } from '@/common/tools';
544+
import { downloadTxt, createSSE } from '@/common/tools';
543545
import processLog from '@/components/process-log-dialog/log.vue';
544546
import { cloneDeep, isEqual } from 'lodash';
545547
import FunctionalDependency from '@blueking/functional-dependency/vue2/index.umd.min.js';
@@ -737,6 +739,8 @@ export default {
737739
parmas: {},
738740
},
739741
showFunctionalDependencyDialog: false,
742+
streamLogEvent: null,
743+
lastStreamLogTime: null,
740744
};
741745
},
742746
computed: {
@@ -799,6 +803,8 @@ export default {
799803
beforeDestroy() {
800804
// 页面销毁 关闭stream
801805
this.closeServerPush();
806+
// 关闭实时日志连接
807+
this.closeStreamLogEvent();
802808
},
803809
804810
methods: {
@@ -1438,13 +1444,11 @@ export default {
14381444
previous,
14391445
lines: this.curTailLines,
14401446
});
1441-
this.logConfig.logs = res.map((log) => {
1442-
return { message: log };
1443-
});
1447+
this.logConfig.logs = res;
14441448
} catch (e) {
14451449
this.logConfig.logs = [];
14461450
if (e.status === 404) {
1447-
this.this.noLogMessage();
1451+
this.noLogMessage();
14481452
}
14491453
this.catchErrorHandler(e);
14501454
} finally {
@@ -1467,7 +1471,7 @@ export default {
14671471
downloadTxt(logs, params.instanceName);
14681472
} catch (e) {
14691473
if (e.status === 404) {
1470-
this.this.noLogMessage();
1474+
this.noLogMessage();
14711475
}
14721476
this.catchErrorHandler(e);
14731477
}
@@ -1549,6 +1553,7 @@ export default {
15491553
this.logConfig.visiable = false;
15501554
this.curTailLines = '400';
15511555
this.logConfig.logs = [];
1556+
this.closeStreamLogEvent();
15521557
},
15531558
15541559
// 刷新日志
@@ -1613,6 +1618,55 @@ export default {
16131618
gotoMore() {
16141619
window.open(this.GLOBAL.DOC.WEB_CONSOLE, '_blank');
16151620
},
1621+
1622+
// 获取实时日志(sse)
1623+
async getStreamLog(type) {
1624+
if (type === 'close') {
1625+
this.closeStreamLogEvent();
1626+
return;
1627+
}
1628+
1629+
const { appCode, moduleId, env, processType, instanceName } = this.logConfig.params;
1630+
1631+
let url = `${BACKEND_URL}/api/bkapps/applications/${appCode}/modules/${moduleId}/envs/${env}/processes/${processType}/instances/${instanceName}/logs/stream/`;
1632+
const sinceTime = this.lastStreamLogTime || this.logConfig.logs[this.logConfig.logs.length - 1]?.timestamp;
1633+
if (sinceTime) {
1634+
url += `?since_time=${sinceTime}`;
1635+
}
1636+
1637+
// 初始化事件流
1638+
this.streamLogEvent = createSSE(url, {
1639+
withCredentials: true,
1640+
onMessage: (event) => {
1641+
try {
1642+
const data = JSON.parse(event.data);
1643+
// 添加日志
1644+
this.$refs.logRef?.addLogData([data]);
1645+
this.lastStreamLogTime = data.timestamp;
1646+
} catch (e) {
1647+
console.error('消息解析错误:', e);
1648+
}
1649+
},
1650+
onError: () => {
1651+
// 服务异常重新连接
1652+
this.logConfig.logs.push({
1653+
message: '正在尝试重新连接... ',
1654+
});
1655+
},
1656+
onEOF: () => {
1657+
this.closeStreamLogEvent();
1658+
// 流式日志结束,需要重新连接
1659+
this.streamLogEvent.reconnect();
1660+
},
1661+
});
1662+
},
1663+
1664+
// 关闭事件源
1665+
closeStreamLogEvent() {
1666+
if (this.streamLogEvent) {
1667+
this.streamLogEvent.close();
1668+
}
1669+
},
16161670
},
16171671
};
16181672
</script>

0 commit comments

Comments
 (0)