From 1e5181c88e2d5797182f8379a0da6d6757bb652d Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:48:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=9D=A2=E6=9D=BF=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E7=AE=B1=E6=96=B0=E5=A2=9E=E6=97=A5=E5=BF=97=E6=B8=85=E7=90=86?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=20(#1228)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * feat: 添加日志清理工具 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * feat: 完成日志清理工具实现 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * feat: 优化日志清理 * feat: 添加 Podman 支持并修复 MySQL 日志清理 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * feat: 优化日志清理 * fix: lint --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> Co-authored-by: 耗子 --- cmd/ace/wire_gen.go | 3 +- go.sum | 7 + internal/http/request/toolbox_log.go | 6 + internal/route/http.go | 8 + internal/service/service.go | 1 + internal/service/toolbox_log.go | 652 +++++++++++++++++++++++++ web/src/api/panel/toolbox-log/index.ts | 8 + web/src/views/toolbox/IndexView.vue | 3 + web/src/views/toolbox/LogView.vue | 227 +++++++++ 9 files changed, 914 insertions(+), 1 deletion(-) create mode 100644 internal/http/request/toolbox_log.go create mode 100644 internal/service/toolbox_log.go create mode 100644 web/src/api/panel/toolbox-log/index.ts create mode 100644 web/src/views/toolbox/LogView.vue diff --git a/cmd/ace/wire_gen.go b/cmd/ace/wire_gen.go index 5b4b858f..05b5556f 100644 --- a/cmd/ace/wire_gen.go +++ b/cmd/ace/wire_gen.go @@ -122,6 +122,7 @@ func initWeb() (*app.Web, error) { toolboxBenchmarkService := service.NewToolboxBenchmarkService(locale) toolboxSSHService := service.NewToolboxSSHService(locale) toolboxDiskService := service.NewToolboxDiskService(locale) + toolboxLogService := service.NewToolboxLogService(locale, db, containerImageRepo, settingRepo) webHookRepo := data.NewWebHookRepo(locale, db) webHookService := service.NewWebHookService(webHookRepo) codeserverApp := codeserver.NewApp() @@ -145,7 +146,7 @@ func initWeb() (*app.Web, error) { s3fsApp := s3fs.NewApp(locale) supervisorApp := supervisor.NewApp(locale) loader := bootstrap.NewLoader(codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp) - http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, projectService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentPHPService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, webHookService, loader) + http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, projectService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentPHPService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, toolboxLogService, webHookService, loader) wsService := service.NewWsService(locale, config, logger, sshRepo) ws := route.NewWs(wsService) mux, err := bootstrap.NewRouter(locale, middlewares, http, ws) diff --git a/go.sum b/go.sum index ac1748f3..72e8812c 100644 --- a/go.sum +++ b/go.sum @@ -122,6 +122,8 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -271,6 +273,7 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= @@ -379,6 +382,8 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -451,6 +456,8 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= diff --git a/internal/http/request/toolbox_log.go b/internal/http/request/toolbox_log.go new file mode 100644 index 00000000..9d875549 --- /dev/null +++ b/internal/http/request/toolbox_log.go @@ -0,0 +1,6 @@ +package request + +// ToolboxLogClean 日志清理请求 +type ToolboxLogClean struct { + Type string `form:"type" json:"type" validate:"required|in:panel,website,mysql,docker,system"` +} diff --git a/internal/route/http.go b/internal/route/http.go index b95f2313..c2fbcccd 100644 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -51,6 +51,7 @@ type Http struct { toolboxBenchmark *service.ToolboxBenchmarkService toolboxSSH *service.ToolboxSSHService toolboxDisk *service.ToolboxDiskService + toolboxLog *service.ToolboxLogService webhook *service.WebHookService apps *apploader.Loader } @@ -91,6 +92,7 @@ func NewHttp( toolboxBenchmark *service.ToolboxBenchmarkService, toolboxSSH *service.ToolboxSSHService, toolboxDisk *service.ToolboxDiskService, + toolboxLog *service.ToolboxLogService, webhook *service.WebHookService, apps *apploader.Loader, ) *Http { @@ -130,6 +132,7 @@ func NewHttp( toolboxBenchmark: toolboxBenchmark, toolboxSSH: toolboxSSH, toolboxDisk: toolboxDisk, + toolboxLog: toolboxLog, webhook: webhook, apps: apps, } @@ -497,6 +500,11 @@ func (route *Http) Register(r *chi.Mux) { r.Post("/lvm/lv/extend", route.toolboxDisk.ExtendLV) }) + r.Route("/toolbox_log", func(r chi.Router) { + r.Get("/scan", route.toolboxLog.Scan) + r.Post("/clean", route.toolboxLog.Clean) + }) + r.Route("/webhook", func(r chi.Router) { r.Get("/", route.webhook.List) r.Post("/", route.webhook.Create) diff --git a/internal/service/service.go b/internal/service/service.go index 1a0ed291..d3253c76 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -40,5 +40,6 @@ var ProviderSet = wire.NewSet( NewToolboxBenchmarkService, NewToolboxSSHService, NewToolboxDiskService, + NewToolboxLogService, NewWsService, ) diff --git a/internal/service/toolbox_log.go b/internal/service/toolbox_log.go new file mode 100644 index 00000000..d1c3b791 --- /dev/null +++ b/internal/service/toolbox_log.go @@ -0,0 +1,652 @@ +package service + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/leonelquinteros/gotext" + "github.com/libtnb/chix" + "github.com/samber/lo" + "gorm.io/gorm" + + "github.com/acepanel/panel/internal/app" + "github.com/acepanel/panel/internal/biz" + "github.com/acepanel/panel/internal/http/request" + "github.com/acepanel/panel/pkg/io" + "github.com/acepanel/panel/pkg/shell" + "github.com/acepanel/panel/pkg/tools" +) + +type ToolboxLogService struct { + t *gotext.Locale + db *gorm.DB + containerImageRepo biz.ContainerImageRepo + settingRepo biz.SettingRepo +} + +func NewToolboxLogService(t *gotext.Locale, db *gorm.DB, containerImageRepo biz.ContainerImageRepo, settingRepo biz.SettingRepo) *ToolboxLogService { + return &ToolboxLogService{ + t: t, + db: db, + containerImageRepo: containerImageRepo, + settingRepo: settingRepo, + } +} + +// LogItem 日志项信息 +type LogItem struct { + Name string `json:"name"` // 日志名称 + Path string `json:"path"` // 日志路径 + Size string `json:"size"` // 日志大小 +} + +// Scan 扫描日志 +func (s *ToolboxLogService) Scan(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ToolboxLogClean](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + var items []LogItem + + switch req.Type { + case "panel": + items = s.scanPanelLogs() + case "website": + items = s.scanWebsiteLogs() + case "mysql": + items = s.scanMySQLLogs() + case "docker": + items = s.scanDockerLogs() + case "system": + items = s.scanSystemLogs() + default: + Error(w, http.StatusUnprocessableEntity, s.t.Get("unknown log type")) + return + } + + Success(w, items) +} + +// Clean 清理日志 +func (s *ToolboxLogService) Clean(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ToolboxLogClean](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + var cleaned int64 + var cleanErr error + + switch req.Type { + case "panel": + cleaned, cleanErr = s.cleanPanelLogs() + case "website": + cleaned, cleanErr = s.cleanWebsiteLogs() + case "mysql": + cleaned, cleanErr = s.cleanMySQLLogs() + case "docker": + cleaned, cleanErr = s.cleanDockerLogs() + case "system": + cleaned, cleanErr = s.cleanSystemLogs() + default: + Error(w, http.StatusUnprocessableEntity, s.t.Get("unknown log type")) + return + } + + if cleanErr != nil { + Error(w, http.StatusInternalServerError, "%v", cleanErr) + return + } + + Success(w, chix.M{ + "cleaned": tools.FormatBytes(float64(cleaned)), + }) +} + +// scanPanelLogs 扫描面板日志 +func (s *ToolboxLogService) scanPanelLogs() []LogItem { + var items []LogItem + logPath := filepath.Join(app.Root, "panel/storage/logs") + + if !io.Exists(logPath) { + return items + } + + entries, err := os.ReadDir(logPath) + if err != nil { + return items + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + info, err := entry.Info() + if err != nil { + continue + } + items = append(items, LogItem{ + Name: entry.Name(), + Path: filepath.Join(logPath, entry.Name()), + Size: tools.FormatBytes(float64(info.Size())), + }) + } + + return items +} + +// scanWebsiteLogs 扫描网站日志 +func (s *ToolboxLogService) scanWebsiteLogs() []LogItem { + var items []LogItem + sitesPath := filepath.Join(app.Root, "sites") + + if !io.Exists(sitesPath) { + return items + } + + // 获取所有网站 + websites := make([]*biz.Website, 0) + if err := s.db.Find(&websites).Error; err != nil { + return items + } + + for _, website := range websites { + logPath := filepath.Join(sitesPath, website.Name, "log") + if !io.Exists(logPath) { + continue + } + + entries, err := os.ReadDir(logPath) + if err != nil { + continue + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + info, err := entry.Info() + if err != nil { + continue + } + items = append(items, LogItem{ + Name: fmt.Sprintf("%s - %s", website.Name, entry.Name()), + Path: filepath.Join(logPath, entry.Name()), + Size: tools.FormatBytes(float64(info.Size())), + }) + } + } + + return items +} + +// scanMySQLLogs 扫描 MySQL 日志 +func (s *ToolboxLogService) scanMySQLLogs() []LogItem { + var items []LogItem + mysqlPath := filepath.Join(app.Root, "server/mysql") + + if !io.Exists(mysqlPath) { + return items + } + + // 慢查询日志 + slowLogPath := filepath.Join(mysqlPath, "mysql-slow.log") + if io.Exists(slowLogPath) { + if info, err := os.Stat(slowLogPath); err == nil { + items = append(items, LogItem{ + Name: "mysql-slow.log", + Path: slowLogPath, + Size: tools.FormatBytes(float64(info.Size())), + }) + } + } + + // 二进制日志 + entries, err := os.ReadDir(mysqlPath) + if err != nil { + return items + } + + binLogRegex := regexp.MustCompile(`^mysql-bin\.\d+$`) + for _, entry := range entries { + if entry.IsDir() { + continue + } + if binLogRegex.MatchString(entry.Name()) { + info, err := entry.Info() + if err != nil { + continue + } + items = append(items, LogItem{ + Name: entry.Name(), + Path: filepath.Join(mysqlPath, entry.Name()), + Size: tools.FormatBytes(float64(info.Size())), + }) + } + } + + return items +} + +// scanDockerLogs 扫描 Docker/Podman 相关内容 +func (s *ToolboxLogService) scanDockerLogs() []LogItem { + var items []LogItem + + // 未使用的容器镜像 (Docker) + images, err := s.containerImageRepo.List() + if err == nil { + // 计算未使用的镜像 + var unusedCount int + for _, img := range images { + if img.Containers == 0 { + unusedCount++ + } + } + + if unusedCount > 0 { + items = append(items, LogItem{ + Name: s.t.Get("Unused container images: %d", unusedCount), + Path: "docker:images", + Size: s.t.Get("%d images", unusedCount), + }) + } + } + + // Docker 容器日志路径 + dockerLogPath := "/var/lib/docker/containers" + if io.Exists(dockerLogPath) { + totalSize, logCount := s.scanContainerLogDir(dockerLogPath) + if logCount > 0 { + items = append(items, LogItem{ + Name: s.t.Get("Docker container logs: %d files", logCount), + Path: "docker:logs", + Size: tools.FormatBytes(float64(totalSize)), + }) + } + } + + // Podman 容器日志路径 + podmanLogPaths := []string{ + "/var/lib/containers/storage/overlay-containers", + "/run/containers/storage/overlay-containers", + } + for _, podmanLogPath := range podmanLogPaths { + if io.Exists(podmanLogPath) { + totalSize, logCount := s.scanContainerLogDir(podmanLogPath) + if logCount > 0 { + items = append(items, LogItem{ + Name: s.t.Get("Podman container logs: %d files", logCount), + Path: "podman:logs", + Size: tools.FormatBytes(float64(totalSize)), + }) + break + } + } + } + + return items +} + +// scanContainerLogDir 扫描容器日志目录 +func (s *ToolboxLogService) scanContainerLogDir(logPath string) (int64, int) { + var totalSize int64 + var logCount int + + entries, err := os.ReadDir(logPath) + if err != nil { + return 0, 0 + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + containerPath := filepath.Join(logPath, entry.Name()) + // 扫描 *.log 文件 + logFiles, _ := filepath.Glob(filepath.Join(containerPath, "*.log")) + for _, logFile := range logFiles { + if info, err := os.Stat(logFile); err == nil { + totalSize += info.Size() + logCount++ + } + } + // 扫描 userdata 子目录下的日志 (Podman) + userdataPath := filepath.Join(containerPath, "userdata") + if io.Exists(userdataPath) { + userdataLogs, _ := filepath.Glob(filepath.Join(userdataPath, "*.log")) + for _, logFile := range userdataLogs { + if info, err := os.Stat(logFile); err == nil { + totalSize += info.Size() + logCount++ + } + } + } + } + + return totalSize, logCount +} + +// scanSystemLogs 扫描系统日志 +func (s *ToolboxLogService) scanSystemLogs() []LogItem { + var items []LogItem + + logFiles := []string{ + "/var/log/syslog", + "/var/log/messages", + "/var/log/auth.log", + "/var/log/secure", + "/var/log/kern.log", + "/var/log/dmesg", + "/var/log/btmp", + "/var/log/wtmp", + "/var/log/lastlog", + } + + for _, logFile := range logFiles { + if !io.Exists(logFile) { + continue + } + info, err := os.Stat(logFile) + if err != nil { + continue + } + items = append(items, LogItem{ + Name: filepath.Base(logFile), + Path: logFile, + Size: tools.FormatBytes(float64(info.Size())), + }) + } + + // /var/log/*.log 文件 + logPattern := "/var/log/*.log" + matches, _ := filepath.Glob(logPattern) + for _, match := range matches { + // 跳过已经添加的文件 + if lo.Contains(logFiles, match) { + continue + } + info, err := os.Stat(match) + if err != nil { + continue + } + items = append(items, LogItem{ + Name: filepath.Base(match), + Path: match, + Size: tools.FormatBytes(float64(info.Size())), + }) + } + + // journal 日志大小 + journalOutput, _ := shell.Execf("journalctl --disk-usage 2>/dev/null | grep -oP '\\d+\\.?\\d*[KMGT]?' || echo '0'") + journalSize := strings.TrimSpace(journalOutput) + if journalSize != "" && journalSize != "0" { + items = append(items, LogItem{ + Name: s.t.Get("Journal logs"), + Path: "system:journal", + Size: journalSize, + }) + } + + return items +} + +// cleanPanelLogs 清理面板日志 +func (s *ToolboxLogService) cleanPanelLogs() (int64, error) { + var cleaned int64 + logPath := filepath.Join(app.Root, "panel/storage/logs") + + if !io.Exists(logPath) { + return 0, nil + } + + entries, err := os.ReadDir(logPath) + if err != nil { + return 0, err + } + + re := regexp.MustCompile(`\d{4}-\d{2}-\d{2}`) + + for _, entry := range entries { + if entry.IsDir() { + continue + } + filePath := filepath.Join(logPath, entry.Name()) + info, err := entry.Info() + if err != nil { + continue + } + cleaned += info.Size() + // 名称带日期的日志文件,删除旧文件 + if re.MatchString(entry.Name()) { + _ = os.Remove(filePath) + } else { + _, _ = shell.Execf("cat /dev/null > '%s'", filePath) + } + } + + return cleaned, nil +} + +// cleanWebsiteLogs 清理网站日志 +func (s *ToolboxLogService) cleanWebsiteLogs() (int64, error) { + var cleaned int64 + sitesPath := filepath.Join(app.Root, "sites") + + if !io.Exists(sitesPath) { + return 0, nil + } + + websites := make([]*biz.Website, 0) + if err := s.db.Find(&websites).Error; err != nil { + return 0, err + } + + for _, website := range websites { + logPath := filepath.Join(sitesPath, website.Name, "log") + if !io.Exists(logPath) { + continue + } + + entries, err := os.ReadDir(logPath) + if err != nil { + continue + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + filePath := filepath.Join(logPath, entry.Name()) + info, err := entry.Info() + if err != nil { + continue + } + cleaned += info.Size() + if _, err = shell.Execf("cat /dev/null > '%s'", filePath); err != nil { + continue + } + } + } + + return cleaned, nil +} + +// cleanMySQLLogs 清理 MySQL 日志 +func (s *ToolboxLogService) cleanMySQLLogs() (int64, error) { + var cleaned int64 + mysqlPath := filepath.Join(app.Root, "server/mysql") + + if !io.Exists(mysqlPath) { + return 0, nil + } + + // 清空慢查询日志 + slowLogPath := filepath.Join(mysqlPath, "mysql-slow.log") + if io.Exists(slowLogPath) { + if info, err := os.Stat(slowLogPath); err == nil { + cleaned += info.Size() + _, _ = shell.Execf("cat /dev/null > '%s'", slowLogPath) + } + } + + // 清理二进制日志 + entries, err := os.ReadDir(mysqlPath) + if err != nil { + return cleaned, nil + } + + binLogRegex := regexp.MustCompile(`^mysql-bin\.\d+$`) + for _, entry := range entries { + if entry.IsDir() { + continue + } + if binLogRegex.MatchString(entry.Name()) { + info, err := entry.Info() + if err != nil { + continue + } + cleaned += info.Size() + } + } + + // 尝试通过 MySQL 清理二进制日志 + // 从面板设置获取 root 密码 + rootPassword, err := s.settingRepo.Get(biz.SettingKeyMySQLRootPassword) + if err == nil && rootPassword != "" { + // 设置环境变量 + if err = os.Setenv("MYSQL_PWD", rootPassword); err == nil { + _, _ = shell.Execf("mysql -u root -e 'PURGE BINARY LOGS BEFORE NOW()' 2>/dev/null") + _ = os.Unsetenv("MYSQL_PWD") + } + } + + return cleaned, nil +} + +// cleanDockerLogs 清理 Docker/Podman 相关内容 +func (s *ToolboxLogService) cleanDockerLogs() (int64, error) { + var cleaned int64 + + // 清理未使用的镜像 (Docker) + _ = s.containerImageRepo.Prune() + + // 清理 Docker 容器日志 + dockerLogPath := "/var/lib/docker/containers" + cleaned += s.cleanContainerLogDir(dockerLogPath) + + // 清理 Podman 容器日志 + podmanLogPaths := []string{ + "/var/lib/containers/storage/overlay-containers", + "/run/containers/storage/overlay-containers", + } + for _, podmanLogPath := range podmanLogPaths { + cleaned += s.cleanContainerLogDir(podmanLogPath) + } + + // 清理 Docker 系统 + _, _ = shell.Execf("docker system prune -f 2>/dev/null") + + // 清理 Podman 系统 + _, _ = shell.Execf("podman system prune -f 2>/dev/null") + + return cleaned, nil +} + +// cleanContainerLogDir 清理容器日志目录 +func (s *ToolboxLogService) cleanContainerLogDir(logPath string) int64 { + var cleaned int64 + + if !io.Exists(logPath) { + return 0 + } + + entries, err := os.ReadDir(logPath) + if err != nil { + return 0 + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + containerPath := filepath.Join(logPath, entry.Name()) + + // 清理 *.log 文件 + logFiles, _ := filepath.Glob(filepath.Join(containerPath, "*.log")) + for _, logFile := range logFiles { + if info, err := os.Stat(logFile); err == nil { + cleaned += info.Size() + _, _ = shell.Execf("cat /dev/null > '%s'", logFile) + } + } + + // 清理 userdata 子目录下的日志 (Podman) + userdataPath := filepath.Join(containerPath, "userdata") + if io.Exists(userdataPath) { + userdataLogs, _ := filepath.Glob(filepath.Join(userdataPath, "*.log")) + for _, logFile := range userdataLogs { + if info, err := os.Stat(logFile); err == nil { + cleaned += info.Size() + _, _ = shell.Execf("cat /dev/null > '%s'", logFile) + } + } + } + } + + return cleaned +} + +// cleanSystemLogs 清理系统日志 +func (s *ToolboxLogService) cleanSystemLogs() (int64, error) { + var cleaned int64 + + // 清理 journal 日志 (保留最近 1 天) + _, _ = shell.Execf("journalctl --vacuum-time=1d 2>/dev/null") + + logFiles := []string{ + "/var/log/syslog", + "/var/log/messages", + "/var/log/auth.log", + "/var/log/secure", + "/var/log/kern.log", + "/var/log/dmesg", + "/var/log/btmp", + "/var/log/wtmp", + } + + for _, logFile := range logFiles { + if !io.Exists(logFile) { + continue + } + info, err := os.Stat(logFile) + if err != nil { + continue + } + cleaned += info.Size() + // 清空日志文件 + _, _ = shell.Execf("cat /dev/null > '%s'", logFile) + } + + // 清理 /var/log/*.log 文件 + matches, _ := filepath.Glob("/var/log/*.log") + for _, match := range matches { + if lo.Contains(logFiles, match) { + continue + } + info, err := os.Stat(match) + if err != nil { + continue + } + cleaned += info.Size() + _, _ = shell.Execf("cat /dev/null > '%s'", match) + } + + return cleaned, nil +} diff --git a/web/src/api/panel/toolbox-log/index.ts b/web/src/api/panel/toolbox-log/index.ts new file mode 100644 index 00000000..e0715970 --- /dev/null +++ b/web/src/api/panel/toolbox-log/index.ts @@ -0,0 +1,8 @@ +import { http } from '@/utils' + +export default { + // 扫描日志 + scan: (type: string): any => http.Get('/toolbox_log/scan', { params: { type } }), + // 清理日志 + clean: (type: string): any => http.Post('/toolbox_log/clean', { type }) +} diff --git a/web/src/views/toolbox/IndexView.vue b/web/src/views/toolbox/IndexView.vue index 01544139..05a3996d 100644 --- a/web/src/views/toolbox/IndexView.vue +++ b/web/src/views/toolbox/IndexView.vue @@ -5,6 +5,7 @@ defineOptions({ import BenchmarkView from '@/views/toolbox/BenchmarkView.vue' import DiskView from '@/views/toolbox/DiskView.vue' +import LogView from '@/views/toolbox/LogView.vue' import ProcessView from '@/views/toolbox/ProcessView.vue' import SshView from '@/views/toolbox/SshView.vue' import SystemView from '@/views/toolbox/SystemView.vue' @@ -23,6 +24,7 @@ const current = ref('process') + @@ -32,6 +34,7 @@ const current = ref('process') + diff --git a/web/src/views/toolbox/LogView.vue b/web/src/views/toolbox/LogView.vue new file mode 100644 index 00000000..fedac649 --- /dev/null +++ b/web/src/views/toolbox/LogView.vue @@ -0,0 +1,227 @@ + + +