1
0
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:
ssongliu 2023-06-13 23:04:12 +08:00 committed by GitHub
parent 47dda4ac9f
commit 8808e1b0c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 96 additions and 55 deletions

View File

@ -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

View File

@ -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"`

View File

@ -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) {

View File

@ -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)

View File

@ -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',

View File

@ -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 小时',

View File

@ -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({