mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 08:19:15 +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/1Panel-dev/1Panel/backend/global"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @Tags Container
|
// @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
|
// @Tags Container Network
|
||||||
// @Summary Page networks
|
// @Summary Page networks
|
||||||
// @Description 获取容器网络列表分页
|
// @Description 获取容器网络列表分页
|
||||||
|
@ -225,3 +225,10 @@ type ComposeUpdate struct {
|
|||||||
Path string `json:"path" validate:"required"`
|
Path string `json:"path" validate:"required"`
|
||||||
Content string `json:"content" 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
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -64,6 +68,7 @@ type IContainerService interface {
|
|||||||
ContainerLogClean(req dto.OperationWithName) error
|
ContainerLogClean(req dto.OperationWithName) error
|
||||||
ContainerOperation(req dto.ContainerOperation) error
|
ContainerOperation(req dto.ContainerOperation) error
|
||||||
ContainerLogs(wsConn *websocket.Conn, containerType, container, since, tail string, follow bool) 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)
|
ContainerStats(id string) (*dto.ContainerStats, error)
|
||||||
Inspect(req dto.InspectReq) (string, error)
|
Inspect(req dto.InspectReq) (string, error)
|
||||||
DeleteNetwork(req dto.BatchDelete) error
|
DeleteNetwork(req dto.BatchDelete) error
|
||||||
@ -769,6 +774,79 @@ func (u *ContainerService) ContainerLogs(wsConn *websocket.Conn, containerType,
|
|||||||
return nil
|
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) {
|
func (u *ContainerService) ContainerStats(id string) (*dto.ContainerStats, error) {
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -26,6 +26,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
|
|||||||
baRouter.POST("/list", baseApi.ListContainer)
|
baRouter.POST("/list", baseApi.ListContainer)
|
||||||
baRouter.GET("/list/stats", baseApi.ContainerListStats)
|
baRouter.GET("/list/stats", baseApi.ContainerListStats)
|
||||||
baRouter.GET("/search/log", baseApi.ContainerLogs)
|
baRouter.GET("/search/log", baseApi.ContainerLogs)
|
||||||
|
baRouter.POST("/download/log", baseApi.DownloadContainerLogs)
|
||||||
baRouter.GET("/limit", baseApi.LoadResourceLimit)
|
baRouter.GET("/limit", baseApi.LoadResourceLimit)
|
||||||
baRouter.POST("/clean/log", baseApi.CleanContainerLog)
|
baRouter.POST("/clean/log", baseApi.CleanContainerLog)
|
||||||
baRouter.POST("/load/log", baseApi.LoadContainerLog)
|
baRouter.POST("/load/log", baseApi.LoadContainerLog)
|
||||||
|
@ -316,4 +316,11 @@ export namespace Container {
|
|||||||
logMaxSize: string;
|
logMaxSize: string;
|
||||||
logMaxFile: 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);
|
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
|
// image
|
||||||
export const searchImage = (params: SearchWithPage) => {
|
export const searchImage = (params: SearchWithPage) => {
|
||||||
return http.post<ResPage<Container.ImageInfo>>(`/containers/image/search`, params);
|
return http.post<ResPage<Container.ImageInfo>>(`/containers/image/search`, params);
|
||||||
|
@ -58,7 +58,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import i18n from '@/lang';
|
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 { computed, 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';
|
||||||
@ -66,6 +66,7 @@ import { oneDark } from '@codemirror/theme-one-dark';
|
|||||||
import { MsgError } from '@/utils/message';
|
import { MsgError } from '@/utils/message';
|
||||||
import { GlobalStore } from '@/store';
|
import { GlobalStore } from '@/store';
|
||||||
import screenfull from 'screenfull';
|
import screenfull from 'screenfull';
|
||||||
|
import { DownloadFile } from '@/api/modules/container';
|
||||||
|
|
||||||
const extensions = [javascript(), oneDark];
|
const extensions = [javascript(), oneDark];
|
||||||
|
|
||||||
@ -163,7 +164,23 @@ const onDownload = async () => {
|
|||||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||||
type: 'info',
|
type: 'info',
|
||||||
}).then(async () => {
|
}).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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { cleanContainerLog } from '@/api/modules/container';
|
import { cleanContainerLog, DownloadFile } from '@/api/modules/container';
|
||||||
import i18n from '@/lang';
|
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 { computed, 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';
|
||||||
@ -173,7 +173,23 @@ const onDownload = async () => {
|
|||||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||||
type: 'info',
|
type: 'info',
|
||||||
}).then(async () => {
|
}).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…
x
Reference in New Issue
Block a user