v0.28.0 3cb3770

logger.go RawHistoryFolder

1package logging
2
3import (
4	"io"
5	"os"
6	"path/filepath"
7	"time"
8
9	"github.com/rs/zerolog"
10)
11
12const (
13	logFormatConsole  = "console"
14	logFormatJSON     = "json"
15	logFormatText     = "text"
16	ConsoleTimeFormat = "15:04:05"
17)
18
19// Config holds logging configuration
20type Config struct {
21	Level      zerolog.Level
22	Format     string // "json" or "console"
23	TimeFormat string
24}
25
26// FileConfig holds file logging configuration
27type FileConfig struct {
28	Enabled       bool
29	LogDir        string
30	SessionID     string
31	WriteToStderr bool // if false, logs go to file only (not stderr)
32}
33
34// DefaultConfig returns sensible defaults
35func DefaultConfig() Config {
36	return Config{
37		Level:      zerolog.InfoLevel,
38		Format:     logFormatConsole,
39		TimeFormat: time.RFC3339,
40	}
41}
42
43// New creates a new zerolog logger with the given configuration
44func New(cfg Config) zerolog.Logger {
45	var output io.Writer = os.Stderr
46
47	switch cfg.Format {
48	case logFormatConsole:
49		output = zerolog.ConsoleWriter{
50			Out:        os.Stderr,
51			TimeFormat: cfg.TimeFormat,
52		}
53	case logFormatJSON:
54		// JSON is the default zerolog format
55		output = os.Stderr
56	}
57
58	return zerolog.New(output).
59		Level(cfg.Level).
60		With().
61		Timestamp().
62		Logger()
63}
64
65// NewWithFile creates a logger that writes to stderr and/or a session log file.
66// The session file uses JSON format for easy parsing by the CLI logs command.
67// LogDir must exist before calling this function (handled by config.EnsureDirectories).
68// Returns the logger and a cleanup function to close the file.
69func NewWithFile(cfg Config, fileCfg FileConfig) (zerolog.Logger, func(), error) {
70	const (
71		logDirPerm  = 0o755
72		logFilePerm = 0o644
73	)
74	var writers []io.Writer
75	cleanup := func() {}
76
77	// Stderr output (only if enabled)
78	if fileCfg.WriteToStderr {
79		switch cfg.Format {
80		case logFormatConsole, logFormatText, "":
81			writers = append(writers, zerolog.ConsoleWriter{
82				Out:        os.Stderr,
83				TimeFormat: cfg.TimeFormat,
84			})
85		case logFormatJSON:
86			writers = append(writers, os.Stderr)
87		default:
88			writers = append(writers, zerolog.ConsoleWriter{
89				Out:        os.Stderr,
90				TimeFormat: cfg.TimeFormat,
91			})
92		}
93	}
94
95	// Session file output (JSON format for parsing)
96	if fileCfg.Enabled && fileCfg.LogDir != "" && fileCfg.SessionID != "" {
97		// Ensure log directory exists (LogDir is from config, may not have logs subdir)
98		if err := os.MkdirAll(fileCfg.LogDir, logDirPerm); err != nil {
99			return zerolog.Logger{}, nil, err
100		}
101
102		filename := filepath.Join(fileCfg.LogDir, SessionFilename(fileCfg.SessionID))
103		file, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, logFilePerm)
104		if err != nil {
105			return zerolog.Logger{}, nil, err
106		}
107
108		writers = append(writers, file)
109		cleanup = func() { _ = file.Close() }
110	}
111
112	// Fallback to discard if no writers configured
113	if len(writers) == 0 {
114		writers = append(writers, io.Discard)
115	}
116
117	multi := zerolog.MultiLevelWriter(writers...)
118	ctx := zerolog.New(multi).
119		Level(cfg.Level).
120		With().
121		Timestamp()
122
123	return ctx.Logger(), cleanup, nil
124}
125
126// NewFromEnv creates a logger based on environment variables
127// DUMBER_LOG_LEVEL: trace, debug, info, warn, error (default: info)
128// DUMBER_LOG_FORMAT: json, console (default: console)
129func NewFromEnv() zerolog.Logger {
130	cfg := DefaultConfig()
131
132	if level := os.Getenv("DUMBER_LOG_LEVEL"); level != "" {
133		cfg.Level = ParseLevel(level)
134	}
135
136	if format := os.Getenv("DUMBER_LOG_FORMAT"); format != "" {
137		switch format {
138		case logFormatJSON, logFormatConsole, logFormatText:
139			cfg.Format = format
140		}
141	}
142
143	return New(cfg)
144}
145
146// NewFromConfigValues creates a logger from level and format strings.
147// This is used by main.go to create a logger from the config package's LoggingConfig
148// without creating an import cycle.
149func NewFromConfigValues(level, format string) zerolog.Logger {
150	return NewFromConfigValuesWithTimeFormat(level, format, "")
151}
152
153// NewFromConfigValuesWithTimeFormat creates a logger from level/format and overrides time format.
154func NewFromConfigValuesWithTimeFormat(level, format, timeFormat string) zerolog.Logger {
155	cfg := DefaultConfig()
156	cfg.Level = ParseLevel(level)
157	if timeFormat != "" {
158		cfg.TimeFormat = timeFormat
159	}
160
161	switch format {
162	case logFormatJSON:
163		cfg.Format = logFormatJSON
164	case logFormatConsole, logFormatText, "":
165		cfg.Format = logFormatConsole
166	default:
167		cfg.Format = logFormatConsole
168	}
169
170	return New(cfg)
171}
172
173// ParseLevel converts a level string to zerolog.Level
174func ParseLevel(level string) zerolog.Level {
175	switch level {
176	case "trace":
177		return zerolog.TraceLevel
178	case "debug":
179		return zerolog.DebugLevel
180	case "info", "":
181		return zerolog.InfoLevel
182	case "warn":
183		return zerolog.WarnLevel
184	case "error":
185		return zerolog.ErrorLevel
186	case "fatal":
187		return zerolog.FatalLevel
188	default:
189		return zerolog.InfoLevel
190	}
191}
192
193// TruncateURL truncates a URL to maxLen characters for logging.
194func TruncateURL(url string, maxLen int) string {
195	if len(url) <= maxLen {
196		return url
197	}
198	return url[:maxLen] + "..."
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