v2.30.0-pre4 15d9fe3

logs.go RawHistoryFolder

1// Package app provides the application initialization and wiring.
2package app
3
4import (
5	"bufio"
6	"bytes"
7	"fmt"
8	"os"
9	"os/exec"
10	"os/signal"
11	"path/filepath"
12	"syscall"
13	"time"
14
15	"github.com/spf13/viper"
16)
17
18// ShowLogs displays Gordon process logs from file or journalctl.
19func ShowLogs(configPath string, follow bool, lines int) error {
20	// Try to find log file from config
21	logPath, err := findLogFile(configPath)
22	if err == nil && logPath != "" {
23		return showFileLog(logPath, follow, lines)
24	}
25
26	// Fallback to journalctl
27	if hasJournalctl() {
28		return showJournalctlLog(follow, lines)
29	}
30
31	return fmt.Errorf("no logs available: enable file logging in config.toml or run gordon as a systemd service")
32}
33
34// findLogFile looks for the configured log file path.
35func findLogFile(configPath string) (string, error) {
36	v := viper.New()
37
38	// Set defaults
39	v.SetDefault("server.data_dir", DefaultDataDir())
40	v.SetDefault("logging.file.enabled", false)
41	v.SetDefault("logging.file.path", "")
42
43	// Try to load config if path provided
44	if configPath != "" {
45		v.SetConfigFile(configPath)
46		if err := v.ReadInConfig(); err != nil {
47			return "", fmt.Errorf("failed to read config: %w", err)
48		}
49	} else {
50		// Try default config locations
51		v.SetConfigName("config")
52		v.SetConfigType("toml")
53		v.AddConfigPath(".")
54		v.AddConfigPath(DefaultDataDir())
55		v.AddConfigPath("/etc/gordon")
56		_ = v.ReadInConfig() // Ignore error, we'll check if logging is enabled
57	}
58
59	if !v.GetBool("logging.file.enabled") {
60		return "", nil
61	}
62
63	logPath := v.GetString("logging.file.path")
64	if logPath == "" {
65		return "", nil
66	}
67
68	// Make path absolute if relative
69	if !filepath.IsAbs(logPath) {
70		dataDir := v.GetString("server.data_dir")
71		logPath = filepath.Join(dataDir, logPath)
72	}
73
74	// Check if file exists
75	if _, err := os.Stat(logPath); err != nil {
76		return "", fmt.Errorf("log file not found: %s", logPath)
77	}
78
79	return logPath, nil
80}
81
82// showFileLog displays logs from a file.
83func showFileLog(logPath string, follow bool, lines int) error {
84	// First show the last N lines
85	if err := tailFile(logPath, lines); err != nil {
86		return err
87	}
88
89	if !follow {
90		return nil
91	}
92
93	// Follow mode: watch for new lines
94	return followFile(logPath)
95}
96
97// tailFile reads the last N lines from a file using a ring buffer.
98// This is memory-efficient as it only keeps the last N lines in memory,
99// regardless of total file size.
100func tailFile(path string, lines int) error {
101	if lines <= 0 {
102		return nil
103	}
104
105	file, err := os.Open(path)
106	if err != nil {
107		return fmt.Errorf("failed to open log file: %w", err)
108	}
109	defer file.Close()
110
111	scanner := bufio.NewScanner(file)
112
113	// Use a ring buffer to store only the last N lines in memory
114	buffer := make([]string, lines)
115	index := 0
116	total := 0
117
118	for scanner.Scan() {
119		buffer[index] = scanner.Text()
120		index = (index + 1) % lines
121		total++
122	}
123	if err := scanner.Err(); err != nil {
124		return fmt.Errorf("failed to read log file: %w", err)
125	}
126
127	if total == 0 {
128		return nil
129	}
130
131	// Print lines in correct order
132	if total <= lines {
133		// Fewer than N lines read; buffer is partially filled, in order from index 0
134		for i := 0; i < total; i++ {
135			fmt.Println(buffer[i])
136		}
137	} else {
138		// More than N lines read; buffer is full ring starting at index
139		for i := 0; i < lines; i++ {
140			pos := (index + i) % lines
141			fmt.Println(buffer[pos])
142		}
143	}
144
145	return nil
146}
147
148// followFile watches a file for new content and prints it.
149// It handles interrupt signals (Ctrl+C) gracefully to ensure proper cleanup.
150func followFile(path string) error {
151	file, err := os.Open(path)
152	if err != nil {
153		return fmt.Errorf("failed to open log file: %w", err)
154	}
155	defer file.Close()
156
157	// Seek to end of file
158	_, err = file.Seek(0, 2)
159	if err != nil {
160		return fmt.Errorf("failed to seek to end of file: %w", err)
161	}
162
163	// Set up signal handling for graceful shutdown
164	sigChan := make(chan os.Signal, 1)
165	signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
166	defer signal.Stop(sigChan)
167
168	reader := bufio.NewReader(file)
169
170	// Poll for new content with signal checking
171	for {
172		select {
173		case <-sigChan:
174			// Graceful exit on interrupt
175			fmt.Println() // Print newline for cleaner terminal output
176			return nil
177		default:
178			line, err := reader.ReadString('\n')
179			if err != nil {
180				// No new content, wait and retry
181				time.Sleep(100 * time.Millisecond)
182				continue
183			}
184			fmt.Print(line)
185		}
186	}
187}
188
189// hasJournalctl checks if journalctl is available.
190func hasJournalctl() bool {
191	_, err := exec.LookPath("journalctl")
192	return err == nil
193}
194
195// showJournalctlLog displays logs from journalctl.
196// It first tries user service, then falls back to system service.
197func showJournalctlLog(follow bool, lines int) error {
198	var userErr, systemErr error
199
200	// Try user service first (works without special permissions)
201	hasLogs, err := hasJournalctlLogs(true)
202	if err != nil {
203		userErr = err
204	} else if hasLogs {
205		return runJournalctl(follow, lines, true)
206	}
207
208	// Fall back to system service
209	hasLogs, err = hasJournalctlLogs(false)
210	if err != nil {
211		systemErr = err
212	} else if hasLogs {
213		return runJournalctl(follow, lines, false)
214	}
215
216	// If either check failed with an error, include that information
217	if userErr != nil || systemErr != nil {
218		return fmt.Errorf("gordon service not found in journalctl (user: %v, system: %v)", userErr, systemErr)
219	}
220
221	return fmt.Errorf("gordon service not found in journalctl: enable file logging in config.toml or run gordon as a systemd service")
222}
223
224// hasJournalctlLogs checks if there are any logs for the gordon service.
225func hasJournalctlLogs(userService bool) (bool, error) {
226	args := []string{"-u", "gordon", "-n", "1", "--no-pager", "-q"}
227	if userService {
228		args = append([]string{"--user"}, args...)
229	}
230
231	// #nosec G204 -- args are constructed internally with controlled values
232	cmd := exec.Command("journalctl", args...)
233	output, err := cmd.Output()
234	if err != nil {
235		return false, err
236	}
237
238	return len(bytes.TrimSpace(output)) > 0, nil
239}
240
241// runJournalctl runs journalctl and displays the output.
242func runJournalctl(follow bool, lines int, userService bool) error {
243	args := []string{"-u", "gordon", "-n", fmt.Sprintf("%d", lines), "--no-pager"}
244	if userService {
245		args = append([]string{"--user"}, args...)
246	}
247	if follow {
248		args = append(args, "-f")
249	}
250
251	// #nosec G204 -- args are constructed internally with controlled values
252	cmd := exec.Command("journalctl", args...)
253	cmd.Stdout = os.Stdout
254	cmd.Stderr = os.Stderr
255
256	return cmd.Run()
257}

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 7

Tags 106

↑↓ moveenter openesc close