-
Notifications
You must be signed in to change notification settings - Fork 250
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
277 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
package rotatewriter | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"os" | ||
"path/filepath" | ||
"sort" | ||
"time" | ||
) | ||
|
||
type RotateWriter interface { | ||
Start() error | ||
Write(p []byte) (int, error) | ||
} | ||
|
||
type Writer struct { | ||
dirPath string | ||
fileBaseName string | ||
maxFileSize int64 | ||
maxNumFiles int | ||
currentFile *os.File | ||
currentSize int64 | ||
numFilesRotated int | ||
} | ||
|
||
func (w *Writer) Start() error { | ||
// Open the current log file | ||
err := w.openNextFile() | ||
if err != nil { | ||
return fmt.Errorf("unable to open next file") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (w *Writer) Write(p []byte) (int, error) { | ||
if w.currentFile == nil { | ||
return 0, io.ErrClosedPipe | ||
} | ||
|
||
// Rotate the log file if it exceeds the maximum file size | ||
if w.currentSize+int64(len(p)) > w.maxFileSize { | ||
err := w.rotateLogFile() | ||
if err != nil { | ||
return 0, err | ||
} | ||
} | ||
|
||
// Write the log message to the current log file | ||
n, err := w.currentFile.Write(p) | ||
w.currentSize += int64(n) | ||
return n, err | ||
} | ||
|
||
func (w *Writer) openNextFile() error { | ||
if w.currentFile != nil { | ||
err := w.currentFile.Close() | ||
if err != nil { | ||
fmt.Println(err) | ||
} | ||
} | ||
|
||
w.currentSize = 0 | ||
w.numFilesRotated = 0 | ||
|
||
// Construct the file name for the next log file | ||
timestamp := time.Now().Format("2006-01-02T15-04-05") | ||
filename := w.fileBaseName + "-" + timestamp + ".log" | ||
filePath := filepath.Join(w.dirPath, filename) | ||
|
||
_, err := os.Stat(filePath) | ||
if err == nil { // File exists, append ms | ||
timestamp = time.Now().Format("2006-01-02T15-04-05.000") | ||
filename = w.fileBaseName + "-" + timestamp + ".log" | ||
filePath = filepath.Join(w.dirPath, filename) | ||
} | ||
|
||
// Open the next log file | ||
file, err := os.Create(filePath) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
w.currentFile = file | ||
return nil | ||
} | ||
|
||
func (w *Writer) rotateLogFile() error { | ||
err := w.openNextFile() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
w.numFilesRotated++ | ||
|
||
// Delete old log files if the maximum number of files has been exceeded | ||
if w.maxNumFiles > 0 && w.numFilesRotated >= w.maxNumFiles { | ||
err = w.deleteOldLogFiles() | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (w *Writer) deleteOldLogFiles() error { | ||
// List all log files in the directory | ||
files, err := filepath.Glob(filepath.Join(w.dirPath, w.fileBaseName+"-*.log")) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// Sort the log files by name (oldest first) | ||
sort.Strings(files) | ||
|
||
// Delete the oldest log files | ||
for i := 0; i < len(files)-w.maxNumFiles+1; i++ { | ||
err := os.Remove(files[i]) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package rotatewriter | ||
|
||
import "fmt" | ||
|
||
type BuilderOptionsFunc func(*Writer) error | ||
|
||
func New(opts ...BuilderOptionsFunc) (RotateWriter, error) { | ||
r := &Writer{ | ||
dirPath: "", | ||
fileBaseName: "", | ||
maxFileSize: 0, | ||
maxNumFiles: 0, | ||
currentFile: nil, | ||
currentSize: 0, | ||
numFilesRotated: 0, | ||
} | ||
for _, opt := range opts { | ||
if err := opt(r); err != nil { | ||
return nil, err | ||
} | ||
} | ||
if r.dirPath == "" || r.fileBaseName == "" || r.maxFileSize == 0 || r.maxNumFiles == 0 { | ||
return nil, fmt.Errorf("missing base setting(s)") | ||
} | ||
|
||
return r, nil | ||
} | ||
|
||
func WithMaxNumberFiles(i int) BuilderOptionsFunc { | ||
return func(w *Writer) error { | ||
w.maxNumFiles = i | ||
return nil | ||
} | ||
} | ||
|
||
func WithFileMaxSize(i int64) BuilderOptionsFunc { | ||
return func(w *Writer) error { | ||
w.maxFileSize = i | ||
return nil | ||
} | ||
} | ||
|
||
func WithFileBaseName(s string) BuilderOptionsFunc { | ||
return func(w *Writer) error { | ||
w.fileBaseName = s | ||
return nil | ||
} | ||
} | ||
|
||
func WithDir(s string) BuilderOptionsFunc { | ||
return func(w *Writer) error { | ||
w.dirPath = s | ||
return nil // TODO check if dir is writable | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package rotatewriter | ||
|
||
import ( | ||
"math/rand" | ||
"os" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestNewRotatingLogWriter(t *testing.T) { | ||
temp, err := os.MkdirTemp("", "*") | ||
require.NoError(t, err) | ||
|
||
// Create a rotating log writer with a maximum size of 1 MB | ||
writer, err := New( | ||
WithDir(temp), | ||
WithFileBaseName("derp"), | ||
WithFileMaxSize(1024*1024), | ||
WithMaxNumberFiles(5), | ||
) | ||
assert.Nil(t, err) | ||
|
||
require.NoError(t, writer.Start()) | ||
|
||
createdFile := writer.(*Writer).currentFile.Name() | ||
|
||
// Use the rotating log writer with the standard log package | ||
data := make([]byte, 1024*1024) | ||
rand.Read(data) //nolint: gosec,staticcheck | ||
_, err = writer.Write(data) | ||
assert.Nil(t, err) | ||
|
||
// Use the rotating log writer with the standard log package | ||
rand.Read(data) //nolint: gosec,staticcheck | ||
_, err = writer.Write(data) | ||
assert.Nil(t, err) | ||
|
||
assert.NotEqual(t, createdFile, writer.(*Writer).currentFile.Name()) | ||
|
||
// make sure the milliseconds dont colide | ||
createdFile = writer.(*Writer).currentFile.Name() | ||
// Use the rotating log writer with the standard log package | ||
rand.Read(data) //nolint: gosec,staticcheck | ||
_, err = writer.Write(data) | ||
assert.Nil(t, err) | ||
|
||
assert.NotEqual(t, createdFile, writer.(*Writer).currentFile.Name()) | ||
} | ||
|
||
func TestStdoutWriter(t *testing.T) { | ||
writer := StdoutWriter() | ||
data := make([]byte, 1024*1024) | ||
rand.Read(data) //nolint: gosec,staticcheck | ||
i, err := writer.Write(data) | ||
assert.Nil(t, err) | ||
assert.Equal(t, len(data), i) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package rotatewriter | ||
|
||
import "fmt" | ||
|
||
type StdoutWriterImpl struct { | ||
} | ||
|
||
func (s StdoutWriterImpl) Start() error { | ||
return nil | ||
} | ||
|
||
func (s StdoutWriterImpl) Write(p []byte) (int, error) { | ||
fmt.Println(string(p)) | ||
return len(p), nil | ||
} | ||
|
||
func StdoutWriter() RotateWriter { // TODO add in io streams | ||
return &StdoutWriterImpl{} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters