fix: Fix the issue of container compose log cleanup failure

This commit is contained in:
ssongliu 2025-12-08 11:33:44 +08:00
parent f9a20ec443
commit a59d8ff2bd
8 changed files with 151 additions and 18 deletions

View file

@ -393,6 +393,28 @@ func (b *BaseApi) CleanContainerLog(c *gin.Context) {
helper.Success(c)
}
// @Tags Container
// @Summary Clean compose log
// @Accept json
// @Param request body dto.ComposeLogClean true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /containers/compose/clean/log [post]
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清理容器编排 [name] 日志","formatEN":"clean compose [name] logs"}
func (b *BaseApi) CleanComposeLog(c *gin.Context) {
var req dto.ComposeLogClean
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := containerService.ComposeLogClean(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Container
// @Summary Rename Container
// @Accept json

View file

@ -291,6 +291,10 @@ type ComposeUpdate struct {
Content string `json:"content" validate:"required"`
Env []string `json:"env"`
}
type ComposeLogClean struct {
Name string `json:"name" validate:"required"`
Path string `json:"path" validate:"required"`
}
type ContainerLog struct {
Container string `json:"container" validate:"required"`

View file

@ -85,6 +85,7 @@ type IContainerService interface {
CreateVolume(req dto.VolumeCreate) error
TestCompose(req dto.ComposeCreate) (bool, error)
ComposeUpdate(req dto.ComposeUpdate) error
ComposeLogClean(req dto.ComposeLogClean) error
Prune(req dto.ContainerPrune) error
LoadUsers(req dto.OperationWithName) []string

View file

@ -6,6 +6,7 @@ import (
"fmt"
"os"
"path"
"path/filepath"
"sort"
"strings"
"time"
@ -284,6 +285,57 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
return nil
}
func (u *ContainerService) ComposeLogClean(req dto.ComposeLogClean) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
defer client.Close()
options := container.ListOptions{All: true}
options.Filters = filters.NewArgs()
options.Filters.Add("label", composeProjectLabel)
list, err := client.ContainerList(context.Background(), options)
if err != nil {
return err
}
ctx := context.Background()
for _, item := range list {
if name, ok := item.Labels[composeProjectLabel]; ok {
if name != req.Name {
continue
}
containerItem, err := client.ContainerInspect(ctx, item.ID)
if err != nil {
return err
}
if err := client.ContainerStop(ctx, containerItem.ID, container.StopOptions{}); err != nil {
return err
}
file, err := os.OpenFile(containerItem.LogPath, os.O_RDWR|os.O_CREATE, constant.FilePerm)
if err != nil {
return err
}
defer file.Close()
if err = file.Truncate(0); err != nil {
return err
}
_, _ = file.Seek(0, 0)
files, _ := filepath.Glob(fmt.Sprintf("%s.*", containerItem.LogPath))
for _, file := range files {
_ = os.Remove(file)
}
}
}
return u.ComposeOperation(dto.ComposeOperation{
Name: req.Name,
Path: req.Path,
Operation: "restart",
})
}
func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
if req.From == "template" || req.From == "edit" {
dir := fmt.Sprintf("%s/docker/compose/%s", global.Dir.DataDir, req.Name)

View file

@ -48,6 +48,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
baRouter.POST("/compose", baseApi.CreateCompose)
baRouter.POST("/compose/test", baseApi.TestCompose)
baRouter.POST("/compose/operate", baseApi.OperatorCompose)
baRouter.POST("/compose/clean/log", baseApi.CleanComposeLog)
baRouter.POST("/compose/update", baseApi.ComposeUpdate)
baRouter.GET("/template", baseApi.ListComposeTemplate)

View file

@ -39,9 +39,17 @@ export const commitContainer = (params: Container.ContainerCommit) => {
export const loadContainerInfo = (name: string) => {
return http.post<Container.ContainerHelper>(`/containers/info`, { name: name });
};
export const cleanComposeLog = (composeName: string, composePath: string, operateNode?: string) => {
const params = operateNode ? `?operateNode=${operateNode}` : '';
return http.post(
`/containers/compose/clean/log${params}`,
{ name: composeName, path: composePath },
TimeoutEnum.T_60S,
);
};
export const cleanContainerLog = (containerName: string, operateNode?: string) => {
const params = operateNode ? `?operateNode=${operateNode}` : '';
return http.post(`/containers/clean/log${params}`, { name: containerName });
return http.post(`/containers/clean/log${params}`, { name: containerName }, TimeoutEnum.T_60S);
};
export const containerItemStats = (containerID: string) => {
return http.post<Container.ContainerItemStats>(`/containers/item/stats`, { name: containerID }, TimeoutEnum.T_60S);

View file

@ -38,7 +38,7 @@
</template>
<script lang="ts" setup>
import { cleanContainerLog, DownloadFile } from '@/api/modules/container';
import { cleanComposeLog, cleanContainerLog, DownloadFile } from '@/api/modules/container';
import i18n from '@/lang';
import { dateFormatForName } from '@/utils/util';
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
@ -48,6 +48,8 @@ import hightlight from '@/components/log/custom-hightlight/index.vue';
import { GlobalStore } from '@/store';
const globalStore = GlobalStore();
const em = defineEmits(['update:loading']);
const props = defineProps({
container: {
type: String,
@ -93,6 +95,7 @@ const logSearch = reactive({
mode: 'all',
tail: props.defaultFollow ? 0 : 100,
compose: '',
resource: '',
});
const logHeight = 20;
const logCount = computed(() => logs.value.length);
@ -215,6 +218,19 @@ const onClean = async () => {
if (props.node && props.node !== '') {
currentNode = props.node;
}
if (logSearch.compose !== '') {
em('update:loading', true);
await cleanComposeLog(logSearch.resource, logSearch.compose, currentNode)
.then(() => {
em('update:loading', false);
searchLogs();
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.finally(() => {
em('update:loading', false);
});
return;
}
await cleanContainerLog(logSearch.container, currentNode);
searchLogs();
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
@ -240,6 +256,7 @@ const resizeObserver = ref<ResizeObserver | null>(null);
onMounted(() => {
logSearch.container = props.container;
logSearch.compose = props.compose;
logSearch.resource = props.resource;
logVisible.value = true;
logSearch.tail = 100;

View file

@ -97,13 +97,7 @@
>
{{ $t('commons.operate.restart') }}
</el-button>
<el-button
:disabled="row.createdBy !== '1Panel'"
plain
round
size="small"
@click="onDelete(row)"
>
<el-button plain round size="small" @click="onDelete(row)">
{{ $t('commons.operate.delete') }}
</el-button>
</div>
@ -174,8 +168,7 @@
<el-descriptions-item :label="$t('container.memTotal')">
{{ computeSizeForDocker(row.memoryLimit) }}
</el-descriptions-item>
<el-descriptions-item>
<el-descriptions-item v-if="row.hasLoadSize">
<template #label>
{{ $t('container.sizeRw') }}
<el-tooltip :content="$t('container.sizeRwHelper')">
@ -184,7 +177,10 @@
</template>
{{ computeSize2(row.sizeRw) }}
</el-descriptions-item>
<el-descriptions-item :label="$t('container.sizeRootFs')">
<el-descriptions-item
:label="$t('container.sizeRootFs')"
v-if="row.hasLoadSize"
>
<template #label>
{{ $t('container.sizeRootFs') }}
<el-tooltip :content="$t('container.sizeRootFsHelper')">
@ -194,6 +190,17 @@
{{ computeSize2(row.sizeRootFs) }}
</el-descriptions-item>
</el-descriptions>
<el-button
class="mt-2"
v-if="!row.hasLoadSize"
size="small"
link
type="primary"
@click="loadSize(row)"
>
{{ $t('container.loadSize') }}
</el-button>
</template>
</el-popover>
</div>
@ -252,8 +259,10 @@
<div v-show="showType === 'log'">
<ContainerLog
v-model:loading="detailLoading"
:key="currentCompose.path"
:compose="currentCompose.path"
:resource="currentCompose.name"
:highlightDiff="450"
:defaultFollow="true"
/>
@ -282,7 +291,14 @@ import TerminalDialog from '@/views/container/container/terminal/index.vue';
import ContainerLogDialog from '@/components/log/container-drawer/index.vue';
import CreateDialog from '@/views/container/compose/create/index.vue';
import DeleteDialog from '@/views/container/compose/delete/index.vue';
import { composeOperator, composeUpdate, containerListStats, inspect, searchCompose } from '@/api/modules/container';
import {
composeOperator,
composeUpdate,
containerItemStats,
containerListStats,
inspect,
searchCompose,
} from '@/api/modules/container';
import DockerStatus from '@/views/container/docker-status/index.vue';
import i18n from '@/lang';
import { Container } from '@/api/interface/container';
@ -381,11 +397,15 @@ const loadDetail = async (row: Container.ComposeInfo, withRefresh: boolean) => {
detailLoading.value = true;
currentCompose.value = row;
composeContainers.value = row.containers || [];
await inspect({ id: currentCompose.value.name, type: 'compose' }).then((res) => {
composeContent.value = res.data;
detailLoading.value = false;
});
loadContainerStats();
await inspect({ id: currentCompose.value.name, type: 'compose' })
.then((res) => {
composeContent.value = res.data;
detailLoading.value = false;
})
.finally(() => {
loadContainerStats();
detailLoading.value = false;
});
};
const loadContainerStats = async () => {
@ -440,6 +460,14 @@ const handleComposeOperate = async (operation: 'up' | 'stop' | 'restart', row: a
});
};
const loadSize = async (row: any) => {
containerItemStats(row.containerID).then((res) => {
row.sizeRw = res.data.sizeRw || 0;
row.sizeRootFs = res.data.sizeRootFs || 0;
row.hasLoadSize = true;
});
};
const onSubmitEdit = async () => {
const param = {
name: currentCompose.value.name,