Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: scheduler log rotation #187

Merged
merged 5 commits into from
Jul 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ It runs [DAGs (Directed acyclic graph)](https://en.wikipedia.org/wiki/Directed_a
- [Admin Configuration](#admin-configuration)
- [Environment Variables](#environment-variables-1)
- [Web UI Configuration](#web-ui-configuration)
- [Scheduler Log](#scheduler-log)
- [Global Configuration](#global-configuration)
- [REST API Interface](#rest-api-interface)
- [FAQ](#faq)
Expand Down Expand Up @@ -405,6 +406,14 @@ basicAuthUsername: <username for basic auth of web UI> # [optional] basic
basicAuthPassword: <password for basic auth of web UI> # [optional] basic auth config
```

### Scheduler Log

The default path is `~/.dagu/logs/`. If you want to change this, you can set `log` field in `~/.dagu/admin.yaml`.

```yaml
logs: <path-to-log-files-for-scheduler>
```

### Global Configuration

Creating a global configuration `~/.dagu/config.yaml` is a convenient way to organize shared settings.
Expand Down
101 changes: 101 additions & 0 deletions internal/logger/simple.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package logger

import (
"fmt"
"io"
"os"
"path"
"sync"
"time"

"github.com/yohamta/dagu/internal/utils"
)

type SimpleLogger struct {
Dir string
LogName string
Period time.Duration
mu sync.Mutex
file *os.File
stop chan struct{}
}

var _ io.Writer = (*SimpleLogger)(nil)

func NewSimpleLogger(dir, logName string, period time.Duration) *SimpleLogger {
return &SimpleLogger{
Dir: dir,
LogName: logName,
Period: period,
stop: make(chan struct{}),
}
}

func (rl *SimpleLogger) Open() error {
err := rl.setupFile()
if err != nil {
return err
}
go func() {
timer := time.NewTimer(time.Until(rl.timeToSwitchLog()))
for {
select {
case <-timer.C:
rl.mu.Lock()
err := rl.setupFile()
utils.LogErr("setup log file", err)
timer = time.NewTimer(time.Until(rl.timeToSwitchLog()))
rl.mu.Unlock()
case <-rl.stop:
timer.Stop()
return
}
}
}()
return nil
}

func (rl *SimpleLogger) Write(p []byte) (n int, err error) {
rl.mu.Lock()
defer rl.mu.Unlock()
if rl.file != nil {
return rl.file.Write(p)
}
return 0, nil
}

func (rl *SimpleLogger) Close() (err error) {
rl.mu.Lock()
defer rl.mu.Unlock()
err = rl.closeFile()
rl.stop <- struct{}{}
return nil
}

func (rl *SimpleLogger) setupFile() (err error) {
rl.closeFile()
filename := path.Join(
rl.Dir,
fmt.Sprintf("%s%s.log",
rl.LogName,
time.Now().Format("20060102.15:04:05.000"),
))
dir := path.Dir(filename)
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
rl.file, err = utils.OpenOrCreateFile(filename)
return
}

func (rl *SimpleLogger) closeFile() (err error) {
if rl.file != nil {
_ = rl.file.Sync()
err = rl.file.Close()
}
return
}

func (rl *SimpleLogger) timeToSwitchLog() time.Time {
return time.Now().Add(rl.Period).Truncate(rl.Period)
}
63 changes: 63 additions & 0 deletions internal/logger/simple_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package logger

import (
"os"
"path"
"sort"
"testing"
"time"

"github.com/stretchr/testify/require"
"github.com/yohamta/dagu/internal/utils"
)

func TestSimpleLogger(t *testing.T) {
tmpDir := utils.MustTempDir("test-simple-logger")
defer func() {
_ = os.RemoveAll(tmpDir)
}()

rl := NewSimpleLogger(tmpDir, "test", time.Millisecond*300)
rl.Open()

_, err := rl.Write([]byte("test log\n"))
require.NoError(t, err)

time.Sleep(time.Millisecond * 300)

_, err = rl.Write([]byte("test log2\n"))
require.NoError(t, err)

_ = rl.Close()
time.Sleep(time.Millisecond * 100)

f, err := os.Open(tmpDir)
require.NoError(t, err)
defer func() {
_ = f.Close()
}()

fis, _ := f.Readdir(0)
require.Equal(t, 2, len(fis))
for _, fi := range fis {
require.Regexp(t, "test\\d{8}.\\d{2}:\\d{2}:\\d{2}.\\d{3}.log", fi.Name())
}

sort.Slice(fis, func(i, j int) bool {
return fis[i].Name() < fis[j].Name()
})

b, _ := os.ReadFile(path.Join(tmpDir, fis[0].Name()))
require.Equal(t, "test log\n", string(b))

b, _ = os.ReadFile(path.Join(tmpDir, fis[1].Name()))
require.Equal(t, "test log2\n", string(b))
}

func TestTimeToSwitchLog(t *testing.T) {
rl := NewSimpleLogger("", "test", time.Hour*24)
tm := rl.timeToSwitchLog()
d := time.Hour*24 - time.Until(tm)
require.GreaterOrEqual(t, d, time.Duration(0))
require.LessOrEqual(t, time.Second, d)
}
16 changes: 9 additions & 7 deletions internal/runner/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import (

type Agent struct {
*admin.Config
stop chan struct{}
logFile *os.File
stop chan struct{}
logger *logger.SimpleLogger
}

func NewAgent(cfg *admin.Config) *Agent {
Expand All @@ -44,8 +44,7 @@ func (a *Agent) Stop() {
}

func (a *Agent) start() error {
// TODO: log rotation
tl := &logger.TeeLogger{Writer: a.logFile}
tl := &logger.TeeLogger{Writer: a.logger}
if err := tl.Open(); err != nil {
return err
}
Expand Down Expand Up @@ -90,13 +89,16 @@ func (a *Agent) setupLogFile() (err error) {
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
a.logFile, err = utils.OpenOrCreateFile(filename)
a.logger = logger.NewSimpleLogger(
a.LogDir, "scheduler", time.Hour*24,
)
err = a.logger.Open()
return
}

func (a *Agent) closeLogFile() error {
if a.logFile != nil {
return a.logFile.Close()
if a.logger != nil {
return a.logger.Close()
}
return nil
}