Skip to content

Commit

Permalink
log: added option to rotate logs based on number of hours
Browse files Browse the repository at this point in the history
  • Loading branch information
bnb-alexlucaci committed Jun 29, 2023
1 parent 2426a5d commit e1e2a7c
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 42 deletions.
41 changes: 20 additions & 21 deletions log/async_file_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,49 +15,48 @@ type TimeTicker struct {
C <-chan time.Time
}

// NewTimeTicker notifies on a daily basis if dailyRotate is true, otherwise on an hourly basis
func NewTimeTicker(dailyRotate bool) *TimeTicker {
// NewTimeTicker creates a TimeTicker that notifies based on rotateHours parameter.
// if rotateHours is 1 and current time is 11:32 it means that the ticker will tick at 12:00
// if rotateHours is 5 and current time is 09:12 means that the ticker will tick at 11:00
func NewTimeTicker(rotateHours int) *TimeTicker {
ch := make(chan time.Time)
ht := TimeTicker{
tt := TimeTicker{
stop: make(chan struct{}),
C: ch,
}

ht.startTicker(ch, dailyRotate)
tt.startTicker(ch, rotateHours)

return &ht
return &tt
}

func (ht *TimeTicker) Stop() {
ht.stop <- struct{}{}
func (tt *TimeTicker) Stop() {
tt.stop <- struct{}{}
}

func (ht *TimeTicker) startTicker(ch chan time.Time, dailyRotate bool) {
func (tt *TimeTicker) startTicker(ch chan time.Time, rotateHours int) {
go func() {
day := time.Now().Day()
hour := time.Now().Hour()
nextRotationHour := getNextRotationHour(time.Now(), rotateHours)
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case t := <-ticker.C:
if dailyRotate && t.Day() != day {
if t.Hour() == nextRotationHour {
ch <- t
day = t.Day()
continue
nextRotationHour = getNextRotationHour(time.Now(), rotateHours)
}

if t.Hour() != hour {
ch <- t
hour = t.Hour()
}
case <-ht.stop:
case <-tt.stop:
return
}
}
}()
}

func getNextRotationHour(now time.Time, delta int) int {
return now.Add(time.Hour * time.Duration(delta)).Hour()
}

type AsyncFileWriter struct {
filePath string
fd *os.File
Expand All @@ -69,7 +68,7 @@ type AsyncFileWriter struct {
timeTicker *TimeTicker
}

func NewAsyncFileWriter(filePath string, maxBytesSize int64, dailyRotate bool) *AsyncFileWriter {
func NewAsyncFileWriter(filePath string, maxBytesSize int64, rotateHours int) *AsyncFileWriter {
absFilePath, err := filepath.Abs(filePath)
if err != nil {
panic(fmt.Sprintf("get file path of logger error. filePath=%s, err=%s", filePath, err))
Expand All @@ -79,7 +78,7 @@ func NewAsyncFileWriter(filePath string, maxBytesSize int64, dailyRotate bool) *
filePath: absFilePath,
buf: make(chan []byte, maxBytesSize),
stop: make(chan struct{}),
timeTicker: NewTimeTicker(dailyRotate),
timeTicker: NewTimeTicker(rotateHours),
}
}

Expand Down
56 changes: 41 additions & 15 deletions log/async_file_writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package log

import (
"os"
"strconv"
"strings"
"testing"
"time"
)

func TestWriterHourly(t *testing.T) {
w := NewAsyncFileWriter("./hello.log", 100, false)
w := NewAsyncFileWriter("./hello.log", 100, 1)
w.Start()
w.Write([]byte("hello\n"))
w.Write([]byte("world\n"))
Expand All @@ -24,20 +26,44 @@ func TestWriterHourly(t *testing.T) {
}
}

func TestWriterDaily(t *testing.T) {
w := NewAsyncFileWriter("./hello.log", 100, true)
w.Start()
w.Write([]byte("hello\n"))
w.Write([]byte("world\n"))
w.Stop()
files, _ := os.ReadDir("./")
for _, f := range files {
fn := f.Name()
if strings.HasPrefix(fn, "hello") {
t.Log(fn)
content, _ := os.ReadFile(fn)
t.Log(content)
os.Remove(fn)
func TestGetNextRotationHour(t *testing.T) {
tcs := []struct {
now time.Time
delta int
expectedHour int
}{
{
now: time.Date(1980, 1, 6, 15, 34, 0, 0, time.UTC),
delta: 3,
expectedHour: 18,
},
{
now: time.Date(1980, 1, 6, 23, 59, 0, 0, time.UTC),
delta: 1,
expectedHour: 0,
},
{
now: time.Date(1980, 1, 6, 22, 15, 0, 0, time.UTC),
delta: 2,
expectedHour: 0,
},
{
now: time.Date(1980, 1, 6, 0, 0, 0, 0, time.UTC),
delta: 1,
expectedHour: 1,
},
}

test := func(now time.Time, delta, expectedHour int) func(*testing.T) {
return func(t *testing.T) {
got := getNextRotationHour(now, delta)
if got != expectedHour {
t.Fatalf("Expected %d, found: %d\n", expectedHour, got)
}
}
}

for i, tc := range tcs {
t.Run("TestGetNextRotationHour_"+strconv.Itoa(i), test(tc.now, tc.delta, tc.expectedHour))
}
}
4 changes: 2 additions & 2 deletions log/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,14 @@ func FileHandler(path string, fmtr Format) (Handler, error) {
// RotatingFileHandler returns a handler which writes log records to file chunks
// at the given path. When a file's size reaches the limit, the handler creates
// a new file named after the timestamp of the first log record it will contain.
func RotatingFileHandler(filePath string, limit uint, formatter Format, dailyRotate bool) (Handler, error) {
func RotatingFileHandler(filePath string, limit uint, formatter Format, rotateHours int) (Handler, error) {
if _, err := os.Stat(path.Dir(filePath)); os.IsNotExist(err) {
err := os.MkdirAll(path.Dir(filePath), 0755)
if err != nil {
return nil, fmt.Errorf("could not create directory %s, %v", path.Dir(filePath), err)
}
}
fileWriter := NewAsyncFileWriter(filePath, int64(limit), dailyRotate)
fileWriter := NewAsyncFileWriter(filePath, int64(limit), rotateHours)
fileWriter.Start()
return StreamHandler(fileWriter, formatter), nil
}
Expand Down
4 changes: 2 additions & 2 deletions log/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,8 @@ func (c Ctx) toArray() []interface{} {
return arr
}

func NewFileLvlHandler(logPath string, maxBytesSize uint, level string, dailyRotate bool) Handler {
rfh, err := RotatingFileHandler(logPath, maxBytesSize, LogfmtFormat(), dailyRotate)
func NewFileLvlHandler(logPath string, maxBytesSize uint, level string, rotateHours int) Handler {
rfh, err := RotatingFileHandler(logPath, maxBytesSize, LogfmtFormat(), rotateHours)
if err != nil {
panic(err)
}
Expand Down
2 changes: 1 addition & 1 deletion node/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ type LogConfig struct {
FilePath *string `toml:",omitempty"`
MaxBytesSize *uint `toml:",omitempty"`
Level *string `toml:",omitempty"`
DailyRotate bool `toml:",omitempty"`
RotateHours int `toml:",omitempty"`

// TermTimeFormat is the time format used for console logging.
TermTimeFormat *string `toml:",omitempty"`
Expand Down
11 changes: 10 additions & 1 deletion node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,16 @@ func New(conf *Config) (*Node, error) {
logFilePath = path.Join(*conf.LogConfig.FileRoot, *conf.LogConfig.FilePath)
}

log.Root().SetHandler(log.NewFileLvlHandler(logFilePath, *conf.LogConfig.MaxBytesSize, *conf.LogConfig.Level, conf.LogConfig.DailyRotate))
if conf.LogConfig.RotateHours > 24 {
return nil, errors.New("Config.LogConfig.RotateHours cannot be greater than 24")
}

// To maintain backwards compatibility, if RotateHours is not set or set to a negative value, then it defaults to 1
if conf.LogConfig.RotateHours < 1 {
conf.LogConfig.RotateHours = 1
}

log.Root().SetHandler(log.NewFileLvlHandler(logFilePath, *conf.LogConfig.MaxBytesSize, *conf.LogConfig.Level, conf.LogConfig.RotateHours))
}
}
if conf.Logger == nil {
Expand Down

0 comments on commit e1e2a7c

Please sign in to comment.