Skip to content

Commit e4fc4fe

Browse files
yoiteyouCYan1203
andauthored
feat(log): enhance log formatting and processing (#1337)
* Draft MR * feat(log): enhance log formatting and processing --------- Co-authored-by: niuran <[email protected]>
1 parent 8efac33 commit e4fc4fe

File tree

1 file changed

+150
-60
lines changed

1 file changed

+150
-60
lines changed

frontend/src/components/application_spaces/ApplicationSpaceDetail.vue

Lines changed: 150 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,8 @@
440440
let lastContainerLogTime = ''
441441
442442
const appendLog = (refElem, data, refLineNum, logBlocksRef, lastLogTimeRef) => {
443+
if (!refElem.value) return;
444+
443445
// ANSI 转 HTML 简单实现
444446
function ansiToHtml(text) {
445447
const ansiMap = {
@@ -454,30 +456,68 @@
454456
};
455457
return text.replace(/\x1b\[(\d+)m/g, (match, code) => ansiMap[code] || '');
456458
}
457-
// 提取时间戳(如 [003s] 或 [0107])
458-
const timeMatch = data.match(/\[(\d+s?|\d{4})\]/)
459-
let timeStr = ''
460-
if (timeMatch) {
461-
timeStr = timeMatch[1]
462-
// 格式化为 03s 或 0107
459+
460+
/**
461+
* 将 [0004] 转换为 [00:04] 格式
462+
*/
463+
function formatTime(timeStr) {
464+
if (!timeStr) return '';
465+
466+
// 处理秒格式 (如 "03s")
463467
if (/^\d+s$/.test(timeStr)) {
464-
timeStr = timeStr.padStart(3, '0')
468+
const seconds = timeStr.replace('s', '').padStart(2, '0');
469+
return `00:${seconds}`;
470+
}
471+
472+
// 处理纯数字格式 (如 "0107")
473+
if (/^\d{4}$/.test(timeStr)) {
474+
return `${timeStr.slice(0, 2)}:${timeStr.slice(2)}`;
465475
}
476+
477+
return timeStr;
466478
}
467-
// 处理内容,去掉时间戳部分
468-
let content = data.replace(/\[(\d+s?|\d{4})\]\s*/, '')
469-
content = ansiToHtml(content.replace(/\\r/g, '<br>'))
470-
// 合并到同一时间块
471-
if (timeStr && lastLogTimeRef.value === timeStr && logBlocksRef.value.length > 0) {
472-
logBlocksRef.value[logBlocksRef.value.length - 1].lines.push(content)
473-
} else if (timeStr) {
474-
logBlocksRef.value.push({ time: timeStr, lines: [content] })
475-
lastLogTimeRef.value = timeStr
476-
} else {
477-
// 没有时间戳,单独作为一块
478-
logBlocksRef.value.push({ time: '', lines: [content] })
479-
lastLogTimeRef.value = ''
479+
480+
/**
481+
* 过滤不需要的前缀如"build-"
482+
*/
483+
function filterPrefixes(text) {
484+
return text.replace(/(build|platform)-\d+\s*\|\s*/g, '');
485+
}
486+
487+
// 新增:在日志级别后添加空格的函数
488+
function addSpaceAfterLogLevel(text) {
489+
return text.replace(/(INFO|WARN|ERROR|DEBUG|TRACE|FATAL)([^\s])/g, '$1 $2');
480490
}
491+
492+
// 处理换行符
493+
const processedData = data.replace(/\\n/g, '\n');
494+
495+
// 按行处理日志内容
496+
processedData.split('\n').forEach(line => {
497+
if (!line.trim()) return;
498+
499+
// 转换时间格式 [0004] → [00:04]
500+
line = line.replace(/\[(\d{4})\]/g, (_, time) => `[${time.slice(0, 2)}:${time.slice(2)}]`)
501+
.replace(/\[(\d+)s\]/g, (_, sec) => `[00:${sec.padStart(2, '0')}]`);
502+
503+
// 处理内容
504+
let content = line;
505+
506+
// 过滤不需要的前缀
507+
content = filterPrefixes(content);
508+
509+
// 在日志级别后添加空格
510+
content = addSpaceAfterLogLevel(content);
511+
512+
content = ansiToHtml(content.replace(/\\r/g, '<br>'));
513+
514+
// 合并到同一时间块(不再使用时间块分组)
515+
logBlocksRef.value.push({
516+
time: '', // 置空时间,不在渲染时显示单独的时间行
517+
lines: [content]
518+
});
519+
});
520+
481521
// 重新渲染
482522
renderLogBlocks(refElem, logBlocksRef)
483523
refLineNum.value = logBlocksRef.value.reduce((acc, b) => acc + b.lines.length, 0)
@@ -486,54 +526,104 @@
486526
})
487527
}
488528
489-
// 渲染日志块
490529
function renderLogBlocks(refElem, logBlocksRef) {
491-
if (!refElem.value) return
492-
refElem.value.innerHTML = ''
493-
let lineNum = 0
494-
logBlocksRef.value.forEach(block => {
495-
const blockDiv = document.createElement('div')
496-
blockDiv.className = 'mb-2'
497-
if (block.time) {
498-
const timeDiv = document.createElement('div')
499-
timeDiv.className = 'text-base text-gray-400 mb-1 flex items-center'
500-
timeDiv.innerHTML = `⏱ <span class='ml-1'>${block.time}</span>`
501-
blockDiv.appendChild(timeDiv)
502-
}
503-
block.lines.forEach(line => {
504-
const lineDiv = document.createElement('div')
505-
lineDiv.className = 'flex items-start'
506-
// 在INFO/WARN/ERROR等类型和内容之间加4px间距
507-
let lineHtml = line
508-
// 匹配以INFO/WARN/ERROR/DEBUG等大写单词开头的内容,后跟内容
509-
lineHtml = lineHtml.replace(/^(<span[^>]*>)*(INFO|WARN|ERROR|DEBUG|TRACE|FATAL|NOTICE|WARNING)(<\/span[^>]*>)*([\s\S]*?)(?=<|$)/, (match, p1, type, p3, rest) => {
510-
return `${p1 || ''}${type}${p3 || ''}<span style='display:inline-block;width:4px'></span>${rest || ''}`
530+
if (!refElem.value) return
531+
refElem.value.innerHTML = ''
532+
let lineNum = 0
533+
logBlocksRef.value.forEach(block => {
534+
const blockDiv = document.createElement('div')
535+
blockDiv.className = 'mb-2'
536+
if (block.time) {
537+
const timeDiv = document.createElement('div')
538+
timeDiv.className = 'text-base text-gray-400 mb-1 flex items-center'
539+
timeDiv.innerHTML = `⏱ <span class='ml-1'>${block.time}</span>`
540+
blockDiv.appendChild(timeDiv)
541+
}
542+
block.lines.forEach(line => {
543+
const lineDiv = document.createElement('div')
544+
lineDiv.className = 'flex items-start'
545+
// 在INFO/WARN/ERROR等类型和内容之间加4px间距
546+
let lineHtml = line
547+
// 匹配以INFO/WARN/ERROR/DEBUG等大写单词开头的内容,后跟内容
548+
lineHtml = lineHtml.replace(/^(<span[^>]*>)*(INFO|WARN|ERROR|DEBUG|TRACE|FATAL|NOTICE|WARNING)(<\/span[^>]*>)*([\s\S]*?)(?=<|$)/, (match, p1, type, p3, rest) => {
549+
return `${p1 || ''}${type}${p3 || ''}<span style='display:inline-block;width:4px'></span>${rest || ''}`
550+
})
551+
lineDiv.innerHTML = `<span class='mr-1 text-base text-gray-400' style='min-width:2.5em;display:inline-block;'>${lineNum}:</span><span class='mr-2 text-base'>•</span><span class='text-base'>${lineHtml}</span>`
552+
blockDiv.appendChild(lineDiv)
553+
lineNum++
511554
})
512-
lineDiv.innerHTML = `<span class='mr-1 text-base text-gray-400' style='min-width:2.5em;display:inline-block;'>${lineNum}:</span><span class='mr-2 text-base'>•</span><span class='text-base'>${lineHtml}</span>`
513-
blockDiv.appendChild(lineDiv)
514-
lineNum++
555+
refElem.value.appendChild(blockDiv)
515556
})
516-
refElem.value.appendChild(blockDiv)
517-
})
518557
}
519558
520559
const downloadLog = () => {
521-
const targetDiv = isBuildLogTab.value ? buildLogDiv : containerLogDiv
522-
if (!targetDiv.value) return
560+
const logBlocks = isBuildLogTab.value ? buildLogBlocks : containerLogBlocks
561+
562+
if (!logBlocks.value.length) {
563+
ElMessage.warning(t('application_spaces.errorPage.downloadNull'))
564+
return
565+
}
523566
524-
const logElements = targetDiv.value.querySelectorAll('p')
525-
let logContent = ''
526-
logElements.forEach((element) => {
527-
logContent += element.textContent + '\n'
528-
})
567+
// 时间格式转换函数
568+
const formatTime = (timeStr) => {
569+
if (!timeStr) return ''
570+
571+
// 处理秒格式 (如 "03s")
572+
if (/^\d+s$/.test(timeStr)) {
573+
const seconds = timeStr.replace('s', '').padStart(2, '0')
574+
return `00:${seconds}`
575+
}
576+
577+
// 处理纯数字格式 (如 "0107")
578+
if (/^\d{4}$/.test(timeStr)) {
579+
return `${timeStr.slice(0, 2)}:${timeStr.slice(2)}`
580+
}
581+
582+
return timeStr
583+
}
529584
530-
const blob = new Blob([logContent], { type: 'text/plain' })
531-
const link = document.createElement('a')
532-
link.href = URL.createObjectURL(blob)
533-
link.download = isBuildLogTab.value ? 'build_log.txt' : 'container_log.txt'
534-
link.click()
535-
URL.revokeObjectURL(link.href)
536-
}
585+
const logContent = logBlocks.value.map(block => {
586+
// 转换时间格式 [0004] → [00:04]
587+
const timeHeader = block.time ? `[${formatTime(block.time)}]\n` : ''
588+
return timeHeader + block.lines.map(line => {
589+
const tempDiv = document.createElement('div')
590+
tempDiv.innerHTML = line
591+
let plainText = tempDiv.textContent || tempDiv.innerText || ''
592+
593+
// 1. 日志级别后添加空格 (INFO/WARN/ERROR等)
594+
plainText = plainText.replace(/^(INFO|WARN|ERROR|DEBUG|TRACE|FATAL|NOTICE|WARNING)([^\s])/, '$1 $2')
595+
596+
// 2. 移除ANSI颜色代码
597+
plainText = plainText.replace(/\x1b\[[0-9;]*m/g, '')
598+
599+
// 3. 处理换行符 (\r → \n)
600+
plainText = plainText.replace(/\\r/g, '\n')
601+
602+
// 4. 处理换行符 (\n → 实际换行)
603+
plainText = plainText.replace(/\\n/g, '\n')
604+
605+
return plainText
606+
}).join('\n')
607+
}).join('\n\n')
608+
609+
try {
610+
const blob = new Blob([logContent], { type: 'text/plain;charset=utf-8' })
611+
const link = document.createElement('a')
612+
link.href = URL.createObjectURL(blob)
613+
link.download = `${props.repoName}_${isBuildLogTab.value ? 'build' : 'container'}_${new Date().toISOString().slice(0,10)}.log`
614+
615+
document.body.appendChild(link)
616+
link.click()
617+
618+
setTimeout(() => {
619+
document.body.removeChild(link)
620+
URL.revokeObjectURL(link.href)
621+
}, 100)
622+
} catch (err) {
623+
console.error('日志下载失败:', err)
624+
ElMessage.error(t('application_spaces.errorPage.downloadError'))
625+
}
626+
}
537627
538628
const syncSpaceStatus = () => {
539629
fetchEventSource(

0 commit comments

Comments
 (0)