1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-19 08:19:15 +08:00

feat: 优化日志读取 (#5485)

Refs https://github.com/1Panel-dev/1Panel/issues/3690
This commit is contained in:
zhengkunwang 2024-06-17 22:24:50 +08:00 committed by GitHub
parent f4103e285d
commit 345dbda400
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 125 additions and 91 deletions

View File

@ -126,6 +126,7 @@ type FileReadByLineReq struct {
Type string `json:"type" validate:"required"`
ID uint `json:"ID"`
Name string `json:"name"`
Latest bool `json:"latest"`
}
type FileExistReq struct {

View File

@ -37,6 +37,7 @@ type FileLineContent struct {
Content string `json:"content"`
End bool `json:"end"`
Path string `json:"path"`
Total int `json:"total"`
}
type FileExist struct {

View File

@ -410,7 +410,7 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi
logFilePath = path.Join(global.CONF.System.TmpDir, fmt.Sprintf("docker_logs/%s", req.Name))
}
lines, isEndOfFile, err := files.ReadFileByLine(logFilePath, req.Page, req.PageSize)
lines, isEndOfFile, total, err := files.ReadFileByLine(logFilePath, req.Page, req.PageSize, req.Latest)
if err != nil {
return nil, err
}
@ -418,6 +418,7 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi
Content: strings.Join(lines, "\n"),
End: isEndOfFile,
Path: logFilePath,
Total: total,
}
return res, nil
}

View File

@ -1008,7 +1008,7 @@ func (w WebsiteService) OpWebsiteLog(req request.WebsiteLogReq) (*response.Websi
}
}
filePath := path.Join(sitePath, "log", req.LogType)
lines, end, err := files.ReadFileByLine(filePath, req.Page, req.PageSize)
lines, end, _, err := files.ReadFileByLine(filePath, req.Page, req.PageSize, false)
if err != nil {
return nil, err
}

View File

