diff --git a/api/api_request_logger.go b/api/api_request_logger.go index efc5dfde9..ae7b1b577 100644 --- a/api/api_request_logger.go +++ b/api/api_request_logger.go @@ -13,7 +13,7 @@ import ( "time" "github.com/ethereum/go-ethereum/log" - "github.com/otherview/filerotatewriter" + "github.com/vechain/thor/v2/api/utils/rotatewriter" ) // RequestLogger logs requests to an output @@ -21,10 +21,10 @@ type RequestLogger struct { enabled bool writerChan chan entry stopChan chan bool - outputWriter filerotatewriter.FileRotateWriter + outputWriter rotatewriter.RotateWriter } -func NewRequestLogger(enabled bool, fileRotate filerotatewriter.FileRotateWriter) *RequestLogger { +func NewRequestLogger(enabled bool, fileRotate rotatewriter.RotateWriter) *RequestLogger { return &RequestLogger{ enabled: enabled, outputWriter: fileRotate, diff --git a/api/api_request_logger_test.go b/api/api_request_logger_test.go index f58401cc8..26ab6712d 100644 --- a/api/api_request_logger_test.go +++ b/api/api_request_logger_test.go @@ -17,7 +17,7 @@ import ( "github.com/stretchr/testify/assert" ) -// MockWriter implements filerotatewriter.FileRotateWriter interface +// MockWriter implements rotatewriter.RotateWriter interface type MockWriter struct { Messages []string lock sync.RWMutex diff --git a/api/utils/rotatewriter/rotate_writer.go b/api/utils/rotatewriter/rotate_writer.go new file mode 100644 index 000000000..b9ab12783 --- /dev/null +++ b/api/utils/rotatewriter/rotate_writer.go @@ -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 +} diff --git a/api/utils/rotatewriter/rotate_writer_builder.go b/api/utils/rotatewriter/rotate_writer_builder.go new file mode 100644 index 000000000..c9c22acb6 --- /dev/null +++ b/api/utils/rotatewriter/rotate_writer_builder.go @@ -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 + } +} diff --git a/api/utils/rotatewriter/rotate_writer_test.go b/api/utils/rotatewriter/rotate_writer_test.go new file mode 100644 index 000000000..7ec4bd43b --- /dev/null +++ b/api/utils/rotatewriter/rotate_writer_test.go @@ -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) +} diff --git a/api/utils/rotatewriter/stdout_rotate_writer.go b/api/utils/rotatewriter/stdout_rotate_writer.go new file mode 100644 index 000000000..5c36392f3 --- /dev/null +++ b/api/utils/rotatewriter/stdout_rotate_writer.go @@ -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{} +} diff --git a/cmd/thor/main.go b/cmd/thor/main.go index 8c483feda..21dd2d5a4 100644 --- a/cmd/thor/main.go +++ b/cmd/thor/main.go @@ -17,10 +17,10 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/inconshreveable/log15" "github.com/mattn/go-isatty" - "github.com/otherview/filerotatewriter" "github.com/pborman/uuid" "github.com/pkg/errors" "github.com/vechain/thor/v2/api" + "github.com/vechain/thor/v2/api/utils/rotatewriter" "github.com/vechain/thor/v2/bft" "github.com/vechain/thor/v2/cmd/thor/node" "github.com/vechain/thor/v2/cmd/thor/optimizer" @@ -282,7 +282,7 @@ func soloAction(ctx *cli.Context) error { var mainDB *muxdb.MuxDB var logDB *logdb.LogDB var instanceDir string - var fileRotateWriter filerotatewriter.FileRotateWriter + var fileRotateWriter rotatewriter.RotateWriter var err error if ctx.Bool(persistFlag.Name) { diff --git a/cmd/thor/utils.go b/cmd/thor/utils.go index 901486f2c..641b14265 100644 --- a/cmd/thor/utils.go +++ b/cmd/thor/utils.go @@ -34,9 +34,9 @@ import ( "github.com/ethereum/go-ethereum/rlp" "github.com/inconshreveable/log15" "github.com/mattn/go-tty" - "github.com/otherview/filerotatewriter" "github.com/pkg/errors" "github.com/vechain/thor/v2/api/doc" + "github.com/vechain/thor/v2/api/utils/rotatewriter" "github.com/vechain/thor/v2/chain" "github.com/vechain/thor/v2/cmd/thor/node" "github.com/vechain/thor/v2/co" @@ -348,24 +348,24 @@ func suggestFDCache() int { return n } -func openFileRotate(ctx *cli.Context, instanceDir string) (filerotatewriter.FileRotateWriter, error) { +func openFileRotate(ctx *cli.Context, instanceDir string) (rotatewriter.RotateWriter, error) { // Max of 10 files with 10Mb each // files will be rotated after that maximum if ctx.Bool(apiLogsEnabledFlag.Name) { - return filerotatewriter.New( - filerotatewriter.WithDir(instanceDir), - filerotatewriter.WithFileBaseName("request-logs"), - filerotatewriter.WithFileMaxSize(ctx.Int64(apiLogsMaxFileSizeFlag.Name)), - filerotatewriter.WithMaxNumberFiles(ctx.Int(apiLogsMaxFileCountFlag.Name)), + return rotatewriter.New( + rotatewriter.WithDir(instanceDir), + rotatewriter.WithFileBaseName("request-logs"), + rotatewriter.WithFileMaxSize(ctx.Int64(apiLogsMaxFileSizeFlag.Name)), + rotatewriter.WithMaxNumberFiles(ctx.Int(apiLogsMaxFileCountFlag.Name)), ) } return nil, nil } -func openMemFileRotate(ctx *cli.Context) filerotatewriter.FileRotateWriter { +func openMemFileRotate(ctx *cli.Context) rotatewriter.RotateWriter { if ctx.Bool(apiLogsEnabledFlag.Name) { - return filerotatewriter.StdoutWriter() + return rotatewriter.StdoutWriter() } return nil diff --git a/go.mod b/go.mod index 98b379d53..eccd92249 100644 --- a/go.mod +++ b/go.mod @@ -18,12 +18,11 @@ require ( github.com/mattn/go-isatty v0.0.3 github.com/mattn/go-sqlite3 v1.14.9 github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a - github.com/otherview/filerotatewriter v0.0.0-20240509103702-a305c09fd53a github.com/pborman/uuid v0.0.0-20170612153648-e790cca94e6c github.com/pkg/errors v0.8.0 github.com/pmezard/go-difflib v1.0.0 github.com/qianbin/directcache v0.9.7 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.7.2 github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a github.com/vechain/go-ecvrf v0.0.0-20220525125849-96fa0442e765 golang.org/x/crypto v0.21.0 diff --git a/go.sum b/go.sum index 16e8f0ea9..774cf1f35 100644 --- a/go.sum +++ b/go.sum @@ -117,8 +117,6 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/otherview/filerotatewriter v0.0.0-20240509103702-a305c09fd53a h1:c8RixuoF5WMDhEHqXRT3kxoomiHmhSxxoxJDL0rzxkg= -github.com/otherview/filerotatewriter v0.0.0-20240509103702-a305c09fd53a/go.mod h1:9JtXzZqdljunuU1JIxWUdzVB8w+ZX0Y3ncShN0/xFrE= github.com/pborman/uuid v0.0.0-20170612153648-e790cca94e6c h1:MUyE44mTvnI5A0xrxIxaMqoWFzPfQvtE2IWUollMDMs= github.com/pborman/uuid v0.0.0-20170612153648-e790cca94e6c/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= @@ -134,9 +132,8 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vechain/go-ecvrf v0.0.0-20220525125849-96fa0442e765 h1:jvr+TSivjObZmOKVdqlgeLtRhaDG27gE39PMuE2IJ24= github.com/vechain/go-ecvrf v0.0.0-20220525125849-96fa0442e765/go.mod h1:cwnTMgAVzMb30xMKnGI1LdU1NjMiPllYb7i3ibj/fzE= github.com/vechain/go-ethereum v1.8.15-0.20240308194045-2f457f0512c5 h1:LNy+K6nqSdUBg/Eot8uXLRc0D63zKFhUCAEjvZmLzQA=