mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 16:29:17 +08:00
feat: 优化容器日志加载方式,增加显示条数过滤 (#1370)
This commit is contained in:
parent
47dda4ac9f
commit
8808e1b0c3
@ -307,30 +307,23 @@ func (b *BaseApi) Inspect(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, result)
|
helper.SuccessWithData(c, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Container
|
|
||||||
// @Summary Container logs
|
|
||||||
// @Description 容器日志
|
|
||||||
// @Accept json
|
|
||||||
// @Param request body dto.ContainerLog true "request"
|
|
||||||
// @Success 200 {string} logs
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Router /containers/search/log [post]
|
|
||||||
func (b *BaseApi) ContainerLogs(c *gin.Context) {
|
func (b *BaseApi) ContainerLogs(c *gin.Context) {
|
||||||
var req dto.ContainerLog
|
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := global.VALID.Struct(req); err != nil {
|
|
||||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logs, err := containerService.ContainerLogs(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
global.LOG.Errorf("gin context http handler failed, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer wsConn.Close()
|
||||||
|
|
||||||
|
container := c.Query("container")
|
||||||
|
since := c.Query("since")
|
||||||
|
follow := c.Query("follow") == "true"
|
||||||
|
tail := c.Query("tail")
|
||||||
|
|
||||||
|
if err := containerService.ContainerLogs(wsConn, container, since, tail, follow); err != nil {
|
||||||
|
_ = wsConn.WriteMessage(1, []byte(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
helper.SuccessWithData(c, logs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Container Network
|
// @Tags Container Network
|
||||||
|
@ -69,11 +69,6 @@ type PortHelper struct {
|
|||||||
Protocol string `json:"protocol"`
|
Protocol string `json:"protocol"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContainerLog struct {
|
|
||||||
ContainerID string `json:"containerID" validate:"required"`
|
|
||||||
Mode string `json:"mode" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContainerOperation struct {
|
type ContainerOperation struct {
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Operation string `json:"operation" validate:"required,oneof=start stop restart kill pause unpause rename remove"`
|
Operation string `json:"operation" validate:"required,oneof=start stop restart kill pause unpause rename remove"`
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@ -26,6 +26,7 @@ import (
|
|||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -42,7 +43,7 @@ type IContainerService interface {
|
|||||||
ContainerCreate(req dto.ContainerCreate) error
|
ContainerCreate(req dto.ContainerCreate) error
|
||||||
ContainerLogClean(req dto.OperationWithName) error
|
ContainerLogClean(req dto.OperationWithName) error
|
||||||
ContainerOperation(req dto.ContainerOperation) error
|
ContainerOperation(req dto.ContainerOperation) error
|
||||||
ContainerLogs(param dto.ContainerLog) (string, error)
|
ContainerLogs(wsConn *websocket.Conn, container, since, tail string, follow bool) error
|
||||||
ContainerStats(id string) (*dto.ContainterStats, error)
|
ContainerStats(id string) (*dto.ContainterStats, error)
|
||||||
Inspect(req dto.InspectReq) (string, error)
|
Inspect(req dto.InspectReq) (string, error)
|
||||||
DeleteNetwork(req dto.BatchDelete) error
|
DeleteNetwork(req dto.BatchDelete) error
|
||||||
@ -332,16 +333,43 @@ func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) ContainerLogs(req dto.ContainerLog) (string, error) {
|
func (u *ContainerService) ContainerLogs(wsConn *websocket.Conn, container, since, tail string, follow bool) error {
|
||||||
cmd := exec.Command("docker", "logs", req.ContainerID)
|
command := fmt.Sprintf("docker logs %s", container)
|
||||||
if req.Mode != "all" {
|
if tail != "0" {
|
||||||
cmd = exec.Command("docker", "logs", req.ContainerID, "--since", req.Mode)
|
command += " -n " + tail
|
||||||
}
|
}
|
||||||
stdout, err := cmd.CombinedOutput()
|
if since != "all" {
|
||||||
|
command += " --since " + since
|
||||||
|
}
|
||||||
|
if follow {
|
||||||
|
command += " -f"
|
||||||
|
}
|
||||||
|
command += " 2>&1"
|
||||||
|
cmd := exec.Command("bash", "-c", command)
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.New(string(stdout))
|
return err
|
||||||
}
|
}
|
||||||
return string(stdout), nil
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := bufio.NewReader(stdout)
|
||||||
|
for {
|
||||||
|
bytes, err := reader.ReadBytes('\n')
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
global.LOG.Errorf("read bytes from container log failed, err: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = wsConn.WriteMessage(websocket.TextMessage, bytes); err != nil {
|
||||||
|
global.LOG.Errorf("send message with container log to ws failed, err: %v", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) ContainerStats(id string) (*dto.ContainterStats, error) {
|
func (u *ContainerService) ContainerStats(id string) (*dto.ContainterStats, error) {
|
||||||
|
@ -20,7 +20,7 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
|
|||||||
|
|
||||||
baRouter.POST("", baseApi.ContainerCreate)
|
baRouter.POST("", baseApi.ContainerCreate)
|
||||||
baRouter.POST("/search", baseApi.SearchContainer)
|
baRouter.POST("/search", baseApi.SearchContainer)
|
||||||
baRouter.POST("/search/log", baseApi.ContainerLogs)
|
baRouter.GET("/search/log", baseApi.ContainerLogs)
|
||||||
baRouter.POST("/clean/log", baseApi.CleanContainerLog)
|
baRouter.POST("/clean/log", baseApi.CleanContainerLog)
|
||||||
baRouter.POST("/inspect", baseApi.Inspect)
|
baRouter.POST("/inspect", baseApi.Inspect)
|
||||||
baRouter.POST("/operate", baseApi.ContainerOperation)
|
baRouter.POST("/operate", baseApi.ContainerOperation)
|
||||||
|
@ -473,6 +473,9 @@ const message = {
|
|||||||
container: 'Container',
|
container: 'Container',
|
||||||
upTime: 'UpTime',
|
upTime: 'UpTime',
|
||||||
all: 'All',
|
all: 'All',
|
||||||
|
fetch: 'Fetch',
|
||||||
|
lines: 'Lines',
|
||||||
|
linesHelper: 'Please enter the correct number of logs to retrieve!',
|
||||||
lastDay: 'Last Day',
|
lastDay: 'Last Day',
|
||||||
last4Hour: 'Last 4 Hours',
|
last4Hour: 'Last 4 Hours',
|
||||||
lastHour: 'Last Hour',
|
lastHour: 'Last Hour',
|
||||||
|
@ -480,6 +480,9 @@ const message = {
|
|||||||
container: '容器',
|
container: '容器',
|
||||||
upTime: '运行时长',
|
upTime: '运行时长',
|
||||||
all: '全部',
|
all: '全部',
|
||||||
|
fetch: '过滤',
|
||||||
|
lines: '条数',
|
||||||
|
linesHelper: '请输入正确的日志获取条数!',
|
||||||
lastDay: '最近一天',
|
lastDay: '最近一天',
|
||||||
last4Hour: '最近 4 小时',
|
last4Hour: '最近 4 小时',
|
||||||
lastHour: '最近 1 小时',
|
lastHour: '最近 1 小时',
|
||||||
|
@ -15,10 +15,23 @@
|
|||||||
</template>
|
</template>
|
||||||
<div>
|
<div>
|
||||||
<el-select @change="searchLogs" style="width: 30%; float: left" v-model="logSearch.mode">
|
<el-select @change="searchLogs" style="width: 30%; float: left" v-model="logSearch.mode">
|
||||||
|
<template #prefix>{{ $t('container.fetch') }}</template>
|
||||||
<el-option v-for="item in timeOptions" :key="item.label" :value="item.value" :label="item.label" />
|
<el-option v-for="item in timeOptions" :key="item.label" :value="item.value" :label="item.label" />
|
||||||
</el-select>
|
</el-select>
|
||||||
|
<el-input
|
||||||
|
@change="searchLogs"
|
||||||
|
class="margin-button"
|
||||||
|
style="width: 20%; float: left"
|
||||||
|
v-model.number="logSearch.tail"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<div style="margin-left: 2px">{{ $t('container.lines') }}</div>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
<div class="margin-button" style="float: left">
|
<div class="margin-button" style="float: left">
|
||||||
<el-checkbox border v-model="logSearch.isWatch">{{ $t('commons.button.watch') }}</el-checkbox>
|
<el-checkbox border @change="searchLogs" v-model="logSearch.isWatch">
|
||||||
|
{{ $t('commons.button.watch') }}
|
||||||
|
</el-checkbox>
|
||||||
</div>
|
</div>
|
||||||
<el-button class="margin-button" @click="onDownload" icon="Download">
|
<el-button class="margin-button" @click="onDownload" icon="Download">
|
||||||
{{ $t('file.download') }}
|
{{ $t('file.download') }}
|
||||||
@ -53,16 +66,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { cleanContainerLog, logContainer } from '@/api/modules/container';
|
import { cleanContainerLog } from '@/api/modules/container';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { dateFormatForName } from '@/utils/util';
|
import { dateFormatForName } from '@/utils/util';
|
||||||
import { nextTick, onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue';
|
import { onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue';
|
||||||
import { Codemirror } from 'vue-codemirror';
|
import { Codemirror } from 'vue-codemirror';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||||
import { ElMessageBox } from 'element-plus';
|
import { ElMessageBox } from 'element-plus';
|
||||||
import { MsgSuccess } from '@/utils/message';
|
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||||
import screenfull from 'screenfull';
|
import screenfull from 'screenfull';
|
||||||
import { GlobalStore } from '@/store';
|
import { GlobalStore } from '@/store';
|
||||||
|
|
||||||
@ -70,20 +83,21 @@ const extensions = [javascript(), oneDark];
|
|||||||
|
|
||||||
const logVisiable = ref(false);
|
const logVisiable = ref(false);
|
||||||
|
|
||||||
const logInfo = ref();
|
const logInfo = ref<string>('');
|
||||||
const view = shallowRef();
|
const view = shallowRef();
|
||||||
const handleReady = (payload) => {
|
const handleReady = (payload) => {
|
||||||
view.value = payload.view;
|
view.value = payload.view;
|
||||||
};
|
};
|
||||||
const globalStore = GlobalStore();
|
const globalStore = GlobalStore();
|
||||||
|
const terminalSocket = ref<WebSocket>();
|
||||||
|
|
||||||
const logSearch = reactive({
|
const logSearch = reactive({
|
||||||
isWatch: false,
|
isWatch: false,
|
||||||
container: '',
|
container: '',
|
||||||
containerID: '',
|
containerID: '',
|
||||||
mode: 'all',
|
mode: 'all',
|
||||||
|
tail: 100,
|
||||||
});
|
});
|
||||||
let timer: NodeJS.Timer | null = null;
|
|
||||||
|
|
||||||
const timeOptions = ref([
|
const timeOptions = ref([
|
||||||
{ label: i18n.global.t('container.all'), value: 'all' },
|
{ label: i18n.global.t('container.all'), value: 'all' },
|
||||||
@ -115,22 +129,32 @@ screenfull.on('change', () => {
|
|||||||
});
|
});
|
||||||
const handleClose = async () => {
|
const handleClose = async () => {
|
||||||
logVisiable.value = false;
|
logVisiable.value = false;
|
||||||
clearInterval(Number(timer));
|
terminalSocket.value.close();
|
||||||
timer = null;
|
|
||||||
};
|
};
|
||||||
watch(logVisiable, (val) => {
|
watch(logVisiable, (val) => {
|
||||||
if (screenfull.isEnabled && !val) screenfull.exit();
|
if (screenfull.isEnabled && !val) screenfull.exit();
|
||||||
});
|
});
|
||||||
const searchLogs = async () => {
|
const searchLogs = async () => {
|
||||||
const res = await logContainer(logSearch);
|
if (!Number(logSearch.tail) || Number(logSearch.tail) <= 0) {
|
||||||
logInfo.value = res.data || '';
|
MsgError(global.i18n.$t('container.linesHelper'));
|
||||||
nextTick(() => {
|
return;
|
||||||
|
}
|
||||||
|
terminalSocket.value?.close();
|
||||||
|
logInfo.value = '';
|
||||||
|
const href = window.location.href;
|
||||||
|
const protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss';
|
||||||
|
const host = href.split('//')[1].split('/')[0];
|
||||||
|
terminalSocket.value = new WebSocket(
|
||||||
|
`${protocol}://${host}/containers/search/log?container=${logSearch.containerID}&since=${logSearch.mode}&tail=${logSearch.tail}&follow=${logSearch.isWatch}`,
|
||||||
|
);
|
||||||
|
terminalSocket.value.onmessage = (event) => {
|
||||||
|
logInfo.value += event.data;
|
||||||
const state = view.value.state;
|
const state = view.value.state;
|
||||||
view.value.dispatch({
|
view.value.dispatch({
|
||||||
selection: { anchor: state.doc.length, head: state.doc.length },
|
selection: { anchor: state.doc.length, head: state.doc.length },
|
||||||
scrollIntoView: true,
|
scrollIntoView: true,
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDownload = async () => {
|
const onDownload = async () => {
|
||||||
@ -163,20 +187,15 @@ interface DialogProps {
|
|||||||
const acceptParams = (props: DialogProps): void => {
|
const acceptParams = (props: DialogProps): void => {
|
||||||
logVisiable.value = true;
|
logVisiable.value = true;
|
||||||
logSearch.containerID = props.containerID;
|
logSearch.containerID = props.containerID;
|
||||||
logSearch.mode = 'all';
|
logSearch.tail = 100;
|
||||||
|
logSearch.mode = '10m';
|
||||||
logSearch.isWatch = false;
|
logSearch.isWatch = false;
|
||||||
logSearch.container = props.container;
|
logSearch.container = props.container;
|
||||||
searchLogs();
|
searchLogs();
|
||||||
timer = setInterval(() => {
|
|
||||||
if (logSearch.isWatch) {
|
|
||||||
searchLogs();
|
|
||||||
}
|
|
||||||
}, 1000 * 5);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
clearInterval(Number(timer));
|
terminalSocket.value?.close();
|
||||||
timer = null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
Loading…
x
Reference in New Issue
Block a user