Просмотр исходного кода

fix: log management race condition, partial delete reporting, and UX issues

- Fix data race on gin.DefaultWriter during log rotation by adding LogWriterMu
- Report partial failure when some log files fail to delete instead of always returning success
- Fix misleading "logging disabled" banner shown before API responds
- Fix en.json translation for numeric validation message
RedwindA 3 недель назад
Родитель
Сommit
dcd0911612

+ 15 - 8
common/sys_log.go

@@ -3,53 +3,60 @@ package common
 import (
 	"fmt"
 	"os"
+	"sync"
 	"time"
 
 	"github.com/gin-gonic/gin"
 )
 
+// LogWriterMu protects concurrent access to gin.DefaultWriter/gin.DefaultErrorWriter
+// during log file rotation. Acquire RLock when reading/writing through the writers,
+// acquire Lock when swapping writers and closing old files.
+var LogWriterMu sync.RWMutex
+
 func SysLog(s string) {
 	t := time.Now()
+	LogWriterMu.RLock()
 	_, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
+	LogWriterMu.RUnlock()
 }
 
 func SysError(s string) {
 	t := time.Now()
+	LogWriterMu.RLock()
 	_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
+	LogWriterMu.RUnlock()
 }
 
 func FatalLog(v ...any) {
 	t := time.Now()
+	LogWriterMu.RLock()
 	_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
+	LogWriterMu.RUnlock()
 	os.Exit(1)
 }
 
 func LogStartupSuccess(startTime time.Time, port string) {
-
 	duration := time.Since(startTime)
 	durationMs := duration.Milliseconds()
 
 	// Get network IPs
 	networkIps := GetNetworkIps()
 
-	// Print blank line for spacing
-	fmt.Fprintf(gin.DefaultWriter, "\n")
+	LogWriterMu.RLock()
+	defer LogWriterMu.RUnlock()
 
-	// Print the main success message
+	fmt.Fprintf(gin.DefaultWriter, "\n")
 	fmt.Fprintf(gin.DefaultWriter, "  \033[32m%s %s\033[0m  ready in %d ms\n", SystemName, Version, durationMs)
 	fmt.Fprintf(gin.DefaultWriter, "\n")
 
-	// Skip fancy startup message in container environments
 	if !IsRunningInContainer() {
-		// Print local URL
 		fmt.Fprintf(gin.DefaultWriter, "  ➜  \033[1mLocal:\033[0m   http://localhost:%s/\n", port)
 	}
 
-	// Print network URLs
 	for _, ip := range networkIps {
 		fmt.Fprintf(gin.DefaultWriter, "  ➜  \033[1mNetwork:\033[0m http://%s:%s/\n", ip, port)
 	}
 
-	// Print blank line for spacing
 	fmt.Fprintf(gin.DefaultWriter, "\n")
 }

+ 17 - 5
controller/performance.go

@@ -1,6 +1,7 @@
 package controller
 
 import (
+	"fmt"
 	"net/http"
 	"os"
 	"path/filepath"
@@ -329,14 +330,25 @@ func CleanupLogFiles(c *gin.Context) {
 		freedBytes += f.Size
 	}
 
+	result := gin.H{
+		"deleted_count": deletedCount,
+		"freed_bytes":   freedBytes,
+		"failed_files":  failedFiles,
+	}
+
+	if len(failedFiles) > 0 {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": fmt.Sprintf("部分文件删除失败(%d/%d)", len(failedFiles), len(toDelete)),
+			"data":    result,
+		})
+		return
+	}
+
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"message": "",
-		"data": gin.H{
-			"deleted_count": deletedCount,
-			"freed_bytes":   freedBytes,
-			"failed_files":  failedFiles,
-		},
+		"data":    result,
 	})
 }
 

+ 10 - 5
logger/logger.go

@@ -61,12 +61,15 @@ func SetupLogger() {
 		oldFile := currentLogFile
 		currentLogPath = logPath
 		currentLogFile = fd
+		currentLogPathMu.Unlock()
+
+		common.LogWriterMu.Lock()
 		gin.DefaultWriter = io.MultiWriter(os.Stdout, fd)
 		gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, fd)
-		currentLogPathMu.Unlock()
 		if oldFile != nil {
 			_ = oldFile.Close()
 		}
+		common.LogWriterMu.Unlock()
 	}
 }
 
@@ -92,16 +95,18 @@ func LogDebug(ctx context.Context, msg string, args ...any) {
 }
 
 func logHelper(ctx context.Context, level string, msg string) {
-	writer := gin.DefaultErrorWriter
-	if level == loggerINFO {
-		writer = gin.DefaultWriter
-	}
 	id := ctx.Value(common.RequestIdKey)
 	if id == nil {
 		id = "SYSTEM"
 	}
 	now := time.Now()
+	common.LogWriterMu.RLock()
+	writer := gin.DefaultErrorWriter
+	if level == loggerINFO {
+		writer = gin.DefaultWriter
+	}
 	_, _ = fmt.Fprintf(writer, "[%s] %v | %s | %s \n", level, now.Format("2006/01/02 - 15:04:05"), id, msg)
+	common.LogWriterMu.RUnlock()
 	logCount++ // we don't need accurate count, so no lock here
 	if logCount > maxLogCount && !setupLogWorking {
 		logCount = 0

+ 1 - 1
web/src/i18n/locales/en.json

@@ -2772,7 +2772,7 @@
     "请输入新的部署名称": "Please enter new deployment name",
     "请输入显示名称": "Please enter display name",
     "请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。": "Please enter a valid JSON format request body. You can refer to the default request body format in the preview panel.",
-    "请输入有效的数值": "Please enter a valid value",
+    "请输入有效的数值": "Please enter a valid number",
     "请输入有效的数字": "Please enter a valid number",
     "请输入有效的镜像地址": "Please enter a valid image address",
     "请输入标签名称": "Please enter the tag name",

+ 1 - 1
web/src/pages/Setting/Performance/SettingsPerformance.jsx

@@ -408,7 +408,7 @@ export default function SettingsPerformance(props) {
           )}
           style={{ marginBottom: 16 }}
         />
-        {logInfo && logInfo.enabled ? (
+        {logInfo === null ? null : logInfo.enabled ? (
           <>
             <Descriptions
               data={[