index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. <template>
  2. <div style="width: 98%;margin-left: 1%;">
  3. <el-tabs
  4. v-model="activeName"
  5. type="card"
  6. class="demo-tabs"
  7. @tab-click="handleClick"
  8. >
  9. <el-tab-pane label="模型事件" name="first">
  10. <el-form ref="formRef" :inline="true" :model="queryParams1">
  11. <el-form-item label="运行事件">
  12. <el-select
  13. v-model="queryParams1.event"
  14. placeholder=""
  15. clearable
  16. style="width: 150px"
  17. >
  18. <el-option
  19. v-for="dict in model_run_log_event"
  20. :key="dict.value"
  21. :label="dict.label"
  22. :value="dict.value"
  23. />
  24. </el-select>
  25. </el-form-item>
  26. <el-form-item label="模型名称">
  27. <el-input v-model="queryParams1.mdName" style="width: 150px"></el-input>
  28. </el-form-item>
  29. <el-form-item label="时间">
  30. <el-date-picker v-model="queryParams1.time" type="daterange" range-separator="至"
  31. start-placeholder="开始时间" end-placeholder="结束时间"
  32. value-format="YYYY-MM-DD">
  33. </el-date-picker>
  34. </el-form-item>
  35. <el-form-item>
  36. <el-button type="primary" @click="getList1" icon="Search">搜索</el-button>
  37. </el-form-item>
  38. </el-form>
  39. <div style="display: flex;width: 100%;">
  40. <div style="width: 60%;">
  41. <el-table
  42. stripe
  43. style="height: 68vh;margin-top: 1%;width: 100%;"
  44. :data="tableDataJian"
  45. :cell-style="{ padding: '5px' }"
  46. :row-style="{ fontSize: '1rem', textAlign:'center' }"
  47. border>
  48. <el-table-column type="index" align="center" label="序号" width="60">
  49. <template #default="{ $index }">
  50. <div style="text-align: center;">{{ $index + 1 }}</div>
  51. </template>
  52. </el-table-column>
  53. <el-table-column show-overflow-tooltip header-align="center" align="left" prop="createBy" label="人员名称"/>
  54. <el-table-column show-overflow-tooltip header-align="center" align="left" prop="createTime" label="创建时间"/>
  55. <el-table-column show-overflow-tooltip header-align="center" align="left" prop="mdName" label="模型名称"/>
  56. <el-table-column show-overflow-tooltip header-align="center" align="left" prop="event" label="运行事件"/>
  57. </el-table>
  58. <div style="display: flex;">
  59. <el-pagination
  60. small
  61. background
  62. style="margin-top: 1%;margin-left: auto;display: flex;margin-right: 1%;"
  63. layout="prev, pager, next"
  64. :total="totalJian"
  65. v-model="pageNumJian"
  66. @change="fanJian"
  67. class="mt-4"
  68. />
  69. </div>
  70. </div>
  71. <div style="width: 49%;margin-left: 1%;margin-top: 1%;">
  72. <div>
  73. 过去7天模型吞吐量
  74. </div>
  75. <div style="height: 40vh;width: 100%;background-color: ;margin-top: 1%;" id="right1">
  76. </div>
  77. <div style="margin-top: 3%;">
  78. 过去7天模型错误率
  79. </div>
  80. <div style="height: 30vh;width: 100%;background-color: ;margin-top: 1%;" id="right2">
  81. </div>
  82. </div>
  83. </div>
  84. </el-tab-pane>
  85. <el-tab-pane label="模型服务监控" name="second" style="height: 100vh;">
  86. <el-row>
  87. <!-- 筛选条件 -->
  88. <el-form ref="formRef" :inline="true" :model="queryParams">
  89. <el-form-item label="服务名称">
  90. <el-input v-model="queryParams.name"></el-input>
  91. </el-form-item>
  92. <el-form-item label="请求用户">
  93. <el-input v-model="queryParams.userName"></el-input>
  94. </el-form-item>
  95. <el-form-item label="时间">
  96. <el-date-picker v-model="queryParams.time" type="daterange" unlink-panels range-separator="至"
  97. start-placeholder="开始时间" end-placeholder="结束时间" :shortcuts="shortcuts"
  98. format="yyyy-MM-dd">
  99. </el-date-picker>
  100. </el-form-item>
  101. <el-form-item>
  102. <el-button type="primary" @click="getList" icon="Search">搜索</el-button>
  103. </el-form-item>
  104. </el-form>
  105. </el-row>
  106. <el-row class="table_box">
  107. <el-table
  108. stripe
  109. style="height: 68vh;"
  110. :data="tableData"
  111. :cell-style="{ padding: '5px' }"
  112. :row-style="{ fontSize: '1rem', textAlign:'center' }"
  113. border>
  114. <el-table-column type="index" align="center" label="序号" width="60">
  115. <template #default="{ $index }">
  116. <div style="text-align: center;">{{ $index + 1 }}</div>
  117. </template>
  118. </el-table-column>
  119. <el-table-column show-overflow-tooltip header-align="center" align="left" width="240" prop="modelName"
  120. label="调用模型"/>
  121. <el-table-column show-overflow-tooltip header-align="center" align="left" width="200" prop="serviceName"
  122. label="服务名称"/>
  123. <el-table-column prop="url" show-overflow-tooltip header-align="center" align="left" label="服务地址"/>
  124. <el-table-column show-overflow-tooltip header-align="center" align="left" width="220" prop="senText"
  125. label="请求参数" :tooltip-formatter="({ row }) => {
  126. if( row.senText.length >= 30){
  127. return row.senText.substring(0, 30) + '...';
  128. }
  129. return row.senText;
  130. }">
  131. <template #default="scope">
  132. <span style="cursor: copy;" @click="copyParams(scope.row.senText)">{{ scope.row.senText }}</span>
  133. </template>
  134. </el-table-column>
  135. <el-table-column show-overflow-tooltip align="center" width="180" prop="tm" label="请求时间"></el-table-column>
  136. <el-table-column show-overflow-tooltip align="center" width="120" prop="execTm" label="请求耗时(ms)"/>
  137. <el-table-column show-overflow-tooltip align="center" width="100" prop="userName" label="请求用户"/>
  138. <el-table-column show-overflow-tooltip header-align="center" align="left" width="220" prop="appName"
  139. label="请求应用"/>
  140. <el-table-column show-overflow-tooltip align="center" width="100" prop="statusCode" label="请求状态">
  141. <template #default="scope">
  142. <el-tag v-if="scope.row.statusCode==200">成功</el-tag>
  143. <el-tag v-else type="danger">失败</el-tag>
  144. </template>
  145. </el-table-column>
  146. </el-table>
  147. </el-row>
  148. <pagination
  149. v-show="total>0"
  150. :total="total"
  151. v-model:page="queryParams.pageNum"
  152. v-model:limit="queryParams.pageSize"
  153. style="margin-top: 1%;"
  154. @pagination="getList"
  155. />
  156. <el-dialog v-model="open" title="服务日志详情" width="60%" append-to-body>
  157. </el-dialog>
  158. </el-tab-pane>
  159. </el-tabs>
  160. </div>
  161. </template>
  162. <script setup>
  163. import * as echarts from 'echarts';
  164. import {onMounted, reactive, ref} from "vue";
  165. import {getServiceLogList,getMdLogList,getMdStatusSum,getMdAllList } from "@/api/service/log.js";
  166. import useClipboard from 'vue-clipboard3'
  167. const {toClipboard} = useClipboard()
  168. const {proxy} = getCurrentInstance();
  169. const { model_run_log_event } = proxy.useDict("model_run_log_event");
  170. const tableHeight = ref(window.innerHeight - 400)
  171. const queryParams1 = ref({
  172. time:[]
  173. })
  174. const queryParams = reactive({
  175. pageNum: 1,
  176. pageSize: 20,
  177. orderBy: "",
  178. name: "",
  179. userName: "",
  180. time: [],
  181. sttm: "",
  182. entm: "",
  183. })
  184. const totalJian = ref(0)
  185. const tableDataJian = ref([])
  186. const pageNumJian = ref(1)
  187. const activeName = ref('first')
  188. const tableData = ref([])
  189. const total = ref(0)
  190. const shortcuts = ref([
  191. {
  192. text: "近一周",
  193. value: () => {
  194. const end = new Date();
  195. const start = new Date();
  196. start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
  197. return [start, end];
  198. },
  199. },
  200. {
  201. text: "近一个月",
  202. value: () => {
  203. const end = new Date();
  204. const start = new Date();
  205. start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
  206. return [start, end];
  207. },
  208. },
  209. {
  210. text: "近三个月",
  211. value: () => {
  212. const end = new Date();
  213. const start = new Date();
  214. start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
  215. return [start, end];
  216. },
  217. },
  218. {
  219. text: "近一年",
  220. value: () => {
  221. const end = new Date();
  222. const start = new Date();
  223. start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
  224. return [start, end];
  225. },
  226. },
  227. ])
  228. async function copyParams(message) {
  229. toClipboard(message).then(() => {
  230. proxy.$modal.msgSuccess("复制成功!");
  231. })
  232. }
  233. function fanJian(val){
  234. pageNumJian.value = val
  235. getList1()
  236. }
  237. function getList1(){
  238. queryParams1.value.pageSize = 20
  239. queryParams1.value.pageNum = pageNumJian.value
  240. var par = {
  241. pageNum:pageNumJian.value,
  242. pageSize:20,
  243. event:queryParams1.value.event,
  244. mdName:queryParams1.value.mdName,
  245. params:{
  246. beginTime:queryParams1.value.time==[]?'':queryParams1.value.time[0],
  247. endTime:queryParams1.value.time==[]?'':queryParams1.value.time[1],
  248. }
  249. }
  250. getMdLogList(par).then(r=>{
  251. tableDataJian.value = r.rows;
  252. totalJian.value = r.total;
  253. for(var i = 0;i<tableDataJian.value.length;i++){
  254. for(var i1 = 0;i1<model_run_log_event.value.length;i1++){
  255. if(tableDataJian.value[i].event===model_run_log_event.value[i1].value){
  256. tableDataJian.value[i].event = model_run_log_event.value[i1].label
  257. }
  258. }
  259. }
  260. })
  261. }
  262. function getLast7DaysRange() {
  263. const end = new Date();
  264. const start = new Date();
  265. end.setDate(start.getDate() - 1);
  266. start.setDate(end.getDate() - 7);
  267. function formatDate(date) {
  268. const year = date.getFullYear();
  269. const month = String(date.getMonth() + 1).padStart(2, '0');
  270. const day = String(date.getDate()).padStart(2, '0');
  271. return `${year}-${month}-${day}`;
  272. }
  273. return {
  274. start: formatDate(start),
  275. end: formatDate(end)
  276. };
  277. }
  278. function mergeModelData(data) {
  279. const mergeMap = {};
  280. data.forEach(item => {
  281. const mdName = item.mdName?.trim(); // 处理可能的空格
  282. if (!mdName) return;
  283. if (!mergeMap[mdName]) {
  284. mergeMap[mdName] = {
  285. mdName: mdName,
  286. mdId: item.mdId,
  287. statisNum: 0,
  288. statisNumErr: 0
  289. };
  290. }
  291. const current = mergeMap[mdName];
  292. const statisNumValue = Number(item.statisNum) || 0;
  293. if (item.statusCode === '200') {
  294. // statusCode为200,保留statisNum
  295. current.statisNum = statisNumValue;
  296. } else if (item.statusCode) {
  297. // 其他statusCode,累加到statisNumErr
  298. current.statisNumErr += statisNumValue;
  299. }
  300. // 保留第一个非空的mdId
  301. if (!current.mdId && item.mdId) {
  302. current.mdId = item.mdId;
  303. }
  304. });
  305. return Object.values(mergeMap);
  306. }
  307. async function drawRight2(){
  308. var chartDom = document.getElementById('right2');
  309. var myChart = echarts.init(chartDom);
  310. var option;
  311. const last7Days = getLast7DaysRange();
  312. var par1 = {
  313. params:{
  314. beginTime:last7Days.start,
  315. endTime:last7Days.end,
  316. }
  317. }
  318. var uniqueMdNames
  319. var mergedData
  320. var success = []
  321. var error = []
  322. await getMdStatusSum(par1).then(res=>{
  323. uniqueMdNames = [...new Map(res.data.map(item => [item.mdName, item.mdName])).values()].filter(Boolean);
  324. mergedData = mergeModelData(res.data)
  325. mergedData.forEach(item=>{
  326. success.push(item.statisNum)
  327. error.push(item.statisNumErr)
  328. })
  329. })
  330. option = {
  331. grid: {
  332. top: '15%', // 图表距离容器顶部的距离
  333. right: '5%', // 图表距离容器右侧的距离
  334. bottom: '15%', // 图表距离容器底部的距离
  335. left: '10%', // 图表距离容器左侧的距离
  336. containLabel: true // 确保坐标轴标签在 grid 区域内
  337. },
  338. tooltip: {
  339. trigger: 'axis',
  340. axisPointer: {
  341. type: 'shadow'
  342. }
  343. },
  344. legend: {},
  345. color: ['#67C23A','#F56C6C',],
  346. xAxis: [
  347. {
  348. type: 'category',
  349. data: uniqueMdNames,
  350. axisLabel: {
  351. interval: 0, // 强制显示所有标签
  352. rotate: -25, // 旋转角度,正值表示顺时针旋转,负值表示逆时针旋转
  353. // 可以设置文字样式,如字体大小、颜色等
  354. textStyle: {
  355. fontSize: 9,
  356. color: '#333'
  357. }
  358. }
  359. }
  360. ],
  361. yAxis: [
  362. {
  363. type: 'value'
  364. }
  365. ],
  366. series: [
  367. {
  368. name: '成功',
  369. type: 'bar',
  370. stack: 'Ad',
  371. emphasis: {
  372. focus: 'series'
  373. },
  374. data: success,
  375. },
  376. {
  377. name: '失败',
  378. type: 'bar',
  379. stack: 'Ad',
  380. emphasis: {
  381. focus: 'series'
  382. },
  383. data: error,
  384. label: {
  385. show: true, // 显示标签
  386. position: 'top', // 标签位置在柱子上方
  387. // 自定义标签内容,formatter可以是字符串模板,也可以是回调函数
  388. formatter: function(params) {
  389. // 计算每个柱子占当天的总比例
  390. const total = params.data + option.series[0].data[params.dataIndex];
  391. const percent = ((params.data / total) * 100).toFixed(1);
  392. if(params.data){
  393. return params.data + `\n(${percent}%)`;
  394. }
  395. },
  396. // 标签文本样式
  397. textStyle: {
  398. color: '#000', // 颜色
  399. fontSize: 12
  400. }
  401. }
  402. },
  403. ]
  404. };
  405. option && myChart.setOption(option);
  406. }
  407. function getList() {
  408. getServiceLogList(queryParams)
  409. .then((r) => {
  410. tableData.value = r.rows;
  411. total.value = r.total;
  412. });
  413. }
  414. function processModelData(data) {
  415. const resultMap = {};
  416. data.forEach(item => {
  417. const mdName = item.mdName?.trim();
  418. if (!mdName) return;
  419. const statisNum = Number(item.statisNum) || 0;
  420. if (!resultMap[mdName]) {
  421. // 初始化该模型的数据
  422. resultMap[mdName] = {
  423. mdName: mdName,
  424. mdId: item.mdId || null,
  425. totalStatisNum: statisNum,
  426. recordCount: 1,
  427. firstRecordDate: item.statisTm,
  428. lastRecordDate: item.statisTm,
  429. records: [item] // 可选:保留详细记录
  430. };
  431. } else {
  432. // 更新现有模型数据
  433. resultMap[mdName].totalStatisNum += statisNum;
  434. resultMap[mdName].recordCount += 1;
  435. // 更新最新记录日期
  436. if (item.statisTm && (!resultMap[mdName].lastRecordDate || item.statisTm > resultMap[mdName].lastRecordDate)) {
  437. resultMap[mdName].lastRecordDate = item.statisTm;
  438. }
  439. resultMap[mdName].records.push(item);
  440. }
  441. });
  442. // 转换为数组并排序(按统计数字降序)
  443. return Object.values(resultMap)
  444. .sort((a, b) => b.totalStatisNum - a.totalStatisNum);
  445. }
  446. function getDateRange(startStr, endStr) {
  447. const startDate = new Date(startStr);
  448. const endDate = new Date(endStr);
  449. const dates = [];
  450. // 验证日期有效性
  451. if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
  452. throw new Error('无效的日期格式');
  453. }
  454. if (startDate > endDate) {
  455. throw new Error('开始日期不能晚于结束日期');
  456. }
  457. // 复制开始日期对象,避免修改原日期
  458. const currentDate = new Date(startDate);
  459. // 循环直到当前日期超过结束日期
  460. while (currentDate <= endDate) {
  461. // 格式化为 YYYY-MM-DD
  462. const year = currentDate.getFullYear();
  463. const month = String(currentDate.getMonth() + 1).padStart(2, '0');
  464. const day = String(currentDate.getDate()).padStart(2, '0');
  465. dates.push(`${year}-${month}-${day}`);
  466. // 日期加一天
  467. currentDate.setDate(currentDate.getDate() + 1);
  468. }
  469. return dates;
  470. }
  471. async function drawRight1(){
  472. var chartDom = document.getElementById('right1');
  473. var myChart = echarts.init(chartDom);
  474. var option;
  475. const last7Days = getLast7DaysRange();
  476. var par = {
  477. params:{
  478. beginTime:last7Days.start,
  479. endTime:last7Days.end,
  480. }
  481. }
  482. var x = getDateRange(last7Days.start,last7Days.end)
  483. var y = []
  484. await getMdAllList(par).then(res=>{
  485. res.data.forEach(item=>{
  486. item.value = []
  487. item.lineCharts.forEach(item1=>{
  488. item.value.push(item1.num)
  489. })
  490. })
  491. res.data.forEach(item=>{
  492. var par = {
  493. name: item.mdName,
  494. type: 'line',
  495. data: item.value,
  496. smooth: true
  497. }
  498. y.push(par)
  499. })
  500. console.log(res.data)
  501. })
  502. option = {
  503. grid: {
  504. top: '10%', // 图表距离容器顶部的距离
  505. right: '5%', // 图表距离容器右侧的距离
  506. bottom: '15%', // 图表距离容器底部的距离
  507. left: '10%', // 图表距离容器左侧的距离
  508. containLabel: true // 确保坐标轴标签在 grid 区域内
  509. },
  510. tooltip: {
  511. trigger: 'axis',
  512. show:'false'
  513. },
  514. legend: {
  515. top:'88%'
  516. },
  517. xAxis: {
  518. splitLine: { show: false },
  519. // type: 'category',
  520. data: x,
  521. axisLabel: {
  522. top:'10%',
  523. interval: 0, // 强制显示所有标签
  524. rotate: -25, // 旋转角度,正值表示顺时针旋转,负值表示逆时针旋转
  525. // 可以设置文字样式,如字体大小、颜色等
  526. textStyle: {
  527. fontSize: 9,
  528. color: '#333'
  529. }
  530. }
  531. },
  532. yAxis: {
  533. type: 'value',
  534. show: true,
  535. splitLine: { show: false },
  536. name: '单位:次',
  537. minInterval: 1,
  538. axisTick: {
  539. show: true // 确保显示刻度线
  540. },
  541. axisLine: {
  542. show: true, // 确保显示轴线
  543. lineStyle: {
  544. color: '#333', // 可以设置轴线的颜色,例如与文字颜色一致
  545. // width: 1 // 可以设置轴线宽度,可选
  546. }
  547. },
  548. },
  549. series:y
  550. };
  551. option && myChart.setOption(option);
  552. }
  553. onMounted(() => {
  554. getList()
  555. getList1()
  556. drawRight2()
  557. drawRight1()
  558. });
  559. watch(() => queryParams.time, value => {
  560. queryParams.sttm = value[0];
  561. queryParams.entm = value[1];
  562. }, {immediate: true})
  563. </script>
  564. <style scoped>
  565. .el-row {
  566. margin-bottom: 10px;
  567. }
  568. .table_box ::v-deep .el-table {
  569. border: 1px solid #e6e6e6;
  570. border-right: 1px solid #e6e6e6;
  571. border-bottom: none;
  572. border-radius: 5px;
  573. }
  574. ::v-deep .el-form-item {
  575. margin-bottom: 0;
  576. }
  577. </style>