Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 12 additions & 19 deletions backend/app/api/v1/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,30 +307,23 @@ func (b *BaseApi) Inspect(c *gin.Context) {
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) {
var req dto.ContainerLog
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)
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
global.LOG.Errorf("gin context http handler failed, err: %v", err)
return
}
logs, err := containerService.ContainerLogs(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
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
}
helper.SuccessWithData(c, logs)
}

// @Tags Container Network
Expand Down
5 changes: 0 additions & 5 deletions backend/app/dto/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,6 @@ type PortHelper struct {
Protocol string `json:"protocol"`
}

type ContainerLog struct {
ContainerID string `json:"containerID" validate:"required"`
Mode string `json:"mode" validate:"required"`
}

type ContainerOperation struct {
Name string `json:"name" validate:"required"`
Operation string `json:"operation" validate:"required,oneof=start stop restart kill pause unpause rename remove"`
Expand Down
46 changes: 37 additions & 9 deletions backend/app/service/container.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package service

import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
Expand All @@ -26,6 +26,7 @@ import (
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"github.com/gorilla/websocket"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)

Expand All @@ -42,7 +43,7 @@ type IContainerService interface {
ContainerCreate(req dto.ContainerCreate) error
ContainerLogClean(req dto.OperationWithName) 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)
Inspect(req dto.InspectReq) (string, error)
DeleteNetwork(req dto.BatchDelete) error
Expand Down Expand Up @@ -332,16 +333,43 @@ func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
return nil
}

func (u *ContainerService) ContainerLogs(req dto.ContainerLog) (string, error) {
cmd := exec.Command("docker", "logs", req.ContainerID)
if req.Mode != "all" {
cmd = exec.Command("docker", "logs", req.ContainerID, "--since", req.Mode)
func (u *ContainerService) ContainerLogs(wsConn *websocket.Conn, container, since, tail string, follow bool) error {
command := fmt.Sprintf("docker logs %s", container)
if tail != "0" {
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 {
return "", errors.New(string(stdout))
return err
}
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 string(stdout), nil
return nil
}

func (u *ContainerService) ContainerStats(id string) (*dto.ContainterStats, error) {
Expand Down
2 changes: 1 addition & 1 deletion backend/router/ro_container.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {

baRouter.POST("", baseApi.ContainerCreate)
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("/inspect", baseApi.Inspect)
baRouter.POST("/operate", baseApi.ContainerOperation)
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lang/modules/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,9 @@ const message = {
container: 'Container',
upTime: 'UpTime',
all: 'All',
fetch: 'Fetch',
lines: 'Lines',
linesHelper: 'Please enter the correct number of logs to retrieve!',
lastDay: 'Last Day',
last4Hour: 'Last 4 Hours',
lastHour: 'Last Hour',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lang/modules/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,9 @@ const message = {
container: '容器',
upTime: '运行时长',
all: '全部',
fetch: '过滤',
lines: '条数',
linesHelper: '请输入正确的日志获取条数!',
lastDay: '最近一天',
last4Hour: '最近 4 小时',
lastHour: '最近 1 小时',
Expand Down
59 changes: 39 additions & 20 deletions frontend/src/views/container/container/log/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,23 @@
</template>
<div>
<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-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">
<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>
<el-button class="margin-button" @click="onDownload" icon="Download">
{{ $t('file.download') }}
Expand Down Expand Up @@ -53,37 +66,38 @@
</template>

<script lang="ts" setup>
import { cleanContainerLog, logContainer } from '@/api/modules/container';
import { cleanContainerLog } from '@/api/modules/container';
import i18n from '@/lang';
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 { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { ElMessageBox } from 'element-plus';
import { MsgSuccess } from '@/utils/message';
import { MsgError, MsgSuccess } from '@/utils/message';
import screenfull from 'screenfull';
import { GlobalStore } from '@/store';

const extensions = [javascript(), oneDark];

const logVisiable = ref(false);

const logInfo = ref();
const logInfo = ref<string>('');
const view = shallowRef();
const handleReady = (payload) => {
view.value = payload.view;
};
const globalStore = GlobalStore();
const terminalSocket = ref<WebSocket>();

const logSearch = reactive({
isWatch: false,
container: '',
containerID: '',
mode: 'all',
tail: 100,
});
let timer: NodeJS.Timer | null = null;

const timeOptions = ref([
{ label: i18n.global.t('container.all'), value: 'all' },
Expand Down Expand Up @@ -115,22 +129,32 @@ screenfull.on('change', () => {
});
const handleClose = async () => {
logVisiable.value = false;
clearInterval(Number(timer));
timer = null;
terminalSocket.value.close();
};
watch(logVisiable, (val) => {
if (screenfull.isEnabled && !val) screenfull.exit();
});
const searchLogs = async () => {
const res = await logContainer(logSearch);
logInfo.value = res.data || '';
nextTick(() => {
if (!Number(logSearch.tail) || Number(logSearch.tail) <= 0) {
MsgError(global.i18n.$t('container.linesHelper'));
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;
view.value.dispatch({
selection: { anchor: state.doc.length, head: state.doc.length },
scrollIntoView: true,
});
});
};
};

const onDownload = async () => {
Expand Down Expand Up @@ -163,20 +187,15 @@ interface DialogProps {
const acceptParams = (props: DialogProps): void => {
logVisiable.value = true;
logSearch.containerID = props.containerID;
logSearch.mode = 'all';
logSearch.tail = 100;
logSearch.mode = '10m';
logSearch.isWatch = false;
logSearch.container = props.container;
searchLogs();
timer = setInterval(() => {
if (logSearch.isWatch) {
searchLogs();
}
}, 1000 * 5);
};

onBeforeUnmount(() => {
clearInterval(Number(timer));
timer = null;
terminalSocket.value?.close();
});

defineExpose({
Expand Down