feat: 面板系统日志增加日期选择,支持追踪读取 (#2361)

This commit is contained in:
ssongliu 2023-09-20 16:24:26 +08:00 committed by GitHub
parent 017eb3814b
commit 695f3278c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 202 additions and 39 deletions

View File

@ -91,13 +91,34 @@ func (b *BaseApi) CleanLogs(c *gin.Context) {
}
// @Tags Logs
// @Summary Load system logs
// @Description 获取系统日志
// @Summary Load system log files
// @Description 获取系统日志文件列表
// @Success 200
// @Security ApiKeyAuth
// @Router /logs/system [get]
func (b *BaseApi) GetSystemLogs(c *gin.Context) {
data, err := logService.LoadSystemLog()
// @Router /logs/system/files [get]
func (b *BaseApi) GetSystemFiles(c *gin.Context) {
data, err := logService.ListSystemLogFile()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Logs
// @Summary Load system logs
// @Description 获取系统日志
// @Success 200
// @Security ApiKeyAuth
// @Router /logs/system [post]
func (b *BaseApi) GetSystemLogs(c *gin.Context) {
var req dto.OperationWithName
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
data, err := logService.LoadSystemLog(req.Name)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return

View File

@ -1,8 +1,12 @@
package service
import (
"fmt"
"os"
"path"
"path/filepath"
"sort"
"strings"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model"
@ -19,13 +23,14 @@ type LogService struct{}
const logs = "https://resource.fit2cloud.com/installation-log.sh"
type ILogService interface {
ListSystemLogFile() ([]string, error)
CreateLoginLog(operation model.LoginLog) error
PageLoginLog(search dto.SearchLgLogWithPage) (int64, interface{}, error)
CreateOperationLog(operation model.OperationLog) error
PageOperationLog(search dto.SearchOpLogWithPage) (int64, interface{}, error)
LoadSystemLog() (string, error)
LoadSystemLog(name string) (string, error)
CleanLogs(logtype string) error
}
@ -38,6 +43,32 @@ func (u *LogService) CreateLoginLog(operation model.LoginLog) error {
return logRepo.CreateLoginLog(&operation)
}
func (u *LogService) ListSystemLogFile() ([]string, error) {
logDir := path.Join(global.CONF.System.BaseDir, "1panel/log")
var files []string
if err := filepath.Walk(logDir, func(pathItem string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && (strings.HasSuffix(info.Name(), ".log") || strings.HasSuffix(info.Name(), ".log.gz")) {
files = append(files, strings.TrimSuffix(info.Name(), ".gz"))
return nil
}
return nil
}); err != nil {
return nil, err
}
if len(files) < 2 {
return files, nil
}
sort.Slice(files, func(i, j int) bool {
return files[i] > files[j]
})
return files, nil
}
func (u *LogService) PageLoginLog(req dto.SearchLgLogWithPage) (int64, interface{}, error) {
total, ops, err := logRepo.PageLoginLog(
req.Page,
@ -81,10 +112,16 @@ func (u *LogService) PageOperationLog(req dto.SearchOpLogWithPage) (int64, inter
return total, dtoOps, err
}
func (u *LogService) LoadSystemLog() (string, error) {
filePath := path.Join(global.CONF.System.DataDir, "log/1Panel.log")
func (u *LogService) LoadSystemLog(name string) (string, error) {
filePath := path.Join(global.CONF.System.DataDir, "log", name)
if _, err := os.Stat(filePath); err != nil {
return "", buserr.New("ErrHttpReqNotFound")
fileGzPath := path.Join(global.CONF.System.DataDir, "log", name+".gz")
if _, err := os.Stat(fileGzPath); err != nil {
return "", buserr.New("ErrHttpReqNotFound")
}
if err := handleGunzip(fileGzPath); err != nil {
return "", fmt.Errorf("handle ungzip file %s falied, err: %v", fileGzPath, err)
}
}
content, err := os.ReadFile(filePath)
if err != nil {

View File

@ -240,7 +240,7 @@ func (u *SSHService) LoadLog(req dto.SearchSSHLog) (*dto.SSHLog, error) {
if err != nil {
return err
}
if !info.IsDir() && strings.HasPrefix(info.Name(), "secure") || strings.HasPrefix(info.Name(), "auth") {
if !info.IsDir() && (strings.HasPrefix(info.Name(), "secure") || strings.HasPrefix(info.Name(), "auth")) {
if !strings.HasSuffix(info.Name(), ".gz") {
fileList = append(fileList, sshFileItem{Name: pathItem, Year: info.ModTime().Year()})
return nil
@ -311,7 +311,7 @@ func (u *SSHService) AnalysisLog(req dto.SearchForAnalysis) ([]dto.SSHLogAnalysi
if err != nil {
return err
}
if !info.IsDir() && strings.HasPrefix(info.Name(), "secure") || strings.HasPrefix(info.Name(), "auth") {
if !info.IsDir() && (strings.HasPrefix(info.Name(), "secure") || strings.HasPrefix(info.Name(), "auth")) {
if !strings.HasSuffix(info.Name(), ".gz") {
fileList = append(fileList, pathItem)
return nil

View File

@ -17,7 +17,7 @@ func (s *LogRouter) InitLogRouter(Router *gin.RouterGroup) {
operationRouter.POST("/login", baseApi.GetLoginLogs)
operationRouter.POST("/operation", baseApi.GetOperationLogs)
operationRouter.POST("/clean", baseApi.CleanLogs)
operationRouter.GET("/system", baseApi.GetSystemLogs)
operationRouter.GET("/system/files", baseApi.GetSystemFiles)
operationRouter.POST("/system", baseApi.GetSystemLogs)
}
}

View File

@ -7465,7 +7465,7 @@ const docTemplate = `{
}
},
"/logs/system": {
"get": {
"post": {
"security": [
{
"ApiKeyAuth": []
@ -7483,6 +7483,25 @@ const docTemplate = `{
}
}
},
"/logs/system/files": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取系统日志文件列表",
"tags": [
"Logs"
],
"summary": "Load system log files",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/openResty": {
"get": {
"security": [

View File

@ -7458,7 +7458,7 @@
}
},
"/logs/system": {
"get": {
"post": {
"security": [
{
"ApiKeyAuth": []
@ -7476,6 +7476,25 @@
}
}
},
"/logs/system/files": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取系统日志文件列表",
"tags": [
"Logs"
],
"summary": "Load system log files",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/openResty": {
"get": {
"security": [

View File

@ -8807,7 +8807,7 @@ paths:
tags:
- Logs
/logs/system:
get:
post:
description: 获取系统日志
responses:
"200":
@ -8817,6 +8817,17 @@ paths:
summary: Load system logs
tags:
- Logs
/logs/system/files:
get:
description: 获取系统日志文件列表
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Load system log files
tags:
- Logs
/openResty:
get:
description: 获取 OpenResty 配置信息

View File

@ -10,8 +10,11 @@ export const getLoginLogs = (info: Log.SearchLgLog) => {
return http.post<ResPage<Log.OperationLog>>(`/logs/login`, info);
};
export const getSystemLogs = () => {
return http.get<string>(`/logs/system`);
export const getSystemFiles = () => {
return http.get<Array<string>>(`/logs/system/files`);
};
export const getSystemLogs = (name: string) => {
return http.post<string>(`/logs/system`, { name: name });
};
export const cleanLogs = (param: Log.CleanLog) => {

View File

@ -336,11 +336,6 @@ html {
}
}
.no-active-button {
background: none;
border: none;
}
.common-prompt {
margin-bottom: 20px !important;
}
@ -369,3 +364,11 @@ html {
box-shadow: 0 1px 0 0 var(--el-input-border-color) inset, 0 -1px 0 0 var(--el-input-border-color) inset,
-1px 0 0 0 var(--el-input-border-color) inset;
}
.tag-button {
margin-right: 10px;
&.no-active {
background: none;
border: none;
}
}

View File

@ -4,13 +4,13 @@
<template #toolbar>
<el-row>
<el-col :xs="24" :sm="16" :md="16" :lg="16" :xl="16">
<el-button class="no-active-button" @click="onChangeRoute('OperationLog')">
<el-button class="tag-button no-active" @click="onChangeRoute('OperationLog')">
{{ $t('logs.operation') }}
</el-button>
<el-button type="primary" @click="onChangeRoute('LoginLog')">
<el-button class="tag-button" type="primary" @click="onChangeRoute('LoginLog')">
{{ $t('logs.login') }}
</el-button>
<el-button class="no-active-button" @click="onChangeRoute('SystemLog')">
<el-button class="tag-button no-active" @click="onChangeRoute('SystemLog')">
{{ $t('logs.system') }}
</el-button>
</el-col>

View File

@ -4,13 +4,13 @@
<template #toolbar>
<el-row>
<el-col :xs="24" :sm="16" :md="16" :lg="16" :xl="16">
<el-button type="primary" @click="onChangeRoute('OperationLog')">
<el-button type="primary" class="tag-button" @click="onChangeRoute('OperationLog')">
{{ $t('logs.operation') }}
</el-button>
<el-button class="no-active-button" @click="onChangeRoute('LoginLog')">
<el-button class="tag-button no-active" @click="onChangeRoute('LoginLog')">
{{ $t('logs.login') }}
</el-button>
<el-button class="no-active-button" @click="onChangeRoute('SystemLog')">
<el-button class="tag-button no-active" @click="onChangeRoute('SystemLog')">
{{ $t('logs.system') }}
</el-button>
</el-col>

View File

@ -4,25 +4,36 @@
<template #toolbar>
<el-row>
<el-col :span="16">
<el-button class="no-active-button" @click="onChangeRoute('OperationLog')">
<el-button class="tag-button no-active" @click="onChangeRoute('OperationLog')">
{{ $t('logs.operation') }}
</el-button>
<el-button class="no-active-button" @click="onChangeRoute('LoginLog')">
<el-button class="tag-button no-active" @click="onChangeRoute('LoginLog')">
{{ $t('logs.login') }}
</el-button>
<el-button type="primary" @click="onChangeRoute('SystemLog')">
<el-button class="tag-button" type="primary" @click="onChangeRoute('SystemLog')">
{{ $t('logs.system') }}
</el-button>
</el-col>
</el-row>
</template>
<template #search>
<el-select class="float-left" v-model="currentFile" @change="search()">
<template #prefix>{{ $t('commons.button.log') }}</template>
<el-option v-for="(item, index) in fileList" :key="index" :label="item" :value="item" />
</el-select>
<div class="watchCheckbox">
<el-checkbox border @change="changeWatch" v-model="isWatch">
{{ $t('commons.button.watch') }}
</el-checkbox>
</div>
</template>
<template #main>
<codemirror
:autofocus="true"
:placeholder="$t('commons.msg.noneData')"
:indent-with-tab="true"
:tabSize="4"
style="height: calc(100vh - 290px)"
style="height: calc(100vh - 370px)"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
@ -41,12 +52,16 @@
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
import { nextTick, onMounted, ref, shallowRef } from 'vue';
import { nextTick, onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
import { useRouter } from 'vue-router';
import { getSystemLogs } from '@/api/modules/log';
import { getSystemFiles, getSystemLogs } from '@/api/modules/log';
const router = useRouter();
const loading = ref();
const isWatch = ref();
const currentFile = ref('1Panel.log');
const fileList = ref();
const extensions = [javascript(), oneDark];
const logs = ref();
const view = shallowRef();
@ -54,11 +69,31 @@ const handleReady = (payload) => {
view.value = payload.view;
};
const loadSystemlogs = async () => {
await getSystemLogs()
let timer: NodeJS.Timer | null = null;
const changeWatch = async () => {
if (isWatch.value) {
timer = setInterval(() => {
search();
}, 1000 * 3);
} else {
if (timer) {
clearInterval(Number(timer));
timer = null;
}
}
};
const loadFiles = async () => {
const res = await getSystemFiles();
fileList.value = res.data || [];
};
const search = async () => {
await getSystemLogs(currentFile.value)
.then((res) => {
loading.value = false;
logs.value = res.data;
logs.value = res.data.replace(/\u0000/g, '');
nextTick(() => {
const state = view.value.state;
view.value.dispatch({
@ -76,7 +111,22 @@ const onChangeRoute = async (addr: string) => {
router.push({ name: addr });
};
onBeforeUnmount(() => {
clearInterval(Number(timer));
timer = null;
});
onMounted(() => {
loadSystemlogs();
loadFiles();
search();
});
</script>
<style scoped lang="scss">
.watchCheckbox {
margin-top: 2px;
margin-bottom: 10px;
float: left;
margin-left: 20px;
}
</style>