@ -66,19 +66,44 @@ func IsHidden(path string) bool {
return path[0] == dotCharacter
}
func ReadFileByLine(filename string, page, pageSize int) ([]string, bool, error) {
if !NewFileOp().Stat(filename) {
return nil, true, nil
}
file, err := os.Open(filename)
func countLines(path string) (int, error) {
file, err := os.Open(path)
if err != nil {
return nil, false, err
return 0, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
lineCount := 0
for scanner.Scan() {
lineCount++
}
if err := scanner.Err(); err != nil {
return 0, err
}
return lineCount, nil
}
func ReadFileByLine(filename string, page, pageSize int, latest bool) (lines []string, isEndOfFile bool, total int, err error) {
if !NewFileOp().Stat(filename) {
return
}
file, err := os.Open(filename)
if err != nil {
return
}
defer file.Close()
totalLines, err := countLines(filename)
if err != nil {
return
}
total = (totalLines + pageSize - 1) / pageSize
reader := bufio.NewReaderSize(file, 8192)
var lines []string
if latest {
page = total
}
currentLine := 0
startLine := (page - 1) * pageSize
endLine := startLine + pageSize
@ -97,9 +122,8 @@ func ReadFileByLine(filename string, page, pageSize int) ([]string, bool, error)
}
}
isEndOfFile := currentLine < endLine
return lines, isEndOfFile, nil
isEndOfFile = currentLine < endLine
return
}
func GetParentMode(path string) (os.FileMode, error) {

View File

@ -1,7 +1,7 @@
{
"name": "1Panel-Frontend",
"private": true,
"version": "1.7",
"version": "1.10",
"description": "1Panel 前端",
"scripts": {
"dev": "vite",
@ -25,6 +25,7 @@
"@codemirror/legacy-modes": "^6.4.0",
"@codemirror/theme-one-dark": "^6.1.2",
"@element-plus/icons-vue": "^1.1.4",
"@highlightjs/vue-plugin": "^2.1.0",
"@vueuse/core": "^8.9.4",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
@ -32,6 +33,7 @@
"echarts": "^5.5.0",
"element-plus": "^2.7.5",
"fit2cloud-ui-plus": "^1.1.4",
"highlight.js": "^11.9.0",
"js-base64": "^3.7.7",
"md-editor-v3": "^2.11.3",
"monaco-editor": "^0.34.1",

View File

@ -12,35 +12,23 @@
</span>
</div>
<div class="mt-2.5">
<Codemirror
ref="logContainer"
:style="styleObject"
:autofocus="true"
:placeholder="$t('website.noLog')"
:indent-with-tab="true"
:tabSize="4"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
:styleActiveLine="true"
:extensions="extensions"
v-model="content"
:disabled="true"
@ready="handleReady"
/>
<highlightjs
ref="editorRef"
class="editor-main"
language="JavaScript"
:autodetect="false"
:code="content"
></highlightjs>
</div>
</div>
</template>
<script lang="ts" setup>
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, shallowRef } from 'vue';
import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import { downloadFile } from '@/utils/util';
import { ReadByLine } from '@/api/modules/files';
import { watch } from 'vue';
const extensions = [javascript(), oneDark];
const editorRef = ref();
interface LogProps {
id?: number;
@ -61,7 +49,7 @@ const props = defineProps({
},
style: {
type: String,
default: 'height: calc(100vh - 200px); width: 100%; min-height: 400px',
default: 'height: calc(100vh - 200px); width: 100%; min-height: 400px; overflow: auto;',
},
defaultButton: {
type: Boolean,
@ -84,29 +72,23 @@ const data = ref({
let timer: NodeJS.Timer | null = null;
const tailLog = ref(false);
const view = shallowRef();
const content = ref('');
const end = ref(false);
const lastContent = ref('');
const logContainer = ref();
const scrollerElement = ref<HTMLElement | null>(null);
const minPage = ref(1);
const maxPage = ref(1);
const readReq = reactive({
id: 0,
type: '',
name: '',
page: 0,
pageSize: 2000,
page: 1,
pageSize: 500,
latest: false,
});
const emit = defineEmits(['update:loading', 'update:hasContent', 'update:isReading']);
const handleReady = (payload) => {
view.value = payload.view;
const editorContainer = payload.container;
const editorElement = editorContainer.querySelector('.cm-editor');
scrollerElement.value = editorElement.querySelector('.cm-scroller') as HTMLElement;
};
const loading = ref(props.loading);
watch(
@ -121,25 +103,6 @@ const changeLoading = () => {
emit('update:loading', loading.value);
};
const styleObject = computed(() => {
const styles = {};
let style = 'height: calc(100vh - 200px); width: 100%; min-height: 400px';
if (props.style != null && props.style != '') {
style = props.style;
}
style.split(';').forEach((styleRule) => {
const [property, value] = styleRule.split(':');
if (property && value) {
const formattedProperty = property
.trim()
.replace(/([a-z])([A-Z])/g, '$1-$2')
.toLowerCase();
styles[formattedProperty] = value.trim();
}
});
return styles;
});
const stopSignals = [
'docker-compose up failed!',
'docker-compose up successful!',
@ -151,11 +114,8 @@ const stopSignals = [
'image push successful!',
];
const getContent = () => {
const getContent = (pre: boolean) => {
emit('update:isReading', true);
if (!end.value) {
readReq.page += 1;
}
readReq.id = props.config.id;
readReq.type = props.config.type;
readReq.name = props.config.name;
@ -163,6 +123,7 @@ const getContent = () => {
if (!end.value && res.data.end) {
lastContent.value = content.value;
}
res.data.content = res.data.content.replace(/\\u(\w{4})/g, function (match, grp) {
return String.fromCharCode(parseInt(grp, 16));
});
@ -175,28 +136,38 @@ const getContent = () => {
if (lastContent.value == '') {
content.value = res.data.content;
} else {
content.value = lastContent.value + '\n' + res.data.content;
content.value = pre
? res.data.content + '\n' + lastContent.value
: lastContent.value + '\n' + res.data.content;
}
} else {
if (content.value == '') {
content.value = res.data.content;
} else {
content.value = content.value + '\n' + res.data.content;
content.value = pre
? res.data.content + '\n' + content.value
: content.value + '\n' + res.data.content;
}
}
}
end.value = res.data.end;
emit('update:hasContent', content.value !== '');
nextTick(() => {
const state = view.value.state;
view.value.dispatch({
selection: { anchor: state.doc.length, head: state.doc.length },
});
view.value.focus();
const firstLine = view.value.state.doc.line(view.value.state.doc.lines);
const { top } = view.value.lineBlockAt(firstLine.from);
scrollerElement.value.scrollTo({ top, behavior: 'instant' });
if (pre) {
if (scrollerElement.value.scrollHeight > 2000) {
scrollerElement.value.scrollTop = 2000;
}
} else {
scrollerElement.value.scrollTop = scrollerElement.value.scrollHeight;
}
});
if (readReq.latest) {
readReq.page = res.data.total;
readReq.latest = false;
maxPage.value = res.data.total;
minPage.value = res.data.total;
}
});
};
@ -206,7 +177,7 @@ const changeTail = (fromOutSide: boolean) => {
}
if (tailLog.value) {
timer = setInterval(() => {
getContent();
getContent(false);
}, 1000 * 2);
} else {
onCloseLog();
@ -230,6 +201,10 @@ function isScrolledToBottom(element: HTMLElement): boolean {
return element.scrollTop + element.clientHeight + 1 >= element.scrollHeight;
}
function isScrolledToTop(element: HTMLElement): boolean {
return element.scrollTop === 0;
}
const init = () => {
if (props.config.tail) {
tailLog.value = props.config.tail;
@ -239,30 +214,56 @@ const init = () => {
if (tailLog.value) {
changeTail(false);
}
getContent();
readReq.latest = true;
getContent(false);
nextTick(() => {
if (scrollerElement.value) {
scrollerElement.value.addEventListener('scroll', function () {
if (isScrolledToBottom(scrollerElement.value)) {
getContent();
}
});
}
});
nextTick(() => {});
};
const clearLog = (): void => {
content.value = '';
};
const initCodemirror = () => {
nextTick(() => {
if (editorRef.value) {
scrollerElement.value = editorRef.value.$el as HTMLElement;
scrollerElement.value.addEventListener('scroll', function () {
if (isScrolledToBottom(scrollerElement.value)) {
readReq.page = maxPage.value;
getContent(false);
}
if (isScrolledToTop(scrollerElement.value)) {
readReq.page = minPage.value - 1;
if (readReq.page < 1) {
return;
}
minPage.value = readReq.page;
getContent(true);
}
});
let hljsDom = scrollerElement.value.querySelector('.hljs') as HTMLElement;
hljsDom.style['min-height'] = '300px';
}
});
};
onUnmounted(() => {
onCloseLog();
});
onMounted(() => {
initCodemirror();
init();
});
defineExpose({ changeTail, onDownload, clearLog });
</script>
<style lang="scss" scoped>
.editor-main {
height: calc(100vh - 480px);
width: 100%;
min-height: 400px;
overflow: auto;
}
</style>

View File

@ -6,6 +6,9 @@ import '@/styles/common.scss';
import '@/assets/iconfont/iconfont.css';
import '@/assets/iconfont/iconfont.js';
import '@/styles/style.css';
import 'highlight.js/styles/atom-one-dark.css';
import 'highlight.js/lib/common';
const styleModule = import.meta.glob('xpack/styles/index.scss');
for (const path in styleModule) {
styleModule[path]?.();
@ -21,8 +24,10 @@ import Components from '@/components';
import ElementPlus from 'element-plus';
import Fit2CloudPlus from 'fit2cloud-ui-plus';
import * as Icons from '@element-plus/icons-vue';
import hljsVuePlugin from '@highlightjs/vue-plugin';
const app = createApp(App);
app.use(hljsVuePlugin);
app.component('SvgIcon', SvgIcon);
app.use(ElementPlus);
app.use(Fit2CloudPlus, { locale: i18n.global.messages.value[localStorage.getItem('lang') || 'zh'] });

View File

@ -33,7 +33,6 @@
</template>
<template #main>
<Basic :id="id" v-if="index === 'basic'"></Basic>
<Safety :id="id" v-if="index === 'safety'"></Safety>
<Log :id="id" v-if="index === 'log'"></Log>
<Resource :id="id" v-if="index === 'resource'"></Resource>
<PHP :id="id" v-if="index === 'php'"></PHP>