diff --git a/client/driver/logging/rotator.go b/client/driver/logging/rotator.go new file mode 100644 index 000000000..91a04a085 --- /dev/null +++ b/client/driver/logging/rotator.go @@ -0,0 +1,189 @@ +package logging + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "sort" + "strconv" + "strings" +) + +var ( + bufSize = 32 + buf = make([]byte, bufSize) +) + +type FileRotator struct { + MaxFiles int + FileSize int64 + + path string + baseFileName string + + logFileIdx int + oldestLogFileIdx int + + currentFile *os.File + currentWr int64 + + logger *log.Logger + purgeCh chan interface{} +} + +func NewFileRotator(path string, baseFile string, maxFiles int, fileSize int64, logger *log.Logger) (*FileRotator, error) { + rotator := &FileRotator{ + MaxFiles: maxFiles, + FileSize: fileSize, + + path: path, + baseFileName: baseFile, + + logger: logger, + purgeCh: make(chan interface{}), + } + if err := rotator.lastFile(); err != nil { + return nil, err + } + go rotator.purgeOldFiles() + return rotator, nil +} + +func (f *FileRotator) Write(p []byte) (n int, err error) { + n = 0 + var nw int + + for n < len(p) { + if f.currentWr >= f.FileSize { + f.currentFile.Close() + if err := f.nextFile(); err != nil { + return 0, err + } + } + remainingSize := f.FileSize - f.currentWr + if remainingSize < int64(len(p[n:])) { + li := int64(n) + remainingSize + nw, err = f.currentFile.Write(p[n:li]) + } else { + nw, err = f.currentFile.Write(p[n:]) + } + n += nw + f.currentWr += int64(n) + if err != nil { + return + } + } + return +} + +func (f *FileRotator) nextFile() error { + nextFileIdx := f.logFileIdx + for { + nextFileIdx += 1 + logFileName := filepath.Join(f.path, fmt.Sprintf("%s.%d", f.baseFileName, nextFileIdx)) + if fi, err := os.Stat(logFileName); err == nil { + if fi.IsDir() { + nextFileIdx += 1 + continue + } + if fi.Size() >= f.FileSize { + continue + } + } + f.logFileIdx = nextFileIdx + if err := f.createFile(); err != nil { + return err + } + break + } + // Purge old files if we have more files than MaxFiles + if f.logFileIdx-f.oldestLogFileIdx >= f.MaxFiles { + select { + case f.purgeCh <- new(interface{}): + default: + } + } + return nil +} + +func (f *FileRotator) lastFile() error { + finfos, err := ioutil.ReadDir(f.path) + if err != nil { + return err + } + + prefix := fmt.Sprintf("%s.", f.baseFileName) + for _, fi := range finfos { + if fi.IsDir() { + continue + } + if strings.HasPrefix(fi.Name(), prefix) { + fileIdx := strings.TrimPrefix(fi.Name(), prefix) + n, err := strconv.Atoi(fileIdx) + if err != nil { + continue + } + if n > f.logFileIdx { + f.logFileIdx = n + } + } + } + if err := f.createFile(); err != nil { + return err + } + return nil +} + +func (f *FileRotator) createFile() error { + logFileName := filepath.Join(f.path, fmt.Sprintf("%s.%d", f.baseFileName, f.logFileIdx)) + cFile, err := os.OpenFile(logFileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + return err + } + f.currentFile = cFile + fi, err := f.currentFile.Stat() + if err != nil { + return err + } + f.currentWr = fi.Size() + return nil +} + +// PurgeOldFiles removes older files and keeps only the last N files rotated for +// a file +func (f *FileRotator) purgeOldFiles() { + for { + select { + case <-f.purgeCh: + var fIndexes []int + files, err := ioutil.ReadDir(f.path) + if err != nil { + return + } + // Inserting all the rotated files in a slice + for _, fi := range files { + if strings.HasPrefix(fi.Name(), f.baseFileName) { + fileIdx := strings.TrimPrefix(fi.Name(), fmt.Sprintf("%s.", f.baseFileName)) + n, err := strconv.Atoi(fileIdx) + if err != nil { + continue + } + fIndexes = append(fIndexes, n) + } + } + + // Sorting the file indexes so that we can purge the older files and keep + // only the number of files as configured by the user + sort.Sort(sort.IntSlice(fIndexes)) + var toDelete []int + toDelete = fIndexes[0 : len(fIndexes)-f.MaxFiles] + for _, fIndex := range toDelete { + fname := filepath.Join(f.path, fmt.Sprintf("%s.%d", f.baseFileName, fIndex)) + os.RemoveAll(fname) + } + f.oldestLogFileIdx = fIndexes[0] + } + } +} diff --git a/client/driver/logging/rotator_test.go b/client/driver/logging/rotator_test.go new file mode 100644 index 000000000..823b7a3e5 --- /dev/null +++ b/client/driver/logging/rotator_test.go @@ -0,0 +1,236 @@ +package logging + +import ( + "io/ioutil" + "log" + "os" + "path/filepath" + "testing" + "time" +) + +var ( + logger = log.New(os.Stdout, "", log.LstdFlags) + pathPrefix = "logrotator" + baseFileName = "redis.stdout" +) + +func TestFileRotator_IncorrectPath(t *testing.T) { + if _, err := NewFileRotator("/foo", baseFileName, 10, 10, logger); err == nil { + t.Fatalf("expected error") + } +} + +func TestFileRotator_CreateNewFile(t *testing.T) { + var path string + var err error + if path, err = ioutil.TempDir("", pathPrefix); err != nil { + t.Fatalf("test setup err: %v", err) + } + defer os.RemoveAll(path) + + _, err = NewFileRotator(path, baseFileName, 10, 10, logger) + if err != nil { + t.Fatalf("test setup err: %v", err) + } + + if _, err := os.Stat(filepath.Join(path, "redis.stdout.0")); err != nil { + t.Fatalf("expected file") + } +} + +func TestFileRotator_OpenLastFile(t *testing.T) { + var path string + var err error + if path, err = ioutil.TempDir("", pathPrefix); err != nil { + t.Fatalf("test setup err: %v", err) + } + defer os.RemoveAll(path) + + fname1 := filepath.Join(path, "redis.stdout.0") + fname2 := filepath.Join(path, "redis.stdout.2") + if _, err := os.Create(fname1); err != nil { + t.Fatalf("test setup failure: %v", err) + } + if _, err := os.Create(fname2); err != nil { + t.Fatalf("test setup failure: %v", err) + } + + fr, err := NewFileRotator(path, baseFileName, 10, 10, logger) + if err != nil { + t.Fatalf("test setup err: %v", err) + } + + if fr.currentFile.Name() != fname2 { + t.Fatalf("expected current file: %v, got: %v", fname2, fr.currentFile.Name()) + } +} + +func TestFileRotator_WriteToCurrentFile(t *testing.T) { + var path string + var err error + if path, err = ioutil.TempDir("", pathPrefix); err != nil { + t.Fatalf("test setup err: %v", err) + } + defer os.RemoveAll(path) + + fname1 := filepath.Join(path, "redis.stdout.0") + if _, err := os.Create(fname1); err != nil { + t.Fatalf("test setup failure: %v", err) + } + + fr, err := NewFileRotator(path, baseFileName, 10, 5, logger) + if err != nil { + t.Fatalf("test setup err: %v", err) + } + + fr.Write([]byte("abcde")) + fi, err := os.Stat(fname1) + if err != nil { + t.Fatalf("error getting the file info: %v", err) + } + if fi.Size() != 5 { + t.Fatalf("expected size: %v, actual: %v", 5, fi.Size()) + } +} + +func TestFileRotator_RotateFiles(t *testing.T) { + var path string + var err error + if path, err = ioutil.TempDir("", pathPrefix); err != nil { + t.Fatalf("test setup err: %v", err) + } + defer os.RemoveAll(path) + + fr, err := NewFileRotator(path, baseFileName, 10, 5, logger) + if err != nil { + t.Fatalf("test setup err: %v", err) + } + + str := "abcdefgh" + nw, err := fr.Write([]byte(str)) + if err != nil { + t.Fatalf("got error while writing: %v", err) + } + if nw != len(str) { + t.Fatalf("expected %v, got %v", len(str), nw) + } + fname1 := filepath.Join(path, "redis.stdout.0") + fi, err := os.Stat(fname1) + if err != nil { + t.Fatalf("error getting the file info: %v", err) + } + if fi.Size() != 5 { + t.Fatalf("expected size: %v, actual: %v", 5, fi.Size()) + } + + fname2 := filepath.Join(path, "redis.stdout.1") + if _, err := os.Stat(fname2); err != nil { + t.Fatalf("expected file %v to exist", fname2) + } + + if fi2, err := os.Stat(fname2); err == nil { + if fi2.Size() != 3 { + t.Fatalf("expected size: %v, actual: %v", 3, fi2.Size()) + } + } else { + t.Fatalf("error getting the file info: %v", err) + } +} + +func TestFileRotator_WriteRemaining(t *testing.T) { + var path string + var err error + if path, err = ioutil.TempDir("", pathPrefix); err != nil { + t.Fatalf("test setup err: %v", err) + } + defer os.RemoveAll(path) + + fname1 := filepath.Join(path, "redis.stdout.0") + if f, err := os.Create(fname1); err == nil { + f.Write([]byte("abcd")) + } else { + t.Fatalf("test setup failure: %v", err) + } + + fr, err := NewFileRotator(path, baseFileName, 10, 5, logger) + if err != nil { + t.Fatalf("test setup err: %v", err) + } + + str := "efghijkl" + nw, err := fr.Write([]byte(str)) + if err != nil { + t.Fatalf("got error while writing: %v", err) + } + if nw != len(str) { + t.Fatalf("expected %v, got %v", len(str), nw) + } + fi, err := os.Stat(fname1) + if err != nil { + t.Fatalf("error getting the file info: %v", err) + } + if fi.Size() != 5 { + t.Fatalf("expected size: %v, actual: %v", 5, fi.Size()) + } + + fname2 := filepath.Join(path, "redis.stdout.1") + if _, err := os.Stat(fname2); err != nil { + t.Fatalf("expected file %v to exist", fname2) + } + + if fi2, err := os.Stat(fname2); err == nil { + if fi2.Size() != 5 { + t.Fatalf("expected size: %v, actual: %v", 5, fi2.Size()) + } + } else { + t.Fatalf("error getting the file info: %v", err) + } + + fname3 := filepath.Join(path, "redis.stdout.2") + if _, err := os.Stat(fname3); err != nil { + t.Fatalf("expected file %v to exist", fname3) + } + + if fi3, err := os.Stat(fname3); err == nil { + if fi3.Size() != 2 { + t.Fatalf("expected size: %v, actual: %v", 2, fi3.Size()) + } + } else { + t.Fatalf("error getting the file info: %v", err) + } + +} + +func TestFileRotator_PurgeOldFiles(t *testing.T) { + var path string + var err error + if path, err = ioutil.TempDir("", pathPrefix); err != nil { + t.Fatalf("test setup err: %v", err) + } + defer os.RemoveAll(path) + + fr, err := NewFileRotator(path, baseFileName, 2, 2, logger) + if err != nil { + t.Fatalf("test setup err: %v", err) + } + + str := "abcdeghijklmn" + nw, err := fr.Write([]byte(str)) + if err != nil { + t.Fatalf("got error while writing: %v", err) + } + if nw != len(str) { + t.Fatalf("expected %v, got %v", len(str), nw) + } + + time.Sleep(1 * time.Second) + f, err := ioutil.ReadDir(path) + if err != nil { + t.Fatalf("test error: %v", err) + } + + if len(f) != 2 { + t.Fatalf("expected number of files: %v, got: %v", 2, len(f)) + } +}