Compare commits

...

2 Commits

Author SHA1 Message Date
kingecg 36b2fe9f34 ```
feat(logger): add maxCount configuration option for file appender

Add support for maxCount parameter in file appender configuration.
The maxCount option allows users to specify the maximum number of
backup files to keep when log rotation is enabled.

The new configuration option defaults to 1 if not specified in the
appender options and is passed to the FileAppender struct during
initialization.
```

Co-authored-by: Copilot <copilot@github.com>
2026-07-11 08:54:19 +08:00
kingecg cdea9d86ed ```
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>
2026-07-09 21:47:03 +08:00
3 changed files with 122 additions and 61 deletions

100
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 // 小时转秒
@ -187,6 +232,10 @@ func makeFileAppender(appenderConfig LogAppenderConfig) *LoggerAppender {
} else if ageInt, ok := appenderConfig.Options["maxAge"].(int64); ok { } else if ageInt, ok := appenderConfig.Options["maxAge"].(int64); ok {
maxAge = ageInt maxAge = ageInt
} }
maxCount := int(1)
if count, ok := appenderConfig.Options["maxCount"].(int); ok {
maxCount = count
}
var ret LoggerAppender = &FileAppender{ var ret LoggerAppender = &FileAppender{
formatter: SelectFormatter(appenderConfig.Formatter), formatter: SelectFormatter(appenderConfig.Formatter),
@ -194,6 +243,7 @@ func makeFileAppender(appenderConfig LogAppenderConfig) *LoggerAppender {
EnableRolling: rollingEnabled, EnableRolling: rollingEnabled,
MaxSize: maxSize, MaxSize: maxSize,
MaxAge: maxAge, MaxAge: maxAge,
MaxCount: maxCount,
} }
ret.(*FileAppender).start() ret.(*FileAppender).start()

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 {