```
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:
parent
8c6cf6310f
commit
cdea9d86ed
95
file.go
95
file.go
|
|
@ -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 // 小时转秒
|
||||||
|
|
|
||||||
44
format.go
44
format.go
|
|
@ -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
39
main.go
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue