Skip to content

Commit

Permalink
✨ feat: rotatefile - add new config RotateMode, support create logfil…
Browse files Browse the repository at this point in the history
…e on rotate time.
  • Loading branch information
inhere committed Jun 15, 2023
1 parent 8468ea4 commit 8c4a9a0
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 58 deletions.
13 changes: 13 additions & 0 deletions rotatefile/README.md
Expand Up @@ -2,6 +2,16 @@

`rotatefile` provides simple file rotation, compression and cleanup.

## Features

- Rotate file by size and time
- Custom filename for rotate file by size
- Custom time clock for rotate
- Custom file perm for create log file
- Custom rotate mode: create, rename
- Compress rotated file
- Cleanup old files

## Install

```bash
Expand All @@ -18,6 +28,9 @@ writer, err := rotatefile.NewConfig(logFile).Create()
if err != nil {
panic(err)
}

// use writer
writer.Write([]byte("log message\n"))
```

### Use on another logger
Expand Down
17 changes: 10 additions & 7 deletions rotatefile/clean_files.go → rotatefile/cleanup.go
Expand Up @@ -25,11 +25,14 @@ type CConfig struct {
// FileDirs list
FileDirs []string `json:"file_dirs" yaml:"file_dirs"`

// Patterns list. filename match patterns. eg: error.log.*
Patterns []string `json:"patterns" yaml:"patterns"`

// TimeClock for clean files
TimeClock Clocker

// skip error
// TODO skipError bool
// ignore error
// TODO ignoreError bool
}

// AddFileDir for clean
Expand All @@ -43,6 +46,7 @@ func NewCConfig() *CConfig {
return &CConfig{
BackupNum: DefaultBackNum,
BackupTime: DefaultBackTime,
TimeClock: DefaultTimeClockFn,
}
}

