mirror of
https://gitee.com/fit2cloud-feizhiyun/1Panel.git
synced 2024-12-05 21:38:26 +08:00
feat: 容器日志下载优化 (#5557)
* feat: 容器日志下载优化 Co-authored-by: zhoujunhong <1298308460@qq.com>
This commit is contained in:
parent
8fedb04c95
commit
0886bcd310
@ -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 获取容器网络列表分页
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -316,4 +316,11 @@ export namespace Container {
|
||||
logMaxSize: string;
|
||||
logMaxFile: string;
|
||||
}
|
||||
|
||||
export interface ContainerLogInfo {
|
||||
container: string;
|
||||
since: string;
|
||||
tail: number;
|
||||
containerType: string;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user