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 <copilot@github.com>
This commit is contained in:
kingecg 2026-07-09 21:47:03 +08:00
parent 8c6cf6310f
commit cdea9d86ed
3 changed files with 117 additions and 61 deletions

95
file.go
View File

@ -4,31 +4,37 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort"
"strconv" "strconv"
"strings"
"sync"
"time" "time"
) )
// FileAppender implements the LoggerAppender interface for file output. // FileAppender implements the LoggerAppender interface for file output.
// It supports log rotation based on file size and age. // It supports log rotation based on file size and age.
// Example: // Example:
// appender := &FileAppender{ //
// formatter: NewSimpleFormatter(), // appender := &FileAppender{
// filePath: "app.log", // formatter: NewSimpleFormatter(),
// EnableRolling: true, // filePath: "app.log",
// MaxSize: 1024 * 1024 * 10, // 10MB // EnableRolling: true,
// } // MaxSize: 1024 * 1024 * 10, // 10MB
// appender.Append(event) // }
// appender.Close() // appender.Append(event)
// appender.Close()
type FileAppender struct { type FileAppender struct {
formatter LogFormatter formatter LogFormatter
filePath string filePath string
lchan chan LogEvent lchan chan LogEvent
file *os.File file *os.File
stopChan chan struct{} stopChan chan struct{}
mu sync.Mutex
// 新增滚动相关字段 // 新增滚动相关字段
EnableRolling bool EnableRolling bool
MaxSize int64 // 文件最大大小(字节) MaxSize int64 // 文件最大大小(字节)
MaxAge int64 // 文件最大时间(秒) MaxAge int64 // 文件最大时间(秒)
MaxCount int // 最大文件数量
currentFileSize int64 // 当前文件大小 currentFileSize int64 // 当前文件大小
createdAt int64 // 文件创建时间戳 createdAt int64 // 文件创建时间戳
} }
@ -63,57 +69,98 @@ func (f *FileAppender) start() {
} }
go func() { go func() {
defer f.file.Close()
for { for {
select { select {
case <-f.stopChan: case <-f.stopChan:
f.mu.Lock()
if f.file != nil {
f.file.Close()
}
f.mu.Unlock()
return return
case logEvent := <-f.lchan: case logEvent := <-f.lchan:
f.mu.Lock()
// 检查日志滚动 // 检查日志滚动
if f.EnableRolling { if f.EnableRolling {
f.checkAndRoll(logEvent) if f.checkAndRoll(logEvent) {
f.mu.Unlock()
continue
}
} }
logMsg := f.formatter(logEvent) 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 { if f.MaxSize > 0 && f.currentFileSize >= f.MaxSize {
f.rollFile(logEvent) f.rollFile(logEvent)
return return true
} }
// 按时间滚动 // 按时间滚动
if f.MaxAge > 0 && time.Now().Unix()-f.createdAt >= f.MaxAge { if f.MaxAge > 0 && time.Now().Unix()-f.createdAt >= f.MaxAge {
f.rollFile(logEvent) f.rollFile(logEvent)
return true
} }
return false
} }
// 日志文件滚动 // 日志文件滚动
func (f *FileAppender) rollFile(logEvent LogEvent) { func (f *FileAppender) rollFile(logEvent LogEvent) {
// 关闭当前文件 // 关闭当前文件
f.file.Close() if f.file != nil {
f.file.Close()
}
// 重命名旧文件 // 重命名旧文件
timestamp := time.Now().Format("20060102150405") timestamp := time.Now().Format("20060102150405")
newPath := f.filePath + "." + timestamp 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.currentFileSize = 0
f.createdAt = time.Now().Unix() f.createdAt = time.Now().Unix()
// 重新写入当前日志 // 重新写入当前日志
logMsg := format(logEvent) logMsg := f.formatter(logEvent)
f.file.WriteString(logMsg) 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) { func (f *FileAppender) Append(logEvent LogEvent) {
@ -122,9 +169,7 @@ func (f *FileAppender) Append(logEvent LogEvent) {
} }
func makeFileAppender(appenderConfig LogAppenderConfig) *LoggerAppender { func makeFileAppender(appenderConfig LogAppenderConfig) *LoggerAppender {
var logfile interface{} logfile, ok := appenderConfig.Options["file"]
var ok bool
logfile, ok = appenderConfig.Options["file"]
if !ok { if !ok {
logfile = "default.log" logfile = "default.log"
} }
@ -167,16 +212,16 @@ func makeFileAppender(appenderConfig LogAppenderConfig) *LoggerAppender {
re := regexp.MustCompile(`^\d+[hd]?$`) re := regexp.MustCompile(`^\d+[hd]?$`)
if re.MatchString(ageStr) { if re.MatchString(ageStr) {
// 提取数字部分和单位 // 提取数字部分和单位
numStr := "" var numStr strings.Builder
unit := "" unit := ""
for _, c := range ageStr { for _, c := range ageStr {
if c >= '0' && c <= '9' { if c >= '0' && c <= '9' {
numStr += string(c) numStr.WriteRune(c)
} else { } else {
unit = string(c) unit = string(c)
} }
} }
num, _ := strconv.ParseInt(numStr, 10, 64) num, _ := strconv.ParseInt(numStr.String(), 10, 64)
switch unit { switch unit {
case "h": case "h":
maxAge = num * 3600 // 小时转秒 maxAge = num * 3600 // 小时转秒

View File

@ -23,16 +23,17 @@ const logTemplate = "[%s] %s : %s - %s\n"
// Template: "[timestamp] level : category - data" // Template: "[timestamp] level : category - data"
func format(logEvent LogEvent) string { func format(logEvent LogEvent) string {
data := logEvent.Ts.Format("2006-01-02 15:04:05") data := logEvent.Ts.Format("2006-01-02 15:04:05")
msg := "" var msg string
firstMsg := logEvent.Data[0] if len(logEvent.Data) > 0 {
if isFormatString(firstMsg) { firstMsg := logEvent.Data[0]
msg = fmt.Sprintf(firstMsg.(string), logEvent.Data[1:]...) if isFormatString(firstMsg) {
} else { msg = fmt.Sprintf(firstMsg.(string), logEvent.Data[1:]...)
msg = sprint(logEvent.Data) } else {
msg = sprint(logEvent.Data)
}
} }
ret := fmt.Sprintf(logTemplate, data, logEvent.Category, getLogLevelStr(logEvent.Level), msg) return fmt.Sprintf(logTemplate, data, logEvent.Category, getLogLevelStr(logEvent.Level), msg)
return ret
} }
func getLogLevelStr(level int) string { func getLogLevelStr(level int) string {
@ -44,34 +45,35 @@ func getLogLevelStr(level int) string {
return "Unknown" return "Unknown"
} }
func isFormatString(f interface{}) bool { func isFormatString(f any) bool {
s, ok := f.(string) s, ok := f.(string)
if !ok { if !ok {
return false return false
} }
// 尝试使用空接口来格式化字符串 // 检查是否包含常见格式化动词
m := fmt.Sprintf(s, []interface{}{}...) return strings.Contains(s, "%s") || strings.Contains(s, "%d") ||
return strings.Contains(m, "MISSING") 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 { func sprint(s []any) string {
str := make([]any, len(s)) var b strings.Builder
for i, v := range s { for i, v := range s {
if i > 0 { if i > 0 {
str[i] = fmt.Sprintf(" %v", v) b.WriteString(" ")
} else {
str[i] = fmt.Sprintf("%v", v)
} }
fmt.Fprintf(&b, "%v", v)
} }
return fmt.Sprint(str...) return b.String()
} }
func jsonFormatter(logEvent LogEvent) string { func jsonFormatter(logEvent LogEvent) string {
_, err := json.Marshal(logEvent.Data) d, err := json.Marshal(logEvent)
if err != nil { 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) return string(d)
} }

39
main.go
View File

@ -4,6 +4,7 @@ package gologger
import ( import (
"strings" "strings"
"sync"
"time" "time"
) )
@ -27,6 +28,7 @@ var logLevelMap map[string]int = map[string]int{
var loggerMap map[string]*Logger = map[string]*Logger{} var loggerMap map[string]*Logger = map[string]*Logger{}
var appenderFactoryMap map[string]func(LogAppenderConfig) *LoggerAppender = map[string]func(LogAppenderConfig) *LoggerAppender{} var appenderFactoryMap map[string]func(LogAppenderConfig) *LoggerAppender = map[string]func(LogAppenderConfig) *LoggerAppender{}
var appenders map[string]*LoggerAppender = map[string]*LoggerAppender{} var appenders map[string]*LoggerAppender = map[string]*LoggerAppender{}
var mu sync.RWMutex
var loggerConfig LoggersConfig = LoggersConfig{ var loggerConfig LoggersConfig = LoggersConfig{
Appenders: map[string]LogAppenderConfig{ Appenders: map[string]LogAppenderConfig{
"console": { "console": {
@ -124,19 +126,24 @@ func (l *Logger) Trace(msg ...interface{}) {
l.log(Trace, msg) l.log(Trace, msg)
} }
func GetLogger(name string) *Logger { func GetLogger(name string) *Logger {
mu.RLock()
if logger, ok := loggerMap[name]; ok { if logger, ok := loggerMap[name]; ok {
mu.RUnlock()
return logger 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 { func makeLogger(name string, config LogConfig) *Logger {
@ -158,14 +165,16 @@ func makeLogger(name string, config LogConfig) *Logger {
func ReconfigLoggers(config LoggersConfig) { func ReconfigLoggers(config LoggersConfig) {
validateConfig(config) validateConfig(config)
mu.Lock()
defer mu.Unlock()
loggerConfig = config loggerConfig = config
for name, appenderConfig := range loggerConfig.Appenders { for name, appenderConfig := range loggerConfig.Appenders {
oldappender, ok := appenders[name] if oldappender, ok := appenders[name]; ok {
if ok {
(*oldappender).Close() (*oldappender).Close()
} }
appenderFactory, ok := appenderFactoryMap[appenderConfig.Type] if appenderFactory, ok := appenderFactoryMap[appenderConfig.Type]; ok {
if ok {
appenders[name] = appenderFactory(appenderConfig) appenders[name] = appenderFactory(appenderConfig)
} else { } else {
appenders[name] = &consoleAppender appenders[name] = &consoleAppender
@ -174,7 +183,7 @@ func ReconfigLoggers(config LoggersConfig) {
for name, logger := range loggerMap { for name, logger := range loggerMap {
logConfig, ok := loggerConfig.Categories[name] logConfig, ok := loggerConfig.Categories[name]
if !ok { if !ok {
logConfig, _ = loggerConfig.Categories["default"] logConfig = loggerConfig.Categories["default"]
} }
logger.level = logLevelMap[strings.ToLower(logConfig.Level)] logger.level = logLevelMap[strings.ToLower(logConfig.Level)]
if len(logConfig.Appenders) == 0 { if len(logConfig.Appenders) == 0 {