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 @@
+
+
+
+
+
+
+
+
+
+ {{ $gettext('Scan All') }}
+
+
+
+
+
+ {{ $gettext('Clean All') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $gettext('Scan') }}
+
+
+
+
+
+ {{ $gettext('Clean') }}
+
+
+
+
+
+ {{ logType.description }}
+
+
+
+
+ {{ $gettext('Scanning...') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $gettext('Click Scan to check logs') }}
+
+
+
+
+
+
+
+