summaryrefslogtreecommitdiff
path: root/internal/loglevel/rotate.go
blob: bfd83511e22daad4200d542ed0ca243144fd78de (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
package loglevel

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 = 10 * 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 (10MB) ---\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 {
	// 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) {
	fr.mu.Lock()
	defer fr.mu.Unlock()
	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()
}