summaryrefslogtreecommitdiff
path: root/internal/log/rotate.go
diff options
context:
space:
mode:
authorJeroen Wijenbergh <jeroen.wijenbergh@geant.org>2025-08-01 15:10:41 +0200
committerJeroen Wijenbergh <jeroen.wijenbergh@geant.org>2025-08-25 14:40:18 +0200
commit5461efc826952833fcdecca5d3daa3ee70423a31 (patch)
tree0d9c71cf09f4a496d28f51409515ee6e79a24071 /internal/log/rotate.go
parent08699e08d39675b76a9b813fd9fe41be6ab47a18 (diff)
Client + Log: Implement a log rotater
Diffstat (limited to 'internal/log/rotate.go')
-rw-r--r--internal/log/rotate.go109
1 files changed, 109 insertions, 0 deletions
diff --git a/internal/log/rotate.go b/internal/log/rotate.go
new file mode 100644
index 0000000..fb93c3c
--- /dev/null
+++ b/internal/log/rotate.go
@@ -0,0 +1,109 @@
+package log
+
+import (
+ "io"
+ "os"
+ "sync"
+
+ "codeberg.org/eduVPN/eduvpn-common/internal/atomicfile"
+)
+
+var (
+ // MaxSize is the maximum size in bytes from when to start trimming
+ MaxSize int64 = 1024 * 1024
+
+ // TrimSize denotes how much to trim from the beginning
+ TrimSize = MaxSize / 2
+
+ // TrimMsg is the message to display when it was trimmed
+ TrimMsg = "--- previous part was trimmed by eduvpn-common as the file was too big ---\n"
+)
+
+// FileRotater is a file that is trimmed when a maximum size is reached
+// This is for logging useful
+type FileRotater struct {
+ filename string
+ file *os.File
+ mu sync.Mutex
+}
+
+// NewFileRotater creates a new log file rotater
+func NewFileRotater(filename string) (*FileRotater, error) {
+ fr := &FileRotater{
+ filename: filename,
+ }
+
+ err := fr.open()
+ if err != nil {
+ return nil, err
+ }
+ return fr, nil
+}
+
+func (fr *FileRotater) open() error {
+ f, err := os.OpenFile(fr.filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666)
+ if err != nil {
+ return err
+ }
+ fr.file = f
+ return nil
+}
+
+func (fr *FileRotater) trim() error {
+ fr.mu.Lock()
+ defer fr.mu.Unlock()
+ // We need to seek to the trim size to skip over that part as we discard it
+ _, err := fr.file.Seek(TrimSize, io.SeekStart)
+ if err != nil {
+ return err
+ }
+
+ // get the part of the file that we want to keep
+ keep, err := io.ReadAll(fr.file)
+ if err != nil {
+ return err
+ }
+
+ all := []byte(TrimMsg)
+ all = append(all, keep...)
+ err = atomicfile.WriteFile(fr.file.Name(), all, 0o666)
+ if err != nil {
+ return err
+ }
+
+ // re-open the handle as the file was renamed
+ err = fr.file.Close()
+ if err != nil {
+ return err
+ }
+ err = fr.open()
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// Write implements io.Writer for the log rotater
+func (fr *FileRotater) Write(p []byte) (n int, err error) {
+ fi, err := fr.file.Stat()
+ if err != nil {
+ return 0, err
+ }
+
+ if fi.Size() >= MaxSize {
+ err = fr.trim()
+ if err != nil {
+ return 0, err
+ }
+ }
+ // we don't write atomically here as we want it to be as fast as possible
+ // and if we lose a part of one log statement it's not a big deal
+ return fr.file.Write(p)
+}
+
+// Close closes the file in a safe way by locking and unlocking the mutex
+func (fr *FileRotater) Close() error {
+ fr.mu.Lock()
+ defer fr.mu.Unlock()
+ return fr.file.Close()
+}