From cdea9d86ed1daac9f19534f18a9d936e16ca3b1f Mon Sep 17 00:00:00 2001 From: kingecg Date: Thu, 9 Jul 2026 21:47:03 +0800 Subject: [PATCH] ``` feat(file): add thread safety and improve log rolling functionality - Add sync.Mutex to FileAppender for thread-safe operations - Implement proper mutex locking in start() goroutine to prevent race conditions during file operations - Return boolean from checkAndRoll() method to indicate if rolling occurred - Add MaxCount field to limit number of archived log files - Implement cleanup of old log files exceeding MaxCount limit - Add proper error handling for file rename operations - Track current file size using WriteString return values refactor(format): update formatting functions with better type safety - Change isFormatString parameter Co-authored-by: Copilot --- file.go | 95 ++++++++++++++++++++++++++++++++++++++++--------------- format.go | 44 ++++++++++++++------------ main.go | 39 ++++++++++++++--------- 3 files changed, 117 insertions(+), 61 deletions(-) diff --git a/file.go b/file.go index 07b47e1..c45275b 100644 --- a/file.go +++ b/file.go @@ -4,31 +4,37 @@ import ( "os" "path/filepath" "regexp" + "sort" "strconv" + "strings" + "sync" "time" ) // FileAppender implements the LoggerAppender interface for file output. // It supports log rotation based on file size and age. // Example: -// appender := &FileAppender{ -// formatter: NewSimpleFormatter(), -// filePath: "app.log", -// EnableRolling: true, -// MaxSize: 1024 * 1024 * 10, // 10MB -// } -// appender.Append(event) -// appender.Close() +// +// appender := &FileAppender{ +// formatter: NewSimpleFormatter(), +// filePath: "app.log", +// EnableRolling: true, +// MaxSize: 1024 * 1024 * 10, // 10MB +// } +// appender.Append(event) +// appender.Close() type FileAppender struct { formatter LogFormatter filePath string lchan chan LogEvent file *os.File stopChan chan struct{} + mu sync.Mutex // 新增滚动相关字段 EnableRolling bool MaxSize int64 // 文件最大大小(字节) MaxAge int64 // 文件最大时间(秒) + MaxCount int // 最大文件数量 currentFileSize int64 // 当前文件大小 createdAt int64 // 文件创建时间戳 } @@ -63,57 +69,98 @@ func (f *FileAppender) start() { } go func() { - defer f.file.Close() for { select { case <-f.stopChan: + f.mu.Lock() + if f.file != nil { + f.file.Close() + } + f.mu.Unlock() return case logEvent := <-f.lchan: + f.mu.Lock() // 检查日志滚动 if f.EnableRolling { - f.checkAndRoll(logEvent) + if f.checkAndRoll(logEvent) { + f.mu.Unlock() + continue + } } logMsg := f.formatter(logEvent) - f.file.WriteString(logMsg) + n, err := f.file.WriteString(logMsg) + if err == nil { + f.currentFileSize += int64(n) + } + f.mu.Unlock() } } }() } -// 新增日志滚动方法 -func (f *FileAppender) checkAndRoll(logEvent LogEvent) { +// 新增日志滚动方法,返回是否进行了滚动 +func (f *FileAppender) checkAndRoll(logEvent LogEvent) bool { // 按大小滚动 if f.MaxSize > 0 && f.currentFileSize >= f.MaxSize { f.rollFile(logEvent) - return + return true } // 按时间滚动 if f.MaxAge > 0 && time.Now().Unix()-f.createdAt >= f.MaxAge { f.rollFile(logEvent) + return true } + return false } // 日志文件滚动 func (f *FileAppender) rollFile(logEvent LogEvent) { // 关闭当前文件 - f.file.Close() + if f.file != nil { + f.file.Close() + } // 重命名旧文件 timestamp := time.Now().Format("20060102150405") newPath := f.filePath + "." + timestamp - os.Rename(f.filePath, newPath) + if err := os.Rename(f.filePath, newPath); err != nil { + // 重命名失败,直接创建新文件 + f.file, _ = os.OpenFile(f.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + return + } // 创建新文件 - f.file, _ = os.OpenFile(f.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + var err error + f.file, err = os.OpenFile(f.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return + } // 重置状态 f.currentFileSize = 0 f.createdAt = time.Now().Unix() // 重新写入当前日志 - logMsg := format(logEvent) - f.file.WriteString(logMsg) + logMsg := f.formatter(logEvent) + if _, err := f.file.WriteString(logMsg); err == nil { + f.currentFileSize += int64(len(logMsg)) + } + + // 清理超过MaxCount的旧文件 + if f.MaxCount > 0 { + logDir := filepath.Dir(newPath) + logFileBase := filepath.Base(f.filePath) + files, _ := filepath.Glob(logDir + "/" + logFileBase + ".*") + if len(files) > f.MaxCount { + sort.Slice(files, func(i, j int) bool { + return files[i] < files[j] + }) + for i := 0; i < len(files)-f.MaxCount; i++ { + os.Remove(files[i]) + } + } + } } func (f *FileAppender) Append(logEvent LogEvent) { @@ -122,9 +169,7 @@ func (f *FileAppender) Append(logEvent LogEvent) { } func makeFileAppender(appenderConfig LogAppenderConfig) *LoggerAppender { - var logfile interface{} - var ok bool - logfile, ok = appenderConfig.Options["file"] + logfile, ok := appenderConfig.Options["file"] if !ok { logfile = "default.log" } @@ -167,16 +212,16 @@ func makeFileAppender(appenderConfig LogAppenderConfig) *LoggerAppender { re := regexp.MustCompile(`^\d+[hd]?$`) if re.MatchString(ageStr) { // 提取数字部分和单位 - numStr := "" + var numStr strings.Builder unit := "" for _, c := range ageStr { if c >= '0' && c <= '9' { - numStr += string(c) + numStr.WriteRune(c) } else { unit = string(c) } } - num, _ := strconv.ParseInt(numStr, 10, 64) + num, _ := strconv.ParseInt(numStr.String(), 10, 64) switch unit { case "h": maxAge = num * 3600 // 小时转秒 diff --git a/format.go b/format.go index ae295f2..9ff4d11 100644 --- a/format.go +++ b/format.go @@ -23,16 +23,17 @@ const logTemplate = "[%s] %s : %s - %s\n" // Template: "[timestamp] level : category - data" func format(logEvent LogEvent) string { data := logEvent.Ts.Format("2006-01-02 15:04:05") - msg := "" - firstMsg := logEvent.Data[0] - if isFormatString(firstMsg) { - msg = fmt.Sprintf(firstMsg.(string), logEvent.Data[1:]...) - } else { - msg = sprint(logEvent.Data) + var msg string + if len(logEvent.Data) > 0 { + firstMsg := logEvent.Data[0] + if isFormatString(firstMsg) { + msg = fmt.Sprintf(firstMsg.(string), logEvent.Data[1:]...) + } else { + msg = sprint(logEvent.Data) + } } - ret := fmt.Sprintf(logTemplate, data, logEvent.Category, getLogLevelStr(logEvent.Level), msg) - return ret + return fmt.Sprintf(logTemplate, data, logEvent.Category, getLogLevelStr(logEvent.Level), msg) } func getLogLevelStr(level int) string { @@ -44,34 +45,35 @@ func getLogLevelStr(level int) string { return "Unknown" } -func isFormatString(f interface{}) bool { +func isFormatString(f any) bool { s, ok := f.(string) if !ok { return false } - // 尝试使用空接口来格式化字符串 - m := fmt.Sprintf(s, []interface{}{}...) - return strings.Contains(m, "MISSING") + // 检查是否包含常见格式化动词 + return strings.Contains(s, "%s") || strings.Contains(s, "%d") || + strings.Contains(s, "%v") || strings.Contains(s, "%f") || + strings.Contains(s, "%x") || strings.Contains(s, "%t") || + strings.Contains(s, "%c") || strings.Contains(s, "%b") || + strings.Contains(s, "%e") || strings.Contains(s, "%g") } -func sprint(s []interface{}) string { - str := make([]any, len(s)) +func sprint(s []any) string { + var b strings.Builder for i, v := range s { if i > 0 { - str[i] = fmt.Sprintf(" %v", v) - } else { - str[i] = fmt.Sprintf("%v", v) + b.WriteString(" ") } + fmt.Fprintf(&b, "%v", v) } - return fmt.Sprint(str...) + return b.String() } func jsonFormatter(logEvent LogEvent) string { - _, err := json.Marshal(logEvent.Data) + d, err := json.Marshal(logEvent) if err != nil { - logEvent.Data = []interface{}{fmt.Sprintf("%v", logEvent.Data)} + return fmt.Sprintf(`{"error":"%v"}`, logEvent.Data) } - d, _ := json.Marshal(logEvent) return string(d) } diff --git a/main.go b/main.go index ab66a2f..b7ad9da 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ package gologger import ( "strings" + "sync" "time" ) @@ -27,6 +28,7 @@ var logLevelMap map[string]int = map[string]int{ var loggerMap map[string]*Logger = map[string]*Logger{} var appenderFactoryMap map[string]func(LogAppenderConfig) *LoggerAppender = map[string]func(LogAppenderConfig) *LoggerAppender{} var appenders map[string]*LoggerAppender = map[string]*LoggerAppender{} +var mu sync.RWMutex var loggerConfig LoggersConfig = LoggersConfig{ Appenders: map[string]LogAppenderConfig{ "console": { @@ -124,19 +126,24 @@ func (l *Logger) Trace(msg ...interface{}) { l.log(Trace, msg) } func GetLogger(name string) *Logger { + mu.RLock() if logger, ok := loggerMap[name]; ok { + mu.RUnlock() return logger - } else { - logConfig, ok := loggerConfig.Categories[name] - if ok { - return makeLogger(name, logConfig) - } - logConfig, ok = loggerConfig.Categories["default"] - l := makeLogger(name, logConfig) - l.category = name - loggerMap[name] = l - return l } + mu.RUnlock() + + logConfig, ok := loggerConfig.Categories[name] + if !ok { + logConfig = loggerConfig.Categories["default"] + } + l := makeLogger(name, logConfig) + l.category = name + + mu.Lock() + loggerMap[name] = l + mu.Unlock() + return l } func makeLogger(name string, config LogConfig) *Logger { @@ -158,14 +165,16 @@ func makeLogger(name string, config LogConfig) *Logger { func ReconfigLoggers(config LoggersConfig) { validateConfig(config) + + mu.Lock() + defer mu.Unlock() + loggerConfig = config for name, appenderConfig := range loggerConfig.Appenders { - oldappender, ok := appenders[name] - if ok { + if oldappender, ok := appenders[name]; ok { (*oldappender).Close() } - appenderFactory, ok := appenderFactoryMap[appenderConfig.Type] - if ok { + if appenderFactory, ok := appenderFactoryMap[appenderConfig.Type]; ok { appenders[name] = appenderFactory(appenderConfig) } else { appenders[name] = &consoleAppender @@ -174,7 +183,7 @@ func ReconfigLoggers(config LoggersConfig) { for name, logger := range loggerMap { logConfig, ok := loggerConfig.Categories[name] if !ok { - logConfig, _ = loggerConfig.Categories["default"] + logConfig = loggerConfig.Categories["default"] } logger.level = logLevelMap[strings.ToLower(logConfig.Level)] if len(logConfig.Appenders) == 0 {