Skip to content

Commit 8808e1b

Browse files
authored
feat: 优化容器日志加载方式,增加显示条数过滤 (#1370)
1 parent 47dda4a commit 8808e1b

File tree

7 files changed

+95
-54
lines changed

7 files changed

+95
-54
lines changed

backend/app/api/v1/container.go

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -307,30 +307,23 @@ func (b *BaseApi) Inspect(c *gin.Context) {
307307
helper.SuccessWithData(c, result)
308308
}
309309

310-
// @Tags Container
311-
// @Summary Container logs
312-
// @Description 容器日志
313-
// @Accept json
314-
// @Param request body dto.ContainerLog true "request"
315-
// @Success 200 {string} logs
316-
// @Security ApiKeyAuth
317-
// @Router /containers/search/log [post]
318310
func (b *BaseApi) ContainerLogs(c *gin.Context) {
319-
var req dto.ContainerLog
320-
if err := c.ShouldBindJSON(&req); err != nil {
321-
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
322-
return
323-
}
324-
if err := global.VALID.Struct(req); err != nil {
325-
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
311+
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
312+
if err != nil {
313+
global.LOG.Errorf("gin context http handler failed, err: %v", err)
326314
return
327315
}
328-
logs, err := containerService.ContainerLogs(req)
329-
if err != nil {
330-
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
316+
defer wsConn.Close()
317+
318+
container := c.Query("container")
319+
since := c.Query("since")
320+
follow := c.Query("follow") == "true"
321+
tail := c.Query("tail")
322+
323+
if err := containerService.ContainerLogs(wsConn, container, since, tail, follow); err != nil {
324+
_ = wsConn.WriteMessage(1, []byte(err.Error()))
331325
return
332326
}
333-
helper.SuccessWithData(c, logs)
334327
}
335328

336329
// @Tags Container Network

backend/app/dto/container.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,6 @@ type PortHelper struct {
6969
Protocol string `json:"protocol"`
7070
}
7171

72-
type ContainerLog struct {
73-
ContainerID string `json:"containerID" validate:"required"`
74-
Mode string `json:"mode" validate:"required"`
75-
}
76-
7772
type ContainerOperation struct {
7873
Name string `json:"name" validate:"required"`
7974
Operation string `json:"operation" validate:"required,oneof=start stop restart kill pause unpause rename remove"`

backend/app/service/container.go

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package service
22

33
import (
4+
"bufio"
45
"context"
56
"encoding/json"
6-
"errors"
77
"fmt"
88
"io"
99
"os"
@@ -26,6 +26,7 @@ import (
2626
"github.com/docker/docker/api/types/network"
2727
"github.com/docker/docker/client"
2828
"github.com/docker/go-connections/nat"
29+
"github.com/gorilla/websocket"
2930
v1 "github.com/opencontainers/image-spec/specs-go/v1"
3031
)
3132

@@ -42,7 +43,7 @@ type IContainerService interface {
4243
ContainerCreate(req dto.ContainerCreate) error
4344
ContainerLogClean(req dto.OperationWithName) error
4445
ContainerOperation(req dto.ContainerOperation) error
45-
ContainerLogs(param dto.ContainerLog) (string, error)
46+
ContainerLogs(wsConn *websocket.Conn, container, since, tail string, follow bool) error
4647
ContainerStats(id string) (*dto.ContainterStats, error)
4748
Inspect(req dto.InspectReq) (string, error)
4849
DeleteNetwork(req dto.BatchDelete) error
@@ -332,16 +333,43 @@ func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
332333
return nil
333334
}
334335

335-
func (u *ContainerService) ContainerLogs(req dto.ContainerLog) (string, error) {
336-
cmd := exec.Command("docker", "logs", req.ContainerID)
337-
if req.Mode != "all" {
338-
cmd = exec.Command("docker", "logs", req.ContainerID, "--since", req.Mode)
336+
func (u *ContainerService) ContainerLogs(wsConn *websocket.Conn, container, since, tail string, follow bool) error {
337+
command := fmt.Sprintf("docker logs %s", container)
338+
if tail != "0" {
339+
command += " -n " + tail
339340
}
340-
stdout, err := cmd.CombinedOutput()
341+
if since != "all" {
342+
command += " --since " + since
343+
}
344+
if follow {
345+
command += " -f"
346+
}
347+
command += " 2>&1"
348+
cmd := exec.Command("bash", "-c", command)
349+
stdout, err := cmd.StdoutPipe()
341350
if err != nil {
342-
return "", errors.New(string(stdout))
351+
return err
352+
}
353+
if err := cmd.Start(); err != nil {
354+
return err
355+
}
356+
357+
reader := bufio.NewReader(stdout)
358+
for {
359+
bytes, err := reader.ReadBytes('\n')
360+
if err != nil {
361+
if err == io.EOF {
362+
break
363+
}
364+
global.LOG.Errorf("read bytes from container log failed, err: %v", err)
365+
continue
366+
}
367+
if err = wsConn.WriteMessage(websocket.TextMessage, bytes); err != nil {
368+
global.LOG.Errorf("send message with container log to ws failed, err: %v", err)
369+
break
370+
}
343371
}
344-
return string(stdout), nil
372+
return nil
345373
}
346374

347375
func (u *ContainerService) ContainerStats(id string) (*dto.ContainterStats, error) {

backend/router/ro_container.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
2020

2121
baRouter.POST("", baseApi.ContainerCreate)
2222
baRouter.POST("/search", baseApi.SearchContainer)
23-
baRouter.POST("/search/log", baseApi.ContainerLogs)
23+
baRouter.GET("/search/log", baseApi.ContainerLogs)
2424
baRouter.POST("/clean/log", baseApi.CleanContainerLog)
2525
baRouter.POST("/inspect", baseApi.Inspect)
2626
baRouter.POST("/operate", baseApi.ContainerOperation)

frontend/src/lang/modules/en.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,9 @@ const message = {
473473
container: 'Container',
474474
upTime: 'UpTime',
475475
all: 'All',
476+
fetch: 'Fetch',
477+
lines: 'Lines',
478+
linesHelper: 'Please enter the correct number of logs to retrieve!',
476479
lastDay: 'Last Day',
477480
last4Hour: 'Last 4 Hours',
478481
lastHour: 'Last Hour',

frontend/src/lang/modules/zh.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,9 @@ const message = {
480480
container: '容器',
481481
upTime: '运行时长',
482482
all: '全部',
483+
fetch: '过滤',
484+
lines: '条数',
485+
linesHelper: '请输入正确的日志获取条数!',
483486
lastDay: '最近一天',
484487
last4Hour: '最近 4 小时',
485488
lastHour: '最近 1 小时',

frontend/src/views/container/container/log/index.vue

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,23 @@
1515
</template>
1616
<div>
1717
<el-select @change="searchLogs" style="width: 30%; float: left" v-model="logSearch.mode">
18+
<template #prefix>{{ $t('container.fetch') }}</template>
1819
<el-option v-for="item in timeOptions" :key="item.label" :value="item.value" :label="item.label" />
1920
</el-select>
21+
<el-input
22+
@change="searchLogs"
23+
class="margin-button"
24+
style="width: 20%; float: left"
25+
v-model.number="logSearch.tail"
26+
>
27+
<template #prefix>
28+
<div style="margin-left: 2px">{{ $t('container.lines') }}</div>
29+
</template>
30+
</el-input>
2031
<div class="margin-button" style="float: left">
21-
<el-checkbox border v-model="logSearch.isWatch">{{ $t('commons.button.watch') }}</el-checkbox>
32+
<el-checkbox border @change="searchLogs" v-model="logSearch.isWatch">
33+
{{ $t('commons.button.watch') }}
34+
</el-checkbox>
2235
</div>
2336
<el-button class="margin-button" @click="onDownload" icon="Download">
2437
{{ $t('file.download') }}
@@ -53,37 +66,38 @@
5366
</template>
5467

5568
<script lang="ts" setup>
56-
import { cleanContainerLog, logContainer } from '@/api/modules/container';
69+
import { cleanContainerLog } from '@/api/modules/container';
5770
import i18n from '@/lang';
5871
import { dateFormatForName } from '@/utils/util';
59-
import { nextTick, onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue';
72+
import { onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue';
6073
import { Codemirror } from 'vue-codemirror';
6174
import { javascript } from '@codemirror/lang-javascript';
6275
import { oneDark } from '@codemirror/theme-one-dark';
6376
import DrawerHeader from '@/components/drawer-header/index.vue';
6477
import { ElMessageBox } from 'element-plus';
65-
import { MsgSuccess } from '@/utils/message';
78+
import { MsgError, MsgSuccess } from '@/utils/message';
6679
import screenfull from 'screenfull';
6780
import { GlobalStore } from '@/store';
6881
6982
const extensions = [javascript(), oneDark];
7083
7184
const logVisiable = ref(false);
7285
73-
const logInfo = ref();
86+
const logInfo = ref<string>('');
7487
const view = shallowRef();
7588
const handleReady = (payload) => {
7689
view.value = payload.view;
7790
};
7891
const globalStore = GlobalStore();
92+
const terminalSocket = ref<WebSocket>();
7993
8094
const logSearch = reactive({
8195
isWatch: false,
8296
container: '',
8397
containerID: '',
8498
mode: 'all',
99+
tail: 100,
85100
});
86-
let timer: NodeJS.Timer | null = null;
87101
88102
const timeOptions = ref([
89103
{ label: i18n.global.t('container.all'), value: 'all' },
@@ -115,22 +129,32 @@ screenfull.on('change', () => {
115129
});
116130
const handleClose = async () => {
117131
logVisiable.value = false;
118-
clearInterval(Number(timer));
119-
timer = null;
132+
terminalSocket.value.close();
120133
};
121134
watch(logVisiable, (val) => {
122135
if (screenfull.isEnabled && !val) screenfull.exit();
123136
});
124137
const searchLogs = async () => {
125-
const res = await logContainer(logSearch);
126-
logInfo.value = res.data || '';
127-
nextTick(() => {
138+
if (!Number(logSearch.tail) || Number(logSearch.tail) <= 0) {
139+
MsgError(global.i18n.$t('container.linesHelper'));
140+
return;
141+
}
142+
terminalSocket.value?.close();
143+
logInfo.value = '';
144+
const href = window.location.href;
145+
const protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss';
146+
const host = href.split('//')[1].split('/')[0];
147+
terminalSocket.value = new WebSocket(
148+
`${protocol}://${host}/containers/search/log?container=${logSearch.containerID}&since=${logSearch.mode}&tail=${logSearch.tail}&follow=${logSearch.isWatch}`,
149+
);
150+
terminalSocket.value.onmessage = (event) => {
151+
logInfo.value += event.data;
128152
const state = view.value.state;
129153
view.value.dispatch({
130154
selection: { anchor: state.doc.length, head: state.doc.length },
131155
scrollIntoView: true,
132156
});
133-
});
157+
};
134158
};
135159
136160
const onDownload = async () => {
@@ -163,20 +187,15 @@ interface DialogProps {
163187
const acceptParams = (props: DialogProps): void => {
164188
logVisiable.value = true;
165189
logSearch.containerID = props.containerID;
166-
logSearch.mode = 'all';
190+
logSearch.tail = 100;
191+
logSearch.mode = '10m';
167192
logSearch.isWatch = false;
168193
logSearch.container = props.container;
169194
searchLogs();
170-
timer = setInterval(() => {
171-
if (logSearch.isWatch) {
172-
searchLogs();
173-
}
174-
}, 1000 * 5);
175195
};
176196
177197
onBeforeUnmount(() => {
178-
clearInterval(Number(timer));
179-
timer = null;
198+
terminalSocket.value?.close();
180199
});
181200
182201
defineExpose({

0 commit comments

Comments
 (0)