Expand All @@ -54,7 +58,7 @@ type FilesClear struct {

namePattern string
filepathDirs []string
// full file path patterns
// full file path glob patterns
filePatterns []string
// file max backup time. equals CConfig.BackupTime * time.Hour
backupDur time.Duration
Expand Down Expand Up @@ -91,7 +95,8 @@ func (r *FilesClear) CleanDaemon() {
return
}

for range time.NewTicker(flushInterval).C {
t := time.NewTicker(flushInterval)
for range t.C {
if err := r.Clean(); err != nil {
printErrln("files-clear: clean old files error:", err)
}
Expand Down Expand Up @@ -145,7 +150,7 @@ func (r *FilesClear) Clean() (err error) {

func (r *FilesClear) cleanByBackupNum(filePattern string, bckNum int) (err error) {
keepNum := 0
err = fsutil.GlobWithFunc(filePattern, func(oldFile string) error {
return fsutil.GlobWithFunc(filePattern, func(oldFile string) error {
stat, err := os.Stat(oldFile)
if err != nil {
return err
Expand All @@ -162,8 +167,6 @@ func (r *FilesClear) cleanByBackupNum(filePattern string, bckNum int) (err error
// remove expired files
return os.Remove(oldFile)
})

return
}

func (r *FilesClear) cleanByBackupTime(filePattern string, cutTime time.Time) (err error) {
Expand Down
4 changes: 4 additions & 0 deletions rotatefile/clean_files_test.go → rotatefile/cleanup_test.go
Expand Up @@ -3,6 +3,7 @@ package rotatefile_test
import (
"testing"

"github.com/gookit/goutil/testutil/assert"
"github.com/gookit/slog/rotatefile"
)

Expand All @@ -11,4 +12,7 @@ func TestNewFilesClear(t *testing.T) {
fc.WithConfigFn(func(c *rotatefile.CConfig) {
c.AddFileDir("./testdata")
})

err := fc.Clean()
assert.NoErr(t, err)
}
24 changes: 8 additions & 16 deletions rotatefile/config.go
Expand Up @@ -143,12 +143,15 @@ type ConfigFn func(c *Config)

// Config struct for rotate dispatcher
type Config struct {
// Filepath the log file path, will be rotating
// Filepath the log file path, will be rotating. eg: "logs/error.log"
Filepath string `json:"filepath" yaml:"filepath"`

// FilePerm for create log file. default DefaultFilePerm
FilePerm os.FileMode `json:"file_perm" yaml:"file_perm"`

// RotateMode for rotate file. default ModeRename
RotateMode RotateMode `json:"rotate_mode" yaml:"rotate_mode"`

// MaxSize file contents max size, unit is bytes.
// If is equals zero, disable rotate file by size
//
Expand All @@ -158,7 +161,7 @@ type Config struct {
// RotateTime the file rotate interval time, unit is seconds.
// If is equals zero, disable rotate file by time
//
// default see EveryHour
// default: EveryHour
RotateTime RotateTime `json:"rotate_time" yaml:"rotate_time"`

// CloseLock use sync lock on write contents, rotating file.
Expand Down Expand Up @@ -193,8 +196,6 @@ func (c *Config) backupDuration() time.Duration {
if c.BackupTime < 1 {
return 0
}

// return int64(c.BackupTime) * 3600
return time.Duration(c.BackupTime) * time.Hour
}

Expand All @@ -209,17 +210,8 @@ func (c *Config) With(fns ...ConfigFn) *Config {
// Create new Writer by config
func (c *Config) Create() (*Writer, error) { return NewWriter(c) }

const (
// OneMByte size
OneMByte uint64 = 1024 * 1024

// DefaultMaxSize of a log file. default is 20M.
DefaultMaxSize = 20 * OneMByte
// DefaultBackNum default backup numbers for old files.
DefaultBackNum uint = 20
// DefaultBackTime default backup time for old files. default keep a week.
DefaultBackTime uint = 24 * 7
)
// IsMode check rotate mode
func (c *Config) IsMode(m RotateMode) bool { return c.RotateMode == m }

var (
// DefaultFilePerm perm and flags for create log file
Expand All @@ -232,7 +224,7 @@ var (
suffix := time.Now().Format("010215")

// eg: /tmp/error.log => /tmp/error.log.163021_0001
return filepath + fmt.Sprintf(".%s_%04d", suffix, rotateNum)
return filepath + fmt.Sprintf(".%s_%03d", suffix, rotateNum)
}

// DefaultTimeClockFn for create time
Expand Down
29 changes: 29 additions & 0 deletions rotatefile/config_test.go
Expand Up @@ -26,6 +26,7 @@ func TestNewConfig(t *testing.T) {
assert.Eq(t, rotatefile.DefaultBackTime, cfg.BackupTime)
assert.Eq(t, rotatefile.EveryHour, cfg.RotateTime)
assert.Eq(t, rotatefile.DefaultMaxSize, cfg.MaxSize)
assert.Eq(t, rotatefile.ModeRename, cfg.RotateMode)

dump.P(cfg)

Expand All @@ -37,6 +38,34 @@ func TestNewConfig(t *testing.T) {
assert.Eq(t, uint(0), cfg.BackupTime)
}

func TestRotateMode_String(t *testing.T) {
assert.Eq(t, "rename", rotatefile.ModeRename.String())
assert.Eq(t, "create", rotatefile.ModeCreate.String())
assert.Eq(t, "unknown", rotatefile.RotateMode(9).String())
}

func TestRotateTime_TimeFormat(t *testing.T) {
now := timex.Now()

rt := rotatefile.EveryDay
assert.Eq(t, "20060102", rt.TimeFormat())
ft := rt.FirstCheckTime(now.T())
assert.Eq(t, now.DayEnd().Unix(), ft)

rt = rotatefile.EveryHour
assert.Eq(t, "20060102_1500", rt.TimeFormat())

rt = rotatefile.Every15Min
assert.Eq(t, "20060102_1504", rt.TimeFormat())
ft = rt.FirstCheckTime(now.T())
assert.Gt(t, ft, 0)

rt = rotatefile.EverySecond
assert.Eq(t, "20060102_150405", rt.TimeFormat())
ft = rt.FirstCheckTime(now.T())
assert.Eq(t, now.Unix()+rt.Interval(), ft)
}

func TestRotateTime_String(t *testing.T) {
assert.Eq(t, "Every 1 Day", rotatefile.EveryDay.String())
assert.Eq(t, "Every 1 Hours", rotatefile.EveryHour.String())
Expand Down
43 changes: 43 additions & 0 deletions rotatefile/rotatefile.go
Expand Up @@ -16,6 +16,49 @@ type RotateWriter interface {
Sync() error
}

// RotateMode for rotate file
type RotateMode uint8

// String get string name
func (m RotateMode) String() string {
switch m {
case ModeRename:
return "rename"
case ModeCreate:
return "create"
default:
return "unknown"
}
}

const (
// ModeRename rotating file by rename.
//
// Example flow:
// - always write to "error.log"
// - rotating by rename it to "error.log.20201223"
// - then re-create "error.log"
ModeRename RotateMode = iota

// ModeCreate rotating file by create new file.
//
// Example flow:
// - directly create new file on each rotate time. eg: "error.log.20201223", "error.log.20201224"
ModeCreate
)

const (
// OneMByte size
OneMByte uint64 = 1024 * 1024

// DefaultMaxSize of a log file. default is 20M.
DefaultMaxSize = 20 * OneMByte
// DefaultBackNum default backup numbers for old files.
DefaultBackNum uint = 20
// DefaultBackTime default backup time for old files. default keep a week.
DefaultBackTime uint = 24 * 7
)

func printErrln(args ...any) {
_, _ = fmt.Fprintln(os.Stderr, args...)
}
2 changes: 1 addition & 1 deletion rotatefile/util.go
Expand Up @@ -37,14 +37,14 @@ func compressFile(srcPath, dstPath string) error {
if _, err = io.Copy(zw, srcFile); err != nil {
return err
}

return zw.Close()
}

type filterFunc func(fPath string, fi os.FileInfo) bool
type handleFunc func(fPath string, fi os.FileInfo) error

// from the go pkg: path/filepath.glob()
// TODO replace use fsutil.FindInDir()
func findFilesInDir(dir string, handleFn handleFunc, filters ...filterFunc) (err error) {
fi, err := os.Stat(dir)
if err != nil {
Expand Down

0 comments on commit 8c4a9a0

Please sign in to comment.