v2.2.3 133ff88

logs.go RawHistoryFolder

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

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