v0.5.0 f906adb

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, 0644)
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		r.currentFile.Close()
90	}
91
92	// Generate backup filename with timestamp
93	timestamp := time.Now().Format("2006-01-02-15-04-05")
94	backupName := fmt.Sprintf("%s.%s", r.baseName, timestamp)
95	
96	currentPath := filepath.Join(r.baseDir, r.baseName)
97	backupPath := filepath.Join(r.baseDir, backupName)
98
99	// Move current file to backup
100	if err := os.Rename(currentPath, backupPath); err != nil {
101		return fmt.Errorf("failed to rotate log file: %w", err)
102	}
103
104	// Compress if enabled
105	if r.compress {
106		if err := r.compressFile(backupPath); err != nil {
107			// Log warning but don't fail rotation
108			fmt.Fprintf(os.Stderr, "Warning: failed to compress log file %s: %v\n", backupPath, err)
109		} else {
110			// Remove uncompressed file after successful compression
111			os.Remove(backupPath)
112		}
113	}
114
115	// Clean up old files
116	r.cleanup()
117
118	// Reset size counter and open new file
119	r.currentSize = 0
120	return r.openCurrentFile()
121}
122
123func (r *LogRotator) compressFile(filePath string) error {
124	inputFile, err := os.Open(filePath)
125	if err != nil {
126		return err
127	}
128	defer inputFile.Close()
129
130	outputPath := filePath + ".gz"
131	outputFile, err := os.Create(outputPath)
132	if err != nil {
133		return err
134	}
135	defer outputFile.Close()
136
137	gzipWriter := gzip.NewWriter(outputFile)
138	defer gzipWriter.Close()
139
140	_, err = io.Copy(gzipWriter, inputFile)
141	return err
142}
143
144func (r *LogRotator) cleanup() {
145	files, err := os.ReadDir(r.baseDir)
146	if err != nil {
147		return
148	}
149
150	var backupFiles []os.FileInfo
151	now := time.Now()
152
153	for _, file := range files {
154		if file.IsDir() {
155			continue
156		}
157
158		name := file.Name()
159		if !strings.HasPrefix(name, r.baseName+".") {
160			continue
161		}
162
163		info, err := file.Info()
164		if err != nil {
165			continue
166		}
167
168		// Remove files older than maxAge
169		if r.maxAge > 0 && now.Sub(info.ModTime()) > r.maxAge {
170			os.Remove(filepath.Join(r.baseDir, name))
171			continue
172		}
173
174		backupFiles = append(backupFiles, info)
175	}
176
177	// Remove excess backup files (keep only maxBackups)
178	if r.maxBackups > 0 && len(backupFiles) > r.maxBackups {
179		// Sort by modification time (oldest first)
180		sort.Slice(backupFiles, func(i, j int) bool {
181			return backupFiles[i].ModTime().Before(backupFiles[j].ModTime())
182		})
183
184		// Remove oldest files
185		for i := 0; i < len(backupFiles)-r.maxBackups; i++ {
186			os.Remove(filepath.Join(r.baseDir, backupFiles[i].Name()))
187		}
188	}
189}
190
191func (r *LogRotator) Close() error {
192	r.mu.Lock()
193	defer r.mu.Unlock()
194	
195	if r.currentFile != nil {
196		return r.currentFile.Close()
197	}
198	return nil
199}

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