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

Add logfile_max_num feature #4548

Merged
merged 2 commits into from Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
80 changes: 73 additions & 7 deletions logger/log.go
Expand Up @@ -18,6 +18,9 @@ import (
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
Expand Down Expand Up @@ -128,13 +131,14 @@ type fileLogger struct {
out int64
canRotate int32
sync.Mutex
l *Logger
f writerAndCloser
limit int64
olimit int64
pid string
time bool
closed bool
l *Logger
f writerAndCloser
limit int64
olimit int64
pid string
time bool
closed bool
archiveLimit int
tbeets marked this conversation as resolved.
Show resolved Hide resolved
}

func newFileLogger(filename, pidPrefix string, time bool) (*fileLogger, error) {
Expand Down Expand Up @@ -169,6 +173,12 @@ func (l *fileLogger) setLimit(limit int64) {
}
}

func (l *fileLogger) setArchiveLimit(limit int) {
l.Lock()
l.archiveLimit = limit
l.Unlock()
}

func (l *fileLogger) logDirect(label, format string, v ...interface{}) int {
var entrya = [256]byte{}
var entry = entrya[:0]
Expand All @@ -190,6 +200,46 @@ func (l *fileLogger) logDirect(label, format string, v ...interface{}) int {
return len(entry)
}

func (l *fileLogger) archivePurge(fname string) {
tbeets marked this conversation as resolved.
Show resolved Hide resolved
// Evaluate number of saved backups for purge
// l readlock held
var backups []string
lDir := filepath.Dir(fname)
lBase := filepath.Base(fname)
entries, err := os.ReadDir(lDir)
tbeets marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
l.logDirect(l.l.errorLabel, "Unable to read directory %q for log purge (%v), will attempt next rotation", lDir, err)
return
}
for _, entry := range entries {
tbeets marked this conversation as resolved.
Show resolved Hide resolved
if entry.IsDir() || entry.Name() == lBase || !strings.HasPrefix(entry.Name(), lBase) {
continue
}
stamp, found := strings.CutPrefix(entry.Name(), fmt.Sprintf("%s%s", lBase, "."))
if found {
tbeets marked this conversation as resolved.
Show resolved Hide resolved
_, err := time.Parse("2006:01:02:15:04:05.999999999", strings.Replace(stamp, ".", ":", 5))
if err == nil {
backups = append(backups, entry.Name())
}
}
}
archives := len(backups)
if archives > l.archiveLimit {
// Oldest to latest
sort.Slice(backups, func(i, j int) bool {
tbeets marked this conversation as resolved.
Show resolved Hide resolved
return backups[i] < backups[j]
})
for i := 0; i < archives-l.archiveLimit; i++ {
if err := os.Remove(fmt.Sprintf("%s%s%s", lDir, string(os.PathSeparator), backups[i])); err != nil {
tbeets marked this conversation as resolved.
Show resolved Hide resolved
l.logDirect(l.l.errorLabel, "Unable to remove backup log file %q (%v), will attempt next rotation", backups[i], err)
// Bail fast, we'll try again next rotation
return
}
l.logDirect(l.l.infoLabel, "Removed archived log file %q", backups[i])
}
}
}

func (l *fileLogger) Write(b []byte) (int, error) {
if atomic.LoadInt32(&l.canRotate) == 0 {
n, err := l.f.Write(b)
Expand Down Expand Up @@ -225,6 +275,9 @@ func (l *fileLogger) Write(b []byte) (int, error) {
n := l.logDirect(l.l.infoLabel, "Rotated log, backup saved as %q", bak)
l.out = int64(n)
l.limit = l.olimit
if l.archiveLimit > 0 {
l.archivePurge(fname)
}
}
}
l.Unlock()
Expand Down Expand Up @@ -257,6 +310,19 @@ func (l *Logger) SetSizeLimit(limit int64) error {
return nil
}

// SetArchiveLimit sets the number of archived log files that will be retained
func (l *Logger) SetArchiveLimit(limit int) error {
l.Lock()
if l.fl == nil {
l.Unlock()
return fmt.Errorf("can set log archive limit only for file logger")
}
fl := l.fl
l.Unlock()
fl.setArchiveLimit(limit)
return nil
}

// NewTestLogger creates a logger with output directed to Stderr with a prefix.
// Useful for tracing in tests when multiple servers are in the same pid
func NewTestLogger(prefix string, time bool) *Logger {
Expand Down
10 changes: 10 additions & 0 deletions server/log.go
Expand Up @@ -72,6 +72,16 @@ func (s *Server) ConfigureLogger() {
l.SetSizeLimit(opts.LogSizeLimit)
}
}
if opts.LogMaxArchives > 0 {
if l, ok := log.(*srvlog.Logger); ok {
al := int(opts.LogMaxArchives)
if int64(al) != opts.LogMaxArchives {
// set to default (no max) on overflow
al = 0
}
l.SetArchiveLimit(al)
}
}
} else if opts.RemoteSyslog != "" {
log = srvlog.NewRemoteSysLogger(opts.RemoteSyslog, opts.Debug, opts.Trace)
} else if syslog {
Expand Down
3 changes: 3 additions & 0 deletions server/opts.go
Expand Up @@ -311,6 +311,7 @@ type Options struct {
PortsFileDir string `json:"-"`
LogFile string `json:"-"`
LogSizeLimit int64 `json:"-"`
LogMaxArchives int64 `json:"-"`
Syslog bool `json:"-"`
RemoteSyslog string `json:"-"`
Routes []*url.URL `json:"-"`
Expand Down Expand Up @@ -999,6 +1000,8 @@ func (o *Options) processConfigFileLine(k string, v interface{}, errors *[]error
o.LogFile = v.(string)
case "logfile_size_limit", "log_size_limit":
o.LogSizeLimit = v.(int64)
case "logfile_max_archives", "log_max_archives":
o.LogMaxArchives = v.(int64)
case "syslog":
o.Syslog = v.(bool)
trackExplicitVal(o, &o.inConfig, "Syslog", o.Syslog)
Expand Down
129 changes: 129 additions & 0 deletions test/log_test.go
@@ -0,0 +1,129 @@
package test

import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"
"time"

"github.com/nats-io/nats-server/v2/server"
)

func RunServerWithLogging(opts *server.Options) *server.Server {
if opts == nil {
opts = &DefaultTestOptions
}
opts.NoLog = false
opts.Cluster.PoolSize = -1
opts.Cluster.Compression.Mode = server.CompressionOff
opts.LeafNode.Compression.Mode = server.CompressionOff
s, err := server.NewServer(opts)
if err != nil || s == nil {
panic(fmt.Sprintf("No NATS Server object returned: %v", err))
}
s.ConfigureLogger()
go s.Start()
if !s.ReadyForConnections(10 * time.Second) {
panic("Unable to start NATS Server in Go Routine")
}
return s
}

func TestLogMaxArchives(t *testing.T) {
// With logfile_size_limit set to small 100 characters, plain startup rotates 8 times
for _, test := range []struct {
name string
config string
totEntriesExpected int
}{
{
"Default implicit, no max archives, expect 0 purged archives",
`
port: -1
log_file: %s
logfile_size_limit: 100
`,
9,
},
{
"Default explicit, no max archives, expect 0 purged archives",
`
port: -1
log_file: %s
logfile_size_limit: 100
logfile_max_archives: 0
`,
9,
},
{
"Default explicit - negative val, no max archives, expect 0 purged archives",
`
port: -1
log_file: %s
logfile_size_limit: 100
logfile_max_archives: -42
`,
9,
},
{
"1-archive limit, expect 7 purged archives",
`
port: -1
log_file: %s
logfile_size_limit: 100
logfile_max_archives: 1
`,
2,
},
{
"5-archive limit, expect 4 purged archives",
`
port: -1
log_file: %s
logfile_size_limit: 100
logfile_max_archives: 5
`,
6,
},
{
"100-archive limit, expect 0 purged archives",
`
port: -1
log_file: %s
logfile_size_limit: 100
logfile_max_archives: 100
`,
9,
},
} {
t.Run(test.name, func(t *testing.T) {
d, err := os.MkdirTemp("", "logtest")
if err != nil {
t.Fatalf("Error creating temp dir: %v", err)
}
content := fmt.Sprintf(test.config, filepath.Join(d, "nats-server.log"))
// server config does not like plain windows backslash
if runtime.GOOS == "windows" {
content = filepath.ToSlash(content)
}
opts, err := server.ProcessConfigFile(createConfFile(t, []byte(content)))
if err != nil {
t.Fatalf("Error processing config file: %v", err)
}
s := RunServerWithLogging(opts)
if s == nil {
t.Fatalf("No NATS Server object returned")
}
s.Shutdown()
entries, err := os.ReadDir(d)
if err != nil {
t.Fatalf("Error reading dir: %v", err)
}
if len(entries) != test.totEntriesExpected {
t.Fatalf("Expected %d log files, got %d", test.totEntriesExpected, len(entries))
}
})
}
}