v0.8.0 0fa6785

logger.go RawHistoryFolder

1package logging
2
3import (
4	"fmt"
5	"io"
6	"log"
7	"os"
8	"strings"
9	"sync"
10	"time"
11)
12
13type LogLevel int
14
15const (
16	DEBUG LogLevel = iota
17	INFO
18	WARN
19	ERROR
20	FATAL
21)
22
23var levelNames = map[LogLevel]string{
24	DEBUG: "DEBUG",
25	INFO:  "INFO",
26	WARN:  "WARN",
27	ERROR: "ERROR",
28	FATAL: "FATAL",
29}
30
31type Logger struct {
32	mu        sync.RWMutex
33	level     LogLevel
34	outputs   []io.Writer
35	rotator   *LogRotator
36	formatter LogFormatter
37}
38
39type LogFormatter interface {
40	Format(level LogLevel, message string, source string) string
41}
42
43type TextFormatter struct{}
44
45func (f *TextFormatter) Format(level LogLevel, message string, source string) string {
46	timestamp := time.Now().Format("2006-01-02 15:04:05")
47	levelName := levelNames[level]
48	if source != "" {
49		return fmt.Sprintf("[%s] %s [%s] %s\n", timestamp, levelName, source, message)
50	}
51	return fmt.Sprintf("[%s] %s %s\n", timestamp, levelName, message)
52}
53
54type JSONFormatter struct{}
55
56func (f *JSONFormatter) Format(level LogLevel, message string, source string) string {
57	timestamp := time.Now().Format(time.RFC3339)
58	levelName := levelNames[level]
59	if source != "" {
60		return fmt.Sprintf(`{"timestamp":"%s","level":"%s","source":"%s","message":"%s"}`+"\n",
61			timestamp, levelName, source, strings.ReplaceAll(message, `"`, `\"`))
62	}
63	return fmt.Sprintf(`{"timestamp":"%s","level":"%s","message":"%s"}`+"\n",
64		timestamp, levelName, strings.ReplaceAll(message, `"`, `\"`))
65}
66
67func NewFormatter(format string) LogFormatter {
68	switch strings.ToLower(format) {
69	case "json":
70		return &JSONFormatter{}
71	default:
72		return &TextFormatter{}
73	}
74}
75
76func parseLevel(levelStr string) LogLevel {
77	switch strings.ToLower(levelStr) {
78	case "debug":
79		return DEBUG
80	case "info":
81		return INFO
82	case "warn":
83		return WARN
84	case "error":
85		return ERROR
86	case "fatal":
87		return FATAL
88	default:
89		return INFO
90	}
91}
92
93var globalLogger *Logger
94var globalLoggerMux sync.RWMutex
95
96func Init(logDir string, level string, format string, enableFileLog bool, maxSize, maxBackups, maxAge int, compress bool) error {
97	globalLoggerMux.Lock()
98	defer globalLoggerMux.Unlock()
99
100	err := os.MkdirAll(logDir, 0755)
101	if err != nil {
102		return fmt.Errorf("failed to create log directory: %w", err)
103	}
104
105	outputs := []io.Writer{}
106
107	// Always include stdout
108	outputs = append(outputs, os.Stdout)
109
110	var rotator *LogRotator
111	if enableFileLog {
112		rotator, err = NewLogRotator(logDir, maxSize, maxBackups, maxAge, compress)
113		if err != nil {
114			return fmt.Errorf("failed to create log rotator: %w", err)
115		}
116		outputs = append(outputs, rotator)
117	}
118
119	globalLogger = &Logger{
120		level:     parseLevel(level),
121		outputs:   outputs,
122		rotator:   rotator,
123		formatter: NewFormatter(format),
124	}
125
126	// Redirect standard log package
127	log.SetOutput(globalLogger)
128	log.SetFlags(0) // We'll handle formatting
129
130	return nil
131}
132
133func GetLogger() *Logger {
134	globalLoggerMux.RLock()
135	defer globalLoggerMux.RUnlock()
136	return globalLogger
137}
138
139// Implement io.Writer interface for standard log package redirection
140func (l *Logger) Write(p []byte) (n int, err error) {
141	message := strings.TrimSpace(string(p))
142	if message == "" {
143		return len(p), nil
144	}
145
146	l.writeLog(INFO, message, "")
147	return len(p), nil
148}
149
150func (l *Logger) writeLog(level LogLevel, message string, source string) {
151	l.mu.RLock()
152	defer l.mu.RUnlock()
153
154	if level < l.level {
155		return
156	}
157
158	formatted := l.formatter.Format(level, message, source)
159
160	for _, output := range l.outputs {
161		output.Write([]byte(formatted))
162	}
163}
164
165func (l *Logger) WriteTagged(source string, message string) {
166	l.writeLog(INFO, message, source)
167}
168
169// WriteFileOnly writes a log entry only to the file (not stdout) to avoid interfering with CLI operations
170func (l *Logger) WriteFileOnly(level LogLevel, message string, source string) {
171	l.mu.RLock()
172	defer l.mu.RUnlock()
173
174	if level < l.level {
175		return
176	}
177
178	formatted := l.formatter.Format(level, message, source)
179
180	// Write only to file outputs (skip stdout)
181	for _, output := range l.outputs {
182		if output != os.Stdout {
183			output.Write([]byte(formatted))
184		}
185	}
186}
187
188func Debug(message string) {
189	if l := GetLogger(); l != nil {
190		l.writeLog(DEBUG, message, "")
191	}
192}
193
194func Info(message string) {
195	if l := GetLogger(); l != nil {
196		l.writeLog(INFO, message, "")
197	}
198}
199
200func Warn(message string) {
201	if l := GetLogger(); l != nil {
202		l.writeLog(WARN, message, "")
203	}
204}
205
206func Error(message string) {
207	if l := GetLogger(); l != nil {
208		l.writeLog(ERROR, message, "")
209	}
210}
211
212func Fatal(message string) {
213	if l := GetLogger(); l != nil {
214		l.writeLog(FATAL, message, "")
215	}
216	os.Exit(1)
217}
218
219func CaptureWebKitLog(message string) {
220	if l := GetLogger(); l != nil {
221		l.WriteTagged("WEBKIT", message)
222	}
223}
224
225// LogLevelInfo returns the INFO log level constant for external packages
226func LogLevelInfo() LogLevel {
227	return INFO
228}

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