feat: Optimize log file reading (#10249)

This commit is contained in:
CityFun 2025-09-03 17:25:30 +08:00 committed by GitHub
parent ea7f324a9d
commit 39cf26628f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 91 additions and 59 deletions

View file

@ -580,13 +580,6 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi
if err != nil { if err != nil {
return nil, err return nil, err
} }
if req.Latest && req.Page == 1 && len(logFileRes.Lines) < 1000 && logFileRes.TotalPages > 1 {
res, err := files.ReadFileByLine(logFilePath, logFileRes.TotalPages-1, req.PageSize, false)
if err != nil {
return nil, err
}
logFileRes.Lines = append(res.Lines, logFileRes.Lines...)
}
scope = "page" scope = "page"
lines = logFileRes.Lines lines = logFileRes.Lines
} }

View file

@ -29,20 +29,22 @@
</div> </div>
<div class="log-container" ref="logContainer" @scroll="onScroll" :style="containerStyle"> <div class="log-container" ref="logContainer" @scroll="onScroll" :style="containerStyle">
<div class="log-spacer" :style="{ height: `${totalHeight}px` }"></div> <div class="log-spacer" :style="{ height: `${totalHeight}px` }"></div>
<div <div class="log-viewport" :style="{ transform: `translateY(${offsetY}px)` }">
v-for="(log, index) in visibleLogs" <div
:key="startIndex + index" v-for="(log, index) in visibleLogs"
class="log-item" :key="`${startIndex + index}-${log}`"
:style="{ top: `${(startIndex + index) * logHeight}px` }" class="log-item"
> :style="{ height: `${logHeight}px` }"
<hightlight :log="log" :type="config.colorMode ?? 'nginx'"></hightlight> >
<hightlight :log="log" :type="config.colorMode ?? 'nginx'"></hightlight>
</div>
</div> </div>
<hightlight v-if="logs.length === 0" :log="$t('commons.log.noLog')" type="system"></hightlight> <hightlight v-if="logs.length === 0" :log="$t('commons.log.noLog')" type="system"></hightlight>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'; import { nextTick, onMounted, onUnmounted, reactive, ref, computed } from 'vue';
import { downloadFile } from '@/utils/util'; import { downloadFile } from '@/utils/util';
import { readByLine } from '@/api/modules/files'; import { readByLine } from '@/api/modules/files';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
@ -144,46 +146,60 @@ const isTailDisabled = ref();
const firstLoading = ref(false); const firstLoading = ref(false);
const logs = ref<string[]>([]); const logs = ref<string[]>([]);
const logContainer = ref<HTMLElement | null>(null); const logContainer = ref<HTMLElement | null>(null);
const logHeight = 20; const logHeight = 23;
const logCount = ref(0);
const totalHeight = computed(() => logHeight * logCount.value);
const containerHeight = ref(500); const containerHeight = ref(500);
const visibleCount = computed(() => Math.ceil(containerHeight.value / logHeight)); const scrollTop = ref(0);
const startIndex = ref(0);
const lastScrollTop = ref(0); const lastScrollTop = ref(0);
const totalLines = ref(0); const totalLines = ref(0);
const stopReading = ref(false); const stopReading = ref(false);
const totalPages = ref(0); const totalPages = ref(0);
const visibleLogs = computed(() => { let resizeObserver: ResizeObserver | null = null;
const safeStartIndex = Math.max(0, Math.min(startIndex.value, Math.max(0, logs.value.length - visibleCount.value))); const isEndOfFile = ref(false);
if (safeStartIndex !== startIndex.value) {
nextTick(() => { const totalHeight = computed(() => logs.value.length * logHeight);
startIndex.value = safeStartIndex;
}); const visibleCount = computed(() => {
} const buffer = 5;
return logs.value.slice(safeStartIndex, safeStartIndex + visibleCount.value); return Math.ceil(containerHeight.value / logHeight) + buffer * 2;
}); });
const startIndex = computed(() => {
const buffer = 5;
const index = Math.floor(scrollTop.value / logHeight) - buffer;
return Math.max(0, index);
});
const endIndex = computed(() => {
return Math.min(logs.value.length, startIndex.value + visibleCount.value);
});
const visibleLogs = computed(() => {
return logs.value.slice(startIndex.value, endIndex.value);
});
const offsetY = computed(() => {
return startIndex.value * logHeight;
});
const updateContainerHeight = () => {
if (logContainer.value) {
const rect = logContainer.value.getBoundingClientRect();
containerHeight.value = rect.height;
}
};
const onScroll = async () => { const onScroll = async () => {
if (!logContainer.value) return; if (!logContainer.value) return;
const scrollTop = logContainer.value.scrollTop; scrollTop.value = logContainer.value.scrollTop;
const scrollHeight = logContainer.value.scrollHeight; const scrollHeight = logContainer.value.scrollHeight;
const clientHeight = logContainer.value.clientHeight; const clientHeight = logContainer.value.clientHeight;
lastScrollTop.value = scrollTop; lastScrollTop.value = scrollTop.value;
if (!isLoading.value) {
const newStartIndex = Math.max(
0,
Math.min(Math.floor(scrollTop / logHeight), Math.max(0, logs.value.length - visibleCount.value)),
);
startIndex.value = newStartIndex;
}
if (isLoading.value) return; if (isLoading.value) return;
if (scrollTop <= 50 && readReq.page > 1) { if (scrollTop.value <= 50 && readReq.page > 1) {
if (minPage.value <= 1) { if (minPage.value <= 1) {
return; return;
} }
@ -192,7 +208,7 @@ const onScroll = async () => {
await getContent(true); await getContent(true);
return; return;
} }
if (scrollHeight - scrollTop - clientHeight <= 50 && !end.value) { if (scrollHeight - scrollTop.value - clientHeight <= 50 && !end.value && !isEndOfFile.value) {
if (readReq.page < maxPage.value) { if (readReq.page < maxPage.value) {
readReq.page = maxPage.value; readReq.page = maxPage.value;
await getContent(false); await getContent(false);
@ -230,7 +246,7 @@ const changeTail = (fromOutSide: boolean) => {
const clearLog = (): void => { const clearLog = (): void => {
logs.value = []; logs.value = [];
startIndex.value = 0; scrollTop.value = 0;
readReq.page = 1; readReq.page = 1;
lastLogs.value = []; lastLogs.value = [];
}; };
@ -251,6 +267,7 @@ const getContent = async (pre: boolean) => {
} catch (error) { } catch (error) {
isLoading.value = false; isLoading.value = false;
firstLoading.value = false; firstLoading.value = false;
return;
} }
totalLines.value = res.data.totalLines; totalLines.value = res.data.totalLines;
@ -319,24 +336,14 @@ const getContent = async (pre: boolean) => {
const addedLines = newLogs.length; const addedLines = newLogs.length;
const newScrollPosition = lastScrollTop.value + addedLines * logHeight; const newScrollPosition = lastScrollTop.value + addedLines * logHeight;
logContainer.value.scrollTop = newScrollPosition; logContainer.value.scrollTop = newScrollPosition;
startIndex.value = Math.max(
0,
Math.min(
Math.floor(newScrollPosition / logHeight),
Math.max(0, logs.value.length - visibleCount.value),
),
);
} }
} else { } else {
logContainer.value.scrollTop = totalHeight.value; logContainer.value.scrollTop = logContainer.value.scrollHeight;
containerHeight.value = logContainer.value.getBoundingClientRect().height;
startIndex.value = Math.max(0, logs.value.length - visibleCount.value);
} }
} }
}); });
} }
logCount.value = logs.value.length;
end.value = res.data.end; end.value = res.data.end;
totalPages.value = res.data.total; totalPages.value = res.data.total;
emit('update:hasContent', logs.value.length > 0); emit('update:hasContent', logs.value.length > 0);
@ -344,6 +351,7 @@ const getContent = async (pre: boolean) => {
readReq.page = res.data.total; readReq.page = res.data.total;
readReq.latest = false; readReq.latest = false;
maxPage.value = res.data.total; maxPage.value = res.data.total;
isEndOfFile.value = true;
if (res.data.lines && res.data.lines.length > 500) { if (res.data.lines && res.data.lines.length > 500) {
minPage.value = res.data.total - 1; minPage.value = res.data.total - 1;
} else { } else {
@ -354,19 +362,26 @@ const getContent = async (pre: boolean) => {
} }
if (logs.value && logs.value.length > 3000) { if (logs.value && logs.value.length > 3000) {
const removedCount = readReq.pageSize; const removedCount = readReq.pageSize;
const currentScrollRatio = scrollTop.value / (logs.value.length * logHeight);
if (pre) { if (pre) {
logs.value.splice(logs.value.length - removedCount, removedCount); logs.value.splice(logs.value.length - removedCount, removedCount);
if (maxPage.value > 1) { if (maxPage.value > 1) {
maxPage.value--; maxPage.value--;
} }
} else { } else {
isEndOfFile.value = false;
logs.value.splice(0, removedCount); logs.value.splice(0, removedCount);
startIndex.value = Math.max(0, startIndex.value - removedCount); nextTick(() => {
if (logContainer.value) {
const newScrollTop = currentScrollRatio * (logs.value.length * logHeight);
logContainer.value.scrollTop = Math.max(0, newScrollTop - removedCount * logHeight);
}
});
if (minPage.value > 1) { if (minPage.value > 1) {
minPage.value++; minPage.value++;
} }
} }
logCount.value = logs.value.length;
} }
isLoading.value = false; isLoading.value = false;
}; };
@ -396,16 +411,19 @@ watch(
); );
const init = async () => { const init = async () => {
tailLog.value = false;
if (props.config.tail) { if (props.config.tail) {
tailLog.value = props.config.tail; tailLog.value = props.config.tail;
} else {
tailLog.value = false;
} }
if (tailLog.value) { if (tailLog.value) {
changeTail(false); changeTail(false);
} }
readReq.latest = true; readReq.latest = true;
await getContent(false); await getContent(false);
if (readReq.page > 1 && totalPages.value == maxPage.value) {
readReq.page--;
await getContent(true);
}
}; };
const containerStyle = computed(() => ({ const containerStyle = computed(() => ({
@ -425,16 +443,29 @@ onMounted(async () => {
readReq.taskOperate = props.config.taskOperate; readReq.taskOperate = props.config.taskOperate;
readReq.resourceID = props.config.resourceID; readReq.resourceID = props.config.resourceID;
await init(); await init();
updateContainerHeight();
if (logContainer.value) {
resizeObserver = new ResizeObserver(() => {
updateContainerHeight();
});
resizeObserver.observe(logContainer.value);
}
nextTick(() => { nextTick(() => {
if (logContainer.value) { if (logContainer.value) {
logContainer.value.scrollTop = totalHeight.value; logContainer.value.scrollTop = logContainer.value.scrollHeight;
containerHeight.value = logContainer.value.getBoundingClientRect().height;
} }
}); });
}); });
onUnmounted(() => { onUnmounted(() => {
onCloseLog(); onCloseLog();
if (resizeObserver && logContainer.value) {
resizeObserver.unobserve(logContainer.value);
resizeObserver.disconnect();
}
}); });
defineExpose({ changeTail, onDownload, clearLog }); defineExpose({ changeTail, onDownload, clearLog });
@ -453,13 +484,21 @@ defineExpose({ changeTail, onDownload, clearLog });
width: 100%; width: 100%;
} }
.log-item { .log-viewport {
position: absolute; position: absolute;
top: 0;
left: 0;
right: 0;
will-change: transform;
}
.log-item {
width: 100%; width: 100%;
padding: 5px; padding: 5px;
color: #f5f5f5; color: #f5f5f5;
box-sizing: border-box; box-sizing: border-box;
white-space: nowrap; white-space: nowrap;
overflow: hidden;
} }
.log-content { .log-content {