v0.13.0 976176a

rotator.go RawHistoryFolder

1package logging
2
3import (
4	"compress/gzip"
5	"fmt"
6	"io"
7	"os"
8	"path/filepath"
9	"sort"
10	"strings"
11	"sync"
12	"time"
13)
14
15type LogRotator struct {
16	mu          sync.Mutex
17	baseDir     string
18	baseName    string
19	maxSize     int64 // bytes
20	maxAge      time.Duration
21	maxBackups  int
22	compress    bool
23	currentFile *os.File
24	currentSize int64
25}
26
27func NewLogRotator(baseDir string, maxSizeMB, maxBackups, maxAgeDays int, compress bool) (*LogRotator, error) {
28	r := &LogRotator{
29		baseDir:    baseDir,
30		baseName:   "dumber.log",
31		maxSize:    int64(maxSizeMB) * 1024 * 1024, // Convert MB to bytes
32		maxAge:     time.Duration(maxAgeDays) * 24 * time.Hour,
33		maxBackups: maxBackups,
34		compress:   compress,
35	}
36
37	if err := r.openCurrentFile(); err != nil {
38		return nil, err
39	}
40
41	return r, nil
42}
43
44func (r *LogRotator) openCurrentFile() error {
45	logPath := filepath.Join(r.baseDir, r.baseName)
46
47	// Get current file size if it exists
48	if info, err := os.Stat(logPath); err == nil {
49		r.currentSize = info.Size()
50	}
51
52	file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
53	if err != nil {
54		return fmt.Errorf("failed to open log file: %w", err)
55	}
56
57	r.currentFile = file
58	return nil
59}
60
61func (r *LogRotator) Write(p []byte) (n int, err error) {
62	r.mu.Lock()
63	defer r.mu.Unlock()
64
65	if r.currentFile == nil {
66		if err := r.openCurrentFile(); err != nil {
67			return 0, err
68		}
69	}
70
71	// Check if rotation is needed
72	if r.currentSize+int64(len(p)) > r.maxSize {
73		if err := r.rotate(); err != nil {
74			return 0, err
75		}
76	}
77
78	n, err = r.currentFile.Write(p)
79	if err != nil {
80		return n, err
81	}
82
83	r.currentSize += int64(n)
84	return n, nil
85}
86
87func (r *LogRotator) rotate() error {
88	if r.currentFile != nil {
89		if err := r.currentFile.Close(); err != nil {
90			fmt.Fprintf(os.Stderr, "Warning: failed to close current log file: %v\n", err)
91		}
92	}
93
94	// Generate backup filename with timestamp
95	timestamp := time.Now().Format("2006-01-02-15-04-05")
96	backupName := fmt.Sprintf("%s.%s", r.baseName, timestamp)
97
98	currentPath := filepath.Join(r.baseDir, r.baseName)
99	backupPath := filepath.Join(r.baseDir, backupName)
100
101	// Move current file to backup
102	if err := os.Rename(currentPath, backupPath); err != nil {
103		return fmt.Errorf("failed to rotate log file: %w", err)
104	}
105
106	// Compress if enabled
107	if r.compress {
108		if err := r.compressFile(backupPath); err != nil {
109			// Log warning but don't fail rotation
110			fmt.Fprintf(os.Stderr, "Warning: failed to compress log file %s: %v\n", backupPath, err)
111		} else {
112			// Remove uncompressed file after successful compression
113			if err := os.Remove(backupPath); err != nil {
114				fmt.Fprintf(os.Stderr, "Warning: failed to remove uncompressed log file %s: %v\n", backupPath, err)
115			}
116		}
117	}
118
119	// Clean up old files
120	r.cleanup()
121
122	// Reset size counter and open new file
123	r.currentSize = 0
124	return r.openCurrentFile()
125}
126
127func (r *LogRotator) compressFile(filePath string) error {
128	inputFile, err := os.Open(filePath)
129	if err != nil {
130		return err
131	}
132	defer func() {
133		if err := inputFile.Close(); err != nil {
134			fmt.Fprintf(os.Stderr, "Warning: failed to close input file during compression: %v\n", err)
135		}
136	}()
137
138	outputPath := filePath + ".gz"
139	outputFile, err := os.Create(outputPath)
140	if err != nil {
141		return err
142	}
143	defer func() {
144		if err := outputFile.Close(); err != nil {
145			fmt.Fprintf(os.Stderr, "Warning: failed to close output file during compression: %v\n", err)
146		}
147	}()
148
149	gzipWriter := gzip.NewWriter(outputFile)
150	defer func() {
151		if err := gzipWriter.Close(); err != nil {
152			fmt.Fprintf(os.Stderr, "Warning: failed to close gzip writer: %v\n", err)
153		}
154	}()
155
156	_, err = io.Copy(gzipWriter, inputFile)
157	return err
158}
159
160func (r *LogRotator) cleanup() {
161	files, err := os.ReadDir(r.baseDir)
162	if err != nil {
163		return
164	}
165
166	var backupFiles []os.FileInfo
167	now := time.Now()
168
169	for _, file := range files {
170		if file.IsDir() {
171			continue
172		}
173
174		name := file.Name()
175		if !strings.HasPrefix(name, r.baseName+".") {
176			continue
177		}
178
179		info, err := file.Info()
180		if err != nil {
181			continue
182		}
183
184		// Remove files older than maxAge
185		if r.maxAge > 0 && now.Sub(info.ModTime()) > r.maxAge {
186			if err := os.Remove(filepath.Join(r.baseDir, name)); err != nil {
187				fmt.Fprintf(os.Stderr, "warning: failed to remove old log file: %v\n", err)
188			}
189			continue
190		}
191
192		backupFiles = append(backupFiles, info)
193	}
194
195	// Remove excess backup files (keep only maxBackups)
196	if r.maxBackups > 0 && len(backupFiles) > r.maxBackups {
197		// Sort by modification time (oldest first)
198		sort.Slice(backupFiles, func(i, j int) bool {
199			return backupFiles[i].ModTime().Before(backupFiles[j].ModTime())
200		})
201
202		// Remove oldest files
203		for i := 0; i < len(backupFiles)-r.maxBackups; i++ {
204			if err := os.Remove(filepath.Join(r.baseDir, backupFiles[i].Name())); err != nil {
205				fmt.Fprintf(os.Stderr, "warning: failed to remove excess backup file: %v\n", err)
206			}
207		}
208	}
209}
210
211func (r *LogRotator) Close() error {
212	r.mu.Lock()
213	defer r.mu.Unlock()
214
215	if r.currentFile != nil {
216		return r.currentFile.Close()
217	}
218	return nil
219}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close