Compare commits

...

7 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
kingecg 8c6cf6310f chore: update module path to git.kingecg.top 2025-06-26 01:05:39 +08:00
kingecg ecd906084a chore: update module path to git.kingecg.top 2025-06-25 22:53:39 +08:00
kingecg 0c28c10c1e chore: update module path to git.kingecg.top 2025-06-25 21:56:20 +08:00
kingecg 27fcdad135 fix nil pointer error 2025-06-06 23:30:27 +08:00
kingecg 1004333534 fix 2025-06-06 20:23:06 +08:00
5 changed files with 136 additions and 69 deletions

100
file.go
View File

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

View File

@ -10,26 +10,30 @@ import (
// LogFormatter is a function type that formats a LogEvent into a string.
// Example:
// formatter := func(event LogEvent) string {
// return fmt.Sprintf("[%s] %s: %v", event.Ts.Format("2006-01-02"), event.Level, event.Data)
// }
//
// formatter := func(event LogEvent) string {
// return fmt.Sprintf("[%s] %s: %v", event.Ts.Format("2006-01-02"), event.Level, event.Data)
// }
type LogFormatter = func(LogEvent) string
const logTemplate = "[%s] %s : %s - %s\n"
// format is the default formatter that converts a LogEvent to a string using the default template.
// It handles both simple values and formatted strings.
// 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 {
@ -41,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)
}

2
go.mod
View File

@ -1,3 +1,3 @@
module git.pyer.club/kingecg/gologger
module git.kingecg.top/kingecg/gologger
go 1.19

48
main.go
View File

@ -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": {
@ -57,12 +59,14 @@ type LoggersConfig struct {
Appenders map[string]LogAppenderConfig `json:"appenders"`
Categories map[string]LogConfig `json:"categories"`
}
// Logger represents a logger instance for a specific category.
// It maintains the log level and appenders for that category.
// Example:
// logger := GetLogger("mycategory")
// logger.Info("This is an info message")
// logger.Error("This is an error message")
//
// logger := GetLogger("mycategory")
// logger.Info("This is an info message")
// logger.Error("This is an error message")
type Logger struct {
category string
level int
@ -122,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 {
@ -156,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
@ -172,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 {
@ -203,4 +214,5 @@ func RegistAppender(typeName string, appenderCreatCb func(LogAppenderConfig) *Lo
func init() {
loggerMap["default"] = defaultLogger
appenders["console"] = &consoleAppender
}

View File

@ -1,7 +1,7 @@
package main
import (
logger "git.pyer.club/kingecg/gologger"
logger "git.kingecg.top/kingecg/gologger"
)
func aqmain() {