e1.7.0 Timestamps improvements.
This commit is contained in:
17
config.go
17
config.go
@ -3,20 +3,22 @@ package log
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config holds all logger configuration values
|
// Config holds all logger configuration values
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// Basic settings
|
// Basic settings
|
||||||
Level int64 `toml:"level"`
|
Level int64 `toml:"level"`
|
||||||
Name string `toml:"name"`
|
Name string `toml:"name"` // Base name for log files
|
||||||
Directory string `toml:"directory"`
|
Directory string `toml:"directory"`
|
||||||
Format string `toml:"format"` // "txt" or "json"
|
Format string `toml:"format"` // "txt" or "json"
|
||||||
Extension string `toml:"extension"`
|
Extension string `toml:"extension"`
|
||||||
|
|
||||||
// Formatting
|
// Formatting
|
||||||
ShowTimestamp bool `toml:"show_timestamp"`
|
ShowTimestamp bool `toml:"show_timestamp"`
|
||||||
ShowLevel bool `toml:"show_level"`
|
ShowLevel bool `toml:"show_level"`
|
||||||
|
TimestampFormat string `toml:"timestamp_format"` // Time format for log timestamps
|
||||||
|
|
||||||
// Buffer and size limits
|
// Buffer and size limits
|
||||||
BufferSize int64 `toml:"buffer_size"` // Channel buffer size
|
BufferSize int64 `toml:"buffer_size"` // Channel buffer size
|
||||||
@ -60,8 +62,9 @@ var defaultConfig = Config{
|
|||||||
Extension: "log",
|
Extension: "log",
|
||||||
|
|
||||||
// Formatting
|
// Formatting
|
||||||
ShowTimestamp: true,
|
ShowTimestamp: true,
|
||||||
ShowLevel: true,
|
ShowLevel: true,
|
||||||
|
TimestampFormat: time.RFC3339Nano,
|
||||||
|
|
||||||
// Buffer and size limits
|
// Buffer and size limits
|
||||||
BufferSize: 1024,
|
BufferSize: 1024,
|
||||||
@ -110,8 +113,8 @@ func (c *Config) validate() error {
|
|||||||
if c.Format != "txt" && c.Format != "json" {
|
if c.Format != "txt" && c.Format != "json" {
|
||||||
return fmtErrorf("invalid format: '%s' (use txt or json)", c.Format)
|
return fmtErrorf("invalid format: '%s' (use txt or json)", c.Format)
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(c.Extension, ".") {
|
if strings.TrimSpace(c.TimestampFormat) == "" {
|
||||||
return fmtErrorf("extension should not start with dot: %s", c.Extension)
|
return fmtErrorf("timestamp_format cannot be empty")
|
||||||
}
|
}
|
||||||
if c.BufferSize <= 0 {
|
if c.BufferSize <= 0 {
|
||||||
return fmtErrorf("buffer_size must be positive: %d", c.BufferSize)
|
return fmtErrorf("buffer_size must be positive: %d", c.BufferSize)
|
||||||
|
|||||||
22
format.go
22
format.go
@ -10,13 +10,15 @@ import (
|
|||||||
|
|
||||||
// serializer manages the buffered writing of log entries.
|
// serializer manages the buffered writing of log entries.
|
||||||
type serializer struct {
|
type serializer struct {
|
||||||
buf []byte
|
buf []byte
|
||||||
|
timestampFormat string
|
||||||
}
|
}
|
||||||
|
|
||||||
// newSerializer creates a serializer instance.
|
// newSerializer creates a serializer instance.
|
||||||
func newSerializer() *serializer {
|
func newSerializer() *serializer {
|
||||||
return &serializer{
|
return &serializer{
|
||||||
buf: make([]byte, 0, 4096), // Initial reasonable capacity
|
buf: make([]byte, 0, 4096), // Initial reasonable capacity
|
||||||
|
timestampFormat: time.RFC3339Nano, // Default until configured
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +44,7 @@ func (s *serializer) serializeJSON(flags int64, timestamp time.Time, level int64
|
|||||||
|
|
||||||
if flags&FlagShowTimestamp != 0 {
|
if flags&FlagShowTimestamp != 0 {
|
||||||
s.buf = append(s.buf, `"time":"`...)
|
s.buf = append(s.buf, `"time":"`...)
|
||||||
s.buf = timestamp.AppendFormat(s.buf, time.RFC3339Nano)
|
s.buf = timestamp.AppendFormat(s.buf, s.timestampFormat)
|
||||||
s.buf = append(s.buf, '"')
|
s.buf = append(s.buf, '"')
|
||||||
needsComma = true
|
needsComma = true
|
||||||
}
|
}
|
||||||
@ -90,7 +92,7 @@ func (s *serializer) serializeText(flags int64, timestamp time.Time, level int64
|
|||||||
needsSpace := false
|
needsSpace := false
|
||||||
|
|
||||||
if flags&FlagShowTimestamp != 0 {
|
if flags&FlagShowTimestamp != 0 {
|
||||||
s.buf = timestamp.AppendFormat(s.buf, time.RFC3339Nano)
|
s.buf = timestamp.AppendFormat(s.buf, s.timestampFormat)
|
||||||
needsSpace = true
|
needsSpace = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,7 +152,7 @@ func (s *serializer) writeTextValue(v any) {
|
|||||||
case nil:
|
case nil:
|
||||||
s.buf = append(s.buf, "null"...)
|
s.buf = append(s.buf, "null"...)
|
||||||
case time.Time:
|
case time.Time:
|
||||||
s.buf = val.AppendFormat(s.buf, time.RFC3339Nano)
|
s.buf = val.AppendFormat(s.buf, s.timestampFormat)
|
||||||
case error:
|
case error:
|
||||||
str := val.Error()
|
str := val.Error()
|
||||||
if len(str) == 0 || strings.ContainsRune(str, ' ') {
|
if len(str) == 0 || strings.ContainsRune(str, ' ') {
|
||||||
@ -206,7 +208,7 @@ func (s *serializer) writeJSONValue(v any) {
|
|||||||
s.buf = append(s.buf, "null"...)
|
s.buf = append(s.buf, "null"...)
|
||||||
case time.Time:
|
case time.Time:
|
||||||
s.buf = append(s.buf, '"')
|
s.buf = append(s.buf, '"')
|
||||||
s.buf = val.AppendFormat(s.buf, time.RFC3339Nano)
|
s.buf = val.AppendFormat(s.buf, s.timestampFormat)
|
||||||
s.buf = append(s.buf, '"')
|
s.buf = append(s.buf, '"')
|
||||||
case error:
|
case error:
|
||||||
s.buf = append(s.buf, '"')
|
s.buf = append(s.buf, '"')
|
||||||
@ -278,4 +280,12 @@ func (s *serializer) writeString(str string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update cached format
|
||||||
|
func (s *serializer) setTimestampFormat(format string) {
|
||||||
|
if format == "" {
|
||||||
|
format = time.RFC3339Nano
|
||||||
|
}
|
||||||
|
s.timestampFormat = format
|
||||||
|
}
|
||||||
|
|
||||||
const hexChars = "0123456789abcdef"
|
const hexChars = "0123456789abcdef"
|
||||||
2
go.mod
2
go.mod
@ -3,7 +3,7 @@ module github.com/lixenwraith/log
|
|||||||
go 1.24.5
|
go 1.24.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/lixenwraith/config v0.0.0-20250701170607-8515fa0543b6
|
github.com/lixenwraith/config v0.0.0-20250712170030-7d38402e0497
|
||||||
github.com/panjf2000/gnet/v2 v2.9.1
|
github.com/panjf2000/gnet/v2 v2.9.1
|
||||||
github.com/valyala/fasthttp v1.63.0
|
github.com/valyala/fasthttp v1.63.0
|
||||||
)
|
)
|
||||||
|
|||||||
2
go.sum
2
go.sum
@ -8,6 +8,8 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt
|
|||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/lixenwraith/config v0.0.0-20250701170607-8515fa0543b6 h1:qE4SpAJWFaLkdRyE0FjTPBBRYE7LOvcmRCB5p86W73Q=
|
github.com/lixenwraith/config v0.0.0-20250701170607-8515fa0543b6 h1:qE4SpAJWFaLkdRyE0FjTPBBRYE7LOvcmRCB5p86W73Q=
|
||||||
github.com/lixenwraith/config v0.0.0-20250701170607-8515fa0543b6/go.mod h1:4wPJ3HnLrYrtUwTinngCsBgtdIXsnxkLa7q4KAIbwY8=
|
github.com/lixenwraith/config v0.0.0-20250701170607-8515fa0543b6/go.mod h1:4wPJ3HnLrYrtUwTinngCsBgtdIXsnxkLa7q4KAIbwY8=
|
||||||
|
github.com/lixenwraith/config v0.0.0-20250712170030-7d38402e0497 h1:ixTIdJSd945n/IhMRwGwQVmQnQ1nUr5z1wn31jXq9FU=
|
||||||
|
github.com/lixenwraith/config v0.0.0-20250712170030-7d38402e0497/go.mod h1:y7kgDrWIFROWJJ6ASM/SPTRRAj27FjRGWh2SDLcdQ68=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
|
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
|
||||||
|
|||||||
@ -174,6 +174,11 @@ func (l *Logger) applyAndReconfigureLocked() error {
|
|||||||
return fmtErrorf("failed to create log directory '%s': %w", dir, err)
|
return fmtErrorf("failed to create log directory '%s': %w", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update serializer format when config changes
|
||||||
|
if tsFormat, err := l.config.String("log.timestamp_format"); err == nil && tsFormat != "" {
|
||||||
|
l.serializer.setTimestampFormat(tsFormat)
|
||||||
|
}
|
||||||
|
|
||||||
// Get current state
|
// Get current state
|
||||||
wasInitialized := l.state.IsInitialized.Load()
|
wasInitialized := l.state.IsInitialized.Load()
|
||||||
disableFile, _ := l.config.Bool("log.disable_file")
|
disableFile, _ := l.config.Bool("log.disable_file")
|
||||||
@ -281,6 +286,7 @@ func (l *Logger) loadCurrentConfig() *Config {
|
|||||||
cfg.Extension, _ = l.config.String("log.extension")
|
cfg.Extension, _ = l.config.String("log.extension")
|
||||||
cfg.ShowTimestamp, _ = l.config.Bool("log.show_timestamp")
|
cfg.ShowTimestamp, _ = l.config.Bool("log.show_timestamp")
|
||||||
cfg.ShowLevel, _ = l.config.Bool("log.show_level")
|
cfg.ShowLevel, _ = l.config.Bool("log.show_level")
|
||||||
|
cfg.TimestampFormat, _ = l.config.String("log.timestamp_format")
|
||||||
cfg.BufferSize, _ = l.config.Int64("log.buffer_size")
|
cfg.BufferSize, _ = l.config.Int64("log.buffer_size")
|
||||||
cfg.MaxSizeMB, _ = l.config.Int64("log.max_size_mb")
|
cfg.MaxSizeMB, _ = l.config.Int64("log.max_size_mb")
|
||||||
cfg.MaxTotalSizeMB, _ = l.config.Int64("log.max_total_size_mb")
|
cfg.MaxTotalSizeMB, _ = l.config.Int64("log.max_total_size_mb")
|
||||||
|
|||||||
163
storage.go
163
storage.go
@ -186,18 +186,17 @@ func (l *Logger) getLogDirSize(dir, fileExt string) (int64, error) {
|
|||||||
func (l *Logger) cleanOldLogs(required int64) error {
|
func (l *Logger) cleanOldLogs(required int64) error {
|
||||||
dir, _ := l.config.String("log.directory")
|
dir, _ := l.config.String("log.directory")
|
||||||
fileExt, _ := l.config.String("log.extension")
|
fileExt, _ := l.config.String("log.extension")
|
||||||
|
name, _ := l.config.String("log.name")
|
||||||
|
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmtErrorf("failed to read log directory '%s' for cleanup: %w", dir, err)
|
return fmtErrorf("failed to read log directory '%s' for cleanup: %w", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentLogFileName := ""
|
// Get the static log filename to exclude from deletion
|
||||||
cfPtr := l.state.CurrentFile.Load()
|
staticLogName := name
|
||||||
if cfPtr != nil {
|
if fileExt != "" {
|
||||||
if clf, ok := cfPtr.(*os.File); ok && clf != nil {
|
staticLogName = name + "." + fileExt
|
||||||
currentLogFileName = filepath.Base(clf.Name())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type logFileMeta struct {
|
type logFileMeta struct {
|
||||||
@ -208,7 +207,10 @@ func (l *Logger) cleanOldLogs(required int64) error {
|
|||||||
var logs []logFileMeta
|
var logs []logFileMeta
|
||||||
targetExt := "." + fileExt
|
targetExt := "." + fileExt
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if entry.IsDir() || filepath.Ext(entry.Name()) != targetExt || entry.Name() == currentLogFileName {
|
if entry.IsDir() || entry.Name() == staticLogName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if fileExt != "" && filepath.Ext(entry.Name()) != targetExt {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
info, errInfo := entry.Info()
|
info, errInfo := entry.Info()
|
||||||
@ -251,7 +253,7 @@ func (l *Logger) cleanOldLogs(required int64) error {
|
|||||||
func (l *Logger) updateEarliestFileTime() {
|
func (l *Logger) updateEarliestFileTime() {
|
||||||
dir, _ := l.config.String("log.directory")
|
dir, _ := l.config.String("log.directory")
|
||||||
fileExt, _ := l.config.String("log.extension")
|
fileExt, _ := l.config.String("log.extension")
|
||||||
baseName, _ := l.config.String("log.name")
|
name, _ := l.config.String("log.name")
|
||||||
|
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -260,22 +262,24 @@ func (l *Logger) updateEarliestFileTime() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var earliest time.Time
|
var earliest time.Time
|
||||||
currentLogFileName := ""
|
// Get the active log filename to exclude from timestamp tracking
|
||||||
cfPtr := l.state.CurrentFile.Load()
|
staticLogName := name
|
||||||
if cfPtr != nil {
|
if fileExt != "" {
|
||||||
if clf, ok := cfPtr.(*os.File); ok && clf != nil {
|
staticLogName = name + "." + fileExt
|
||||||
currentLogFileName = filepath.Base(clf.Name())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
targetExt := "." + fileExt
|
targetExt := "." + fileExt
|
||||||
prefix := baseName + "_"
|
prefix := name + "_"
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if entry.IsDir() {
|
if entry.IsDir() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fname := entry.Name()
|
fname := entry.Name()
|
||||||
if !strings.HasPrefix(fname, prefix) || filepath.Ext(fname) != targetExt || fname == currentLogFileName {
|
// Skip the active log file
|
||||||
|
if fname == staticLogName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(fname, prefix) || (fileExt != "" && filepath.Ext(fname) != targetExt) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
info, errInfo := entry.Info()
|
info, errInfo := entry.Info()
|
||||||
@ -293,6 +297,7 @@ func (l *Logger) updateEarliestFileTime() {
|
|||||||
func (l *Logger) cleanExpiredLogs(oldest time.Time) error {
|
func (l *Logger) cleanExpiredLogs(oldest time.Time) error {
|
||||||
dir, _ := l.config.String("log.directory")
|
dir, _ := l.config.String("log.directory")
|
||||||
fileExt, _ := l.config.String("log.extension")
|
fileExt, _ := l.config.String("log.extension")
|
||||||
|
name, _ := l.config.String("log.name")
|
||||||
retentionPeriodHrs, _ := l.config.Float64("log.retention_period_hrs")
|
retentionPeriodHrs, _ := l.config.Float64("log.retention_period_hrs")
|
||||||
rpDuration := time.Duration(retentionPeriodHrs * float64(time.Hour))
|
rpDuration := time.Duration(retentionPeriodHrs * float64(time.Hour))
|
||||||
|
|
||||||
@ -309,18 +314,20 @@ func (l *Logger) cleanExpiredLogs(oldest time.Time) error {
|
|||||||
return fmtErrorf("failed to read log directory '%s' for retention cleanup: %w", dir, err)
|
return fmtErrorf("failed to read log directory '%s' for retention cleanup: %w", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentLogFileName := ""
|
// Get the active log filename to exclude from deletion
|
||||||
cfPtr := l.state.CurrentFile.Load()
|
staticLogName := name
|
||||||
if cfPtr != nil {
|
if fileExt != "" {
|
||||||
if clf, ok := cfPtr.(*os.File); ok && clf != nil {
|
staticLogName = name + "." + fileExt
|
||||||
currentLogFileName = filepath.Base(clf.Name())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
targetExt := "." + fileExt
|
targetExt := "." + fileExt
|
||||||
var deletedCount int
|
var deletedCount int
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if entry.IsDir() || filepath.Ext(entry.Name()) != targetExt || entry.Name() == currentLogFileName {
|
if entry.IsDir() || entry.Name() == staticLogName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Only consider files with correct extension
|
||||||
|
if fileExt != "" && filepath.Ext(entry.Name()) != targetExt {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
info, errInfo := entry.Info()
|
info, errInfo := entry.Info()
|
||||||
@ -344,29 +351,37 @@ func (l *Logger) cleanExpiredLogs(oldest time.Time) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateLogFileName creates a unique log filename using a timestamp
|
// getStaticLogFilePath returns the full path to the active log file
|
||||||
func (l *Logger) generateLogFileName(timestamp time.Time) string {
|
func (l *Logger) getStaticLogFilePath() string {
|
||||||
|
dir, _ := l.config.String("log.directory")
|
||||||
|
name, _ := l.config.String("log.name")
|
||||||
|
ext, _ := l.config.String("log.extension")
|
||||||
|
|
||||||
|
// Handle extension with or without dot
|
||||||
|
filename := name
|
||||||
|
if ext != "" {
|
||||||
|
filename = name + "." + ext
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(dir, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateArchiveLogFileName creates a timestamped filename for archived logs during rotation
|
||||||
|
func (l *Logger) generateArchiveLogFileName(timestamp time.Time) string {
|
||||||
name, _ := l.config.String("log.name")
|
name, _ := l.config.String("log.name")
|
||||||
ext, _ := l.config.String("log.extension")
|
ext, _ := l.config.String("log.extension")
|
||||||
tsFormat := timestamp.Format("060102_150405")
|
tsFormat := timestamp.Format("060102_150405")
|
||||||
nano := timestamp.Nanosecond()
|
nano := timestamp.Nanosecond()
|
||||||
return fmt.Sprintf("%s_%s_%d.%s", name, tsFormat, nano, ext)
|
|
||||||
|
if ext != "" {
|
||||||
|
return fmt.Sprintf("%s_%s_%d.%s", name, tsFormat, nano, ext)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s_%s_%d", name, tsFormat, nano)
|
||||||
}
|
}
|
||||||
|
|
||||||
// createNewLogFile generates a unique name and opens a new log file
|
// createNewLogFile generates a unique name and opens a new log file
|
||||||
func (l *Logger) createNewLogFile() (*os.File, error) {
|
func (l *Logger) createNewLogFile() (*os.File, error) {
|
||||||
dir, _ := l.config.String("log.directory")
|
fullPath := l.getStaticLogFilePath()
|
||||||
filename := l.generateLogFileName(time.Now())
|
|
||||||
fullPath := filepath.Join(dir, filename)
|
|
||||||
|
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(1 * time.Millisecond)
|
|
||||||
filename := l.generateLogFileName(time.Now())
|
|
||||||
fullPath = filepath.Join(dir, filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := os.OpenFile(fullPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
file, err := os.OpenFile(fullPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -375,27 +390,71 @@ func (l *Logger) createNewLogFile() (*os.File, error) {
|
|||||||
return file, nil
|
return file, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// rotateLogFile handles closing the current log file and opening a new one
|
// rotateLogFile implements the rename-on-rotate strategy
|
||||||
|
// Closes current file, renames it with timestamp, creates new static file
|
||||||
func (l *Logger) rotateLogFile() error {
|
func (l *Logger) rotateLogFile() error {
|
||||||
|
// Get current file handle
|
||||||
|
cfPtr := l.state.CurrentFile.Load()
|
||||||
|
if cfPtr == nil {
|
||||||
|
// No current file, just create a new one
|
||||||
|
newFile, err := l.createNewLogFile()
|
||||||
|
if err != nil {
|
||||||
|
return fmtErrorf("failed to create log file during rotation: %w", err)
|
||||||
|
}
|
||||||
|
l.state.CurrentFile.Store(newFile)
|
||||||
|
l.state.CurrentSize.Store(0)
|
||||||
|
l.state.TotalRotations.Add(1)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentFile, ok := cfPtr.(*os.File)
|
||||||
|
if !ok || currentFile == nil {
|
||||||
|
// Invalid file handle, create new one
|
||||||
|
newFile, err := l.createNewLogFile()
|
||||||
|
if err != nil {
|
||||||
|
return fmtErrorf("failed to create log file during rotation: %w", err)
|
||||||
|
}
|
||||||
|
l.state.CurrentFile.Store(newFile)
|
||||||
|
l.state.CurrentSize.Store(0)
|
||||||
|
l.state.TotalRotations.Add(1)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close current file before renaming
|
||||||
|
if err := currentFile.Close(); err != nil {
|
||||||
|
l.internalLog("failed to close log file before rotation: %v\n", err)
|
||||||
|
// Continue with rotation anyway
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate archive filename with current timestamp
|
||||||
|
dir, _ := l.config.String("log.directory")
|
||||||
|
archiveName := l.generateArchiveLogFileName(time.Now())
|
||||||
|
archivePath := filepath.Join(dir, archiveName)
|
||||||
|
|
||||||
|
// Rename current file to archive name
|
||||||
|
currentPath := l.getStaticLogFilePath()
|
||||||
|
if err := os.Rename(currentPath, archivePath); err != nil {
|
||||||
|
// The original file is closed and couldn't be renamed. This is a terminal state for file logging.
|
||||||
|
l.internalLog("failed to rename log file from '%s' to '%s': %v. file logging disabled.",
|
||||||
|
currentPath, archivePath, err)
|
||||||
|
l.state.LoggerDisabled.Store(true)
|
||||||
|
return fmtErrorf("failed to rotate log file, logging is disabled: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new log file at static path
|
||||||
newFile, err := l.createNewLogFile()
|
newFile, err := l.createNewLogFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmtErrorf("failed to create new log file for rotation: %w", err)
|
return fmtErrorf("failed to create new log file after rotation: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
oldFilePtr := l.state.CurrentFile.Swap(newFile)
|
// Update state
|
||||||
|
l.state.CurrentFile.Store(newFile)
|
||||||
l.state.CurrentSize.Store(0)
|
l.state.CurrentSize.Store(0)
|
||||||
|
|
||||||
if oldFilePtr != nil {
|
|
||||||
if oldFile, ok := oldFilePtr.(*os.File); ok && oldFile != nil {
|
|
||||||
if err := oldFile.Close(); err != nil {
|
|
||||||
l.internalLog("failed to close old log file '%s': %v\n", oldFile.Name(), err)
|
|
||||||
// Continue with new file anyway
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
l.updateEarliestFileTime()
|
|
||||||
l.state.TotalRotations.Add(1)
|
l.state.TotalRotations.Add(1)
|
||||||
|
|
||||||
|
// Update earliest file time after successful rotation
|
||||||
|
l.updateEarliestFileTime()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -109,6 +109,10 @@ func validateConfigValue(key string, value interface{}) error {
|
|||||||
if v, ok := value.(string); ok && strings.HasPrefix(v, ".") {
|
if v, ok := value.(string); ok && strings.HasPrefix(v, ".") {
|
||||||
return fmtErrorf("extension should not start with dot: %s", v)
|
return fmtErrorf("extension should not start with dot: %s", v)
|
||||||
}
|
}
|
||||||
|
case "timestamp_format":
|
||||||
|
if v, ok := value.(string); ok && strings.TrimSpace(v) == "" {
|
||||||
|
return fmtErrorf("timestamp_format cannot be empty")
|
||||||
|
}
|
||||||
case "buffer_size":
|
case "buffer_size":
|
||||||
if v, ok := value.(int64); ok && v <= 0 {
|
if v, ok := value.(int64); ok && v <= 0 {
|
||||||
return fmtErrorf("buffer_size must be positive: %d", v)
|
return fmtErrorf("buffer_size must be positive: %d", v)
|
||||||
|
|||||||
Reference in New Issue
Block a user