@@ -1,11 +1,14 @@
4 "encoding/json"
5 "encoding/json"
7 "path/filepath"
9 "path/filepath"
11 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/config"
@@ -14,20 +17,30 @@ import (
14 "github.com/spf13/cobra"
17 "github.com/spf13/cobra"
18− rootCmd.AddCommand(logsCmd)
21var logsCmd = &cobra.Command{
20var logsCmd = &cobra.Command{
22 Use: "logs",
21 Use: "logs",
23 Short: "View crush logs",
22 Short: "View crush logs",
24− Long: `View the logs generated by Crush. This command allows you to see the log output for debugging and monitoring purposes.`,
23+ Long: `View the logs generated by Crush. This command allows you to see the log output for debugging and monitoring.`,
25 RunE: func(cmd *cobra.Command, args []string) error {
24 RunE: func(cmd *cobra.Command, args []string) error {
26 cwd, err := cmd.Flags().GetString("cwd")
25 cwd, err := cmd.Flags().GetString("cwd")
27 if err != nil {
26 if err != nil {
28 return fmt.Errorf("failed to get current working directory: %v", err)
27 return fmt.Errorf("failed to get current working directory: %v", err)
30+ follow, err := cmd.Flags().GetBool("follow")
32+ return fmt.Errorf("failed to get follow flag: %v", err)
35+ tailLines, err := cmd.Flags().GetInt("tail")
37+ return fmt.Errorf("failed to get tail flag: %v", err)
30 log.SetLevel(log.DebugLevel)
40 log.SetLevel(log.DebugLevel)
41+ // Configure log to output to stdout instead of stderr
42+ log.SetOutput(os.Stdout)
31 cfg, err := config.Load(cwd, false)
44 cfg, err := config.Load(cwd, false)
32 if err != nil {
45 if err != nil {
33 return fmt.Errorf("failed to load configuration: %v", err)
46 return fmt.Errorf("failed to load configuration: %v", err)
@@ -38,62 +51,155 @@ var logsCmd = &cobra.Command{
38 log.Warn("Looks like you are not in a crush project. No logs found.")
51 log.Warn("Looks like you are not in a crush project. No logs found.")
39 return nil
52 return nil
41− t, err := tail.TailFile(logsFile, tail.Config{Follow: true, ReOpen: true, Logger: tail.DiscardingLogger})
43− return fmt.Errorf("failed to tail log file: %v", err)
46− // Print the text of each received line
55+ if follow {
47− for line := range t.Lines {
56+ // Follow mode - tail the file continuously
48− var data map[string]any
57+ t, err := tail.TailFile(logsFile, tail.Config{Follow: true, ReOpen: true, Logger: tail.DiscardingLogger})
49− if err := json.Unmarshal([]byte(line.Text), &data); err != nil {
58+ if err != nil {
50− continue
59+ return fmt.Errorf("failed to tail log file: %v", err)
53− level := data["level"]
56− for k := range data {
57− keys = append(keys, k)
60− for _, k := range keys {
62− case "msg", "level", "time":
65− source, ok := data[k].(map[string]any)
69− sourceFile := fmt.Sprintf("%s:%d", source["file"], int(source["line"].(float64)))
70− otherData = append(otherData, "source", sourceFile)
72− default:
62+ // Print the text of each received line
73− otherData = append(otherData, k, data[k])
63+ for line := range t.Lines {
74− }
64+ printLogLine(line.Text)
76− log.SetTimeFunction(func(_ time.Time) time.Time {
66+ } else if tailLines > 0 {
77− // parse the timestamp from the log line if available
67+ // Tail mode - show last N lines
78− t, err := time.Parse(time.RFC3339, data["time"].(string))
68+ lines, err := readLastNLines(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')
79 if err != nil {
86 if err != nil {
80− return time.Now() // fallback to current time if parsing fails
87+ if err == io.EOF && line != "" {
88+ // Handle last line without newline
82− return t
93+ // Remove trailing newline
83− })
94+ line = strings.TrimSuffix(line, "\n")
84− switch level {
95+ printLogLine(line)
86− log.Info(msg, otherData...)
88− log.Debug(msg, otherData...)
90− log.Error(msg, otherData...)
92− log.Warn(msg, otherData...)
94− log.Info(msg, otherData...)
97 return nil
99 return nil
104+ logsCmd.Flags().BoolP("follow", "f", false, "Follow log output")
105+ logsCmd.Flags().IntP("tail", "t", 0, "Show only the last N lines")
106+ rootCmd.AddCommand(logsCmd)
109+// readLastNLines reads the last N lines from a file using a simple circular buffer approach
110+func readLastNLines(filename string, n int) ([]string, error) {
111+ file, err := os.Open(filename)
117+ // Use a circular buffer to keep only the last N lines
118+ lines := make([]string, n)
122+ reader := bufio.NewReader(file)
124+ line, err := reader.ReadString('\n')
126+ if err == io.EOF && line != "" {
127+ // Handle last line without newline
128+ line = strings.TrimSuffix(line, "\n")
131+ index = (index + 1) % n
135+ // Remove trailing newline
136+ line = strings.TrimSuffix(line, "\n")
139+ index = (index + 1) % n
142+ // Extract the last N lines in correct order
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
149+ result := make([]string, n)
151+ result[i] = lines[(index+i)%n]
156+func printLogLine(lineText string) {
157+ var data map[string]any
158+ if err := json.Unmarshal([]byte(lineText), &data); err != nil {
162+ level := data["level"]
163+ otherData := []any{}
165+ for k := range data {
166+ keys = append(keys, k)
169+ for _, k := range keys {
171+ case "msg", "level", "time":
174+ source, ok := data[k].(map[string]any)
178+ sourceFile := fmt.Sprintf("%s:%d", source["file"], int(source["line"].(float64)))
179+ otherData = append(otherData, "source", sourceFile)
182+ otherData = append(otherData, k, data[k])
185+ log.SetTimeFunction(func(_ time.Time) time.Time {
186+ // parse the timestamp from the log line if available
187+ t, err := time.Parse(time.RFC3339, data["time"].(string))
189+ return time.Now() // fallback to current time if parsing fails
195+ log.Info(msg, otherData...)
197+ log.Debug(msg, otherData...)
199+ log.Error(msg, otherData...)
201+ log.Warn(msg, otherData...)
203+ log.Info(msg, otherData...)