@@ -1,14 +1,13 @@
5 "encoding/json"
5 "encoding/json"
9 "path/filepath"
9 "path/filepath"
14 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
@@ -17,6 +16,8 @@ import (
17 "github.com/spf13/cobra"
16 "github.com/spf13/cobra"
19+const defaultTailLines = 1000
20var logsCmd = &cobra.Command{
21var logsCmd = &cobra.Command{
21 Use: "logs",
22 Use: "logs",
22 Short: "View crush logs",
23 Short: "View crush logs",
@@ -38,7 +39,6 @@ var logsCmd = &cobra.Command{
40 log.SetLevel(log.DebugLevel)
41 log.SetLevel(log.DebugLevel)
41− // Configure log to output to stdout instead of stderr
42 log.SetOutput(os.Stdout)
42 log.SetOutput(os.Stdout)
44 cfg, err := config.Load(cwd, false)
44 cfg, err := config.Load(cwd, false)
@@ -53,104 +53,114 @@ var logsCmd = &cobra.Command{
55 if follow {
55 if follow {
56− // Follow mode - tail the file continuously
57− t, err := tail.TailFile(logsFile, tail.Config{Follow: true, ReOpen: true, Logger: tail.DiscardingLogger})
59− return fmt.Errorf("failed to tail log file: %v", err)
62− // Print the text of each received line
63− for line := range t.Lines {
64− printLogLine(line.Text)
66− } else if tailLines > 0 {
67− // Tail mode - show last N lines
68− lines, err := readLastNLines(logsFile, tailLines)
56+ return followLogs(cmd.Context(), logsFile, tailLines)
70− return fmt.Errorf("failed to read last %d lines: %v", tailLines, err)
72− for _, line := range lines {
76− // Oneshot mode - read the entire file once
77− file, err := os.Open(logsFile)
79− return fmt.Errorf("failed to open log file: %v", err)
83− reader := bufio.NewReader(file)
85− line, err := reader.ReadString('\n')
87− if err == io.EOF && line != "" {
88− // Handle last line without newline
93− // Remove trailing newline
94− line = strings.TrimSuffix(line, "\n")
99− return nil
59+ return showLogs(logsFile, tailLines)
103func init() {
63func init() {
104 logsCmd.Flags().BoolP("follow", "f", false, "Follow log output")
64 logsCmd.Flags().BoolP("follow", "f", false, "Follow log output")
105− logsCmd.Flags().IntP("tail", "t", 0, "Show only the last N lines")
65+ logsCmd.Flags().IntP("tail", "t", defaultTailLines, "Show only the last N lines default: 1000 for performance")
106 rootCmd.AddCommand(logsCmd)
66 rootCmd.AddCommand(logsCmd)
109−// readLastNLines reads the last N lines from a file using a simple circular buffer approach
69+func followLogs(ctx context.Context, logsFile string, tailLines int) error {
110−func readLastNLines(filename string, n int) ([]string, error) {
70+ t, err := tail.TailFile(logsFile, tail.Config{
111− file, err := os.Open(filename)
71+ Follow: false,
73+ Logger: tail.DiscardingLogger,
112 if err != nil {
75 if err != nil {
113− return nil, err
76+ return fmt.Errorf("failed to tail log file: %v", err)
117− // Use a circular buffer to keep only the last N lines
118− lines := make([]string, n)
79+ var lines []string
119− count := 0
80+ lineCount := 0
120− index := 0
81+ for line := range t.Lines {
85+ lines = append(lines, line.Text)
87+ if lineCount >= tailLines {
88+ if len(lines) > tailLines {
89+ lines = lines[len(lines)-tailLines:]
95+ for _, line := range lines {
99+ if len(lines) == tailLines {
100+ fmt.Fprintf(os.Stderr, "\nShowing last %d lines. Full logs available at: %s\n", tailLines, logsFile)
101+ fmt.Fprintf(os.Stderr, "Following new log entries...\n\n")
104+ t, err = tail.TailFile(logsFile, tail.Config{
107+ Logger: tail.DiscardingLogger,
108+ Location: &tail.SeekInfo{Offset: 0, Whence: io.SeekEnd},
111+ return fmt.Errorf("failed to tail log file: %v", err)
122− reader := bufio.NewReader(file)
124− line, err := reader.ReadString('\n')
116+ select {
117+ case line := <-t.Lines:
125− if err != nil {
118+ if line.Err != nil {
126− if err == io.EOF && line != "" {
119+ continue
127− // Handle last line without newline
120+ }
128− line = strings.TrimSuffix(line, "\n")
121+ printLogLine(line.Text)
129− lines[index] = line
122+ case <-ctx.Done():
130− count++
123+ return nil
131− index = (index + 1) % n
124+ }
128+func showLogs(logsFile string, tailLines int) error {
129+ t, err := tail.TailFile(logsFile, tail.Config{
132+ Logger: tail.DiscardingLogger,
136+ return fmt.Errorf("failed to tail log file: %v", err)
142+ for line := range t.Lines {
143+ if line.Err != nil {
146+ lines = append(lines, line.Text)
148+ if lineCount >= tailLines {
149+ if len(lines) > tailLines {
150+ lines = lines[len(lines)-tailLines:]
135− // Remove trailing newline
136− line = strings.TrimSuffix(line, "\n")
139− index = (index + 1) % n
142− // Extract the last N lines in correct order
155+ for _, line := range lines {
143− if count <= n {
156+ printLogLine(line)
144− // We have fewer lines than requested, return them all
145− return lines[:count], nil
148− // We have more lines than requested, extract from circular buffer
159+ if len(lines) == tailLines {
149− result := make([]string, n)
160+ fmt.Fprintf(os.Stderr, "\nShowing last %d lines. Full logs available at: %s\n", tailLines, logsFile)
151− result[i] = lines[(index+i)%n]
153− return result, nil
163+ return nil
156func printLogLine(lineText string) {
166func printLogLine(lineText string) {