feat: 容器日志下载优化 (#5557)

* feat: 容器日志下载优化

Co-authored-by: zhoujunhong <1298308460@qq.com>
This commit is contained in:
John Bro 2024-06-25 18:17:15 +08:00 committed by GitHub
parent 8fedb04c95
commit 0886bcd310
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 152 additions and 5 deletions

View File

@ -7,6 +7,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"strconv"
)
// @Tags Container
@ -460,6 +461,19 @@ func (b *BaseApi) ContainerLogs(c *gin.Context) {
}
}
// @Description 下载容器日志
// @Router /containers/download/log [post]
func (b *BaseApi) DownloadContainerLogs(c *gin.Context) {
var req dto.ContainerLog
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := containerService.DownloadContainerLogs(req.ContainerType, req.Container, req.Since, strconv.Itoa(int(req.Tail)), c)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
}
}
// @Tags Container Network
// @Summary Page networks
// @Description 获取容器网络列表分页

View File

@ -225,3 +225,10 @@ type ComposeUpdate struct {
Path string `json:"path" validate:"required"`
Content string `json:"content" validate:"required"`
}
type ContainerLog struct {
Container string `json:"container" validate:"required"`
Since string `json:"since"`
Tail uint `json:"tail"`
ContainerType string `json:"containerType"`
}

View File

@ -1,11 +1,15 @@
package service
import (
"bufio"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
@ -64,6 +68,7 @@ type IContainerService interface {
ContainerLogClean(req dto.OperationWithName) error
ContainerOperation(req dto.ContainerOperation) error
ContainerLogs(wsConn *websocket.Conn, containerType, container, since, tail string, follow bool) error
DownloadContainerLogs(containerType, container, since, tail string, c *gin.Context) error
ContainerStats(id string) (*dto.ContainerStats, error)
Inspect(req dto.InspectReq) (string, error)
DeleteNetwork(req dto.BatchDelete) error
@ -769,6 +774,79 @@ func (u *ContainerService) ContainerLogs(wsConn *websocket.Conn, containerType,
return nil
}
func (u *ContainerService) DownloadContainerLogs(containerType, container, since, tail string, c *gin.Context) error {
if cmd.CheckIllegal(container, since, tail) {
return buserr.New(constant.ErrCmdIllegal)
}
commandName := "docker"
commandArg := []string{"logs", container}
if containerType == "compose" {
commandName = "docker-compose"
commandArg = []string{"-f", container, "logs"}
}
if tail != "0" {
commandArg = append(commandArg, "--tail")
commandArg = append(commandArg, tail)
}
if since != "all" {
commandArg = append(commandArg, "--since")
commandArg = append(commandArg, since)
}
cmd := exec.Command(commandName, commandArg...)
stdout, err := cmd.StdoutPipe()
if err != nil {
_ = cmd.Process.Signal(syscall.SIGTERM)
return err
}
cmd.Stderr = cmd.Stdout
if err := cmd.Start(); err != nil {
_ = cmd.Process.Signal(syscall.SIGTERM)
return err
}
tempFile, err := os.CreateTemp("", "cmd_output_*.txt")
if err != nil {
return err
}
defer tempFile.Close()
defer func() {
if err := os.Remove(tempFile.Name()); err != nil {
global.LOG.Errorf("os.Remove() failed: %v", err)
}
}()
errCh := make(chan error)
go func() {
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
line := scanner.Text()
if _, err := tempFile.WriteString(line + "\n"); err != nil {
errCh <- err
return
}
}
if err := scanner.Err(); err != nil {
errCh <- err
return
}
errCh <- nil
}()
select {
case err := <-errCh:
if err != nil {
global.LOG.Errorf("Error: %v", err)
}
case <-time.After(3 * time.Second):
global.LOG.Errorf("Timeout reached")
}
info, _ := tempFile.Stat()
c.Header("Content-Length", strconv.FormatInt(info.Size(), 10))
c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name()))
http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), tempFile)
return nil
}
func (u *ContainerService) ContainerStats(id string) (*dto.ContainerStats, error) {
client, err := docker.NewDockerClient()
if err != nil {

View File

@ -26,6 +26,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
baRouter.POST("/list", baseApi.ListContainer)
baRouter.GET("/list/stats", baseApi.ContainerListStats)
baRouter.GET("/search/log", baseApi.ContainerLogs)
baRouter.POST("/download/log", baseApi.DownloadContainerLogs)
baRouter.GET("/limit", baseApi.LoadResourceLimit)
baRouter.POST("/clean/log", baseApi.CleanContainerLog)
baRouter.POST("/load/log", baseApi.LoadContainerLog)

View File

@ -316,4 +316,11 @@ export namespace Container {
logMaxSize: string;
logMaxFile: string;
}
export interface ContainerLogInfo {
container: string;
since: string;
tail: number;
containerType: string;
}
}

View File

@ -52,6 +52,13 @@ export const inspect = (params: Container.ContainerInspect) => {
return http.post<string>(`/containers/inspect`, params);
};
export const DownloadFile = (params: Container.ContainerLogInfo) => {
return http.download<BlobPart>('/containers/download/log', params, {
responseType: 'blob',
timeout: TimeoutEnum.T_40S,
});
};
// image
export const searchImage = (params: SearchWithPage) => {
return http.post<ResPage<Container.ImageInfo>>(`/containers/image/search`, params);

View File

@ -58,7 +58,7 @@
<script lang="ts" setup>
import i18n from '@/lang';
import { dateFormatForName, downloadWithContent } from '@/utils/util';
import { dateFormatForName } from '@/utils/util';
import { computed, onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue';
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
@ -66,6 +66,7 @@ import { oneDark } from '@codemirror/theme-one-dark';
import { MsgError } from '@/utils/message';
import { GlobalStore } from '@/store';
import screenfull from 'screenfull';
import { DownloadFile } from '@/api/modules/container';
const extensions = [javascript(), oneDark];
@ -163,7 +164,23 @@ const onDownload = async () => {
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
downloadWithContent(logInfo.value, resource.value + '-' + dateFormatForName(new Date()) + '.log');
let params = {
container: logSearch.compose,
since: logSearch.mode,
tail: logSearch.tail,
containerType: 'compose',
};
let addItem = {};
addItem['name'] = logSearch.compose + '-' + dateFormatForName(new Date()) + '.log';
DownloadFile(params).then((res) => {
const downloadUrl = window.URL.createObjectURL(new Blob([res]));
const a = document.createElement('a');
a.style.display = 'none';
a.href = downloadUrl;
a.download = addItem['name'];
const event = new MouseEvent('click');
a.dispatchEvent(event);
});
});
};

View File

@ -68,9 +68,9 @@
</template>
<script lang="ts" setup>
import { cleanContainerLog } from '@/api/modules/container';
import { cleanContainerLog, DownloadFile } from '@/api/modules/container';
import i18n from '@/lang';
import { dateFormatForName, downloadWithContent } from '@/utils/util';
import { dateFormatForName } from '@/utils/util';
import { computed, onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue';
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
@ -173,7 +173,23 @@ const onDownload = async () => {
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
downloadWithContent(logInfo.value, logSearch.container + '-' + dateFormatForName(new Date()) + '.log');
let params = {
container: logSearch.containerID,
since: logSearch.mode,
tail: logSearch.tail,
containerType: 'container',
};
let addItem = {};
addItem['name'] = logSearch.container + '-' + dateFormatForName(new Date()) + '.log';
DownloadFile(params).then((res) => {
const downloadUrl = window.URL.createObjectURL(new Blob([res]));
const a = document.createElement('a');
a.style.display = 'none';
a.href = downloadUrl;
a.download = addItem['name'];
const event = new MouseEvent('click');
a.dispatchEvent(event);
});
});
};