v0.21.0 73bf644

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

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