From 5711e087bd60f2f24689c69f9811d70aa7bb24c3 Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Thu, 10 Jul 2025 17:01:33 -0700 Subject: [PATCH 1/4] update logs command to have oneshot + follow mode, write to stdout --- cmd/logs.go | 142 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 89 insertions(+), 53 deletions(-) diff --git a/cmd/logs.go b/cmd/logs.go index d690625de90d8a0f7ec42d366f5d414c21dbebf0..00ac38cbe2cf3ff0c0635b178102eaa461009d09 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -1,6 +1,7 @@ package cmd import ( + "bufio" "encoding/json" "fmt" "os" @@ -14,10 +15,6 @@ import ( "github.com/spf13/cobra" ) -func init() { - rootCmd.AddCommand(logsCmd) -} - var logsCmd = &cobra.Command{ Use: "logs", Short: "View crush logs", @@ -27,7 +24,16 @@ var logsCmd = &cobra.Command{ if err != nil { return fmt.Errorf("failed to get current working directory: %v", err) } + + follow, err := cmd.Flags().GetBool("follow") + if err != nil { + return fmt.Errorf("failed to get follow flag: %v", err) + } + log.SetLevel(log.DebugLevel) + // Configure log to output to stdout instead of stderr + log.SetOutput(os.Stdout) + cfg, err := config.Load(cwd, false) if err != nil { return fmt.Errorf("failed to load configuration: %v", err) @@ -38,62 +44,92 @@ var logsCmd = &cobra.Command{ log.Warn("Looks like you are not in a crush project. No logs found.") return nil } - t, err := tail.TailFile(logsFile, tail.Config{Follow: true, ReOpen: true, Logger: tail.DiscardingLogger}) - if err != nil { - return fmt.Errorf("failed to tail log file: %v", err) - } - // Print the text of each received line - for line := range t.Lines { - var data map[string]any - if err := json.Unmarshal([]byte(line.Text), &data); err != nil { - continue + if follow { + // Follow mode - tail the file continuously + t, err := tail.TailFile(logsFile, tail.Config{Follow: true, ReOpen: true, Logger: tail.DiscardingLogger}) + if err != nil { + return fmt.Errorf("failed to tail log file: %v", err) + } + + // Print the text of each received line + for line := range t.Lines { + printLogLine(line.Text) } - msg := data["msg"] - level := data["level"] - otherData := []any{} - keys := []string{} - for k := range data { - keys = append(keys, k) + } else { + // Oneshot mode - read the entire file once + file, err := os.Open(logsFile) + if err != nil { + return fmt.Errorf("failed to open log file: %v", err) } - slices.Sort(keys) - for _, k := range keys { - switch k { - case "msg", "level", "time": - continue - case "source": - source, ok := data[k].(map[string]any) - if !ok { - continue - } - sourceFile := fmt.Sprintf("%s:%d", source["file"], int(source["line"].(float64))) - otherData = append(otherData, "source", sourceFile) + defer file.Close() - default: - otherData = append(otherData, k, data[k]) - } + scanner := bufio.NewScanner(file) + for scanner.Scan() { + printLogLine(scanner.Text()) } - log.SetTimeFunction(func(_ time.Time) time.Time { - // parse the timestamp from the log line if available - t, err := time.Parse(time.RFC3339, data["time"].(string)) - if err != nil { - return time.Now() // fallback to current time if parsing fails - } - return t - }) - switch level { - case "INFO": - log.Info(msg, otherData...) - case "DEBUG": - log.Debug(msg, otherData...) - case "ERROR": - log.Error(msg, otherData...) - case "WARN": - log.Warn(msg, otherData...) - default: - log.Info(msg, otherData...) + + if err := scanner.Err(); err != nil { + return fmt.Errorf("failed to read log file: %v", err) } } + return nil }, } + +func init() { + logsCmd.Flags().BoolP("follow", "f", false, "Follow log output") + rootCmd.AddCommand(logsCmd) +} + +func printLogLine(lineText string) { + var data map[string]any + if err := json.Unmarshal([]byte(lineText), &data); err != nil { + return + } + msg := data["msg"] + level := data["level"] + otherData := []any{} + keys := []string{} + for k := range data { + keys = append(keys, k) + } + slices.Sort(keys) + for _, k := range keys { + switch k { + case "msg", "level", "time": + continue + case "source": + source, ok := data[k].(map[string]any) + if !ok { + continue + } + sourceFile := fmt.Sprintf("%s:%d", source["file"], int(source["line"].(float64))) + otherData = append(otherData, "source", sourceFile) + + default: + otherData = append(otherData, k, data[k]) + } + } + log.SetTimeFunction(func(_ time.Time) time.Time { + // parse the timestamp from the log line if available + t, err := time.Parse(time.RFC3339, data["time"].(string)) + if err != nil { + return time.Now() // fallback to current time if parsing fails + } + return t + }) + switch level { + case "INFO": + log.Info(msg, otherData...) + case "DEBUG": + log.Debug(msg, otherData...) + case "ERROR": + log.Error(msg, otherData...) + case "WARN": + log.Warn(msg, otherData...) + default: + log.Info(msg, otherData...) + } +} From b958de1c5051f97cb53978b7dc91e29b7db852fb Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Thu, 10 Jul 2025 21:39:16 -0400 Subject: [PATCH 2/4] chore(logs): copyedit --- cmd/logs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/logs.go b/cmd/logs.go index 00ac38cbe2cf3ff0c0635b178102eaa461009d09..e1cfde432b858afd070d50b2a9de1d4b4c0c1b17 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -18,7 +18,7 @@ import ( var logsCmd = &cobra.Command{ Use: "logs", Short: "View crush logs", - Long: `View the logs generated by Crush. This command allows you to see the log output for debugging and monitoring purposes.`, + Long: `View the logs generated by Crush. This command allows you to see the log output for debugging and monitoring.`, RunE: func(cmd *cobra.Command, args []string) error { cwd, err := cmd.Flags().GetString("cwd") if err != nil { From d031c540ae050b9df81beffa32a319dcad78e5b6 Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Fri, 11 Jul 2025 10:46:20 -0700 Subject: [PATCH 3/4] use readstring instead of bufio to copy behavior of tailinglibrary without token limitation --- cmd/logs.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/cmd/logs.go b/cmd/logs.go index e1cfde432b858afd070d50b2a9de1d4b4c0c1b17..94ba0509d27bb8cefdbf9ffcef65409f2074557a 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -4,9 +4,11 @@ import ( "bufio" "encoding/json" "fmt" + "io" "os" "path/filepath" "slices" + "strings" "time" "github.com/charmbracelet/crush/internal/config" @@ -64,13 +66,19 @@ var logsCmd = &cobra.Command{ } defer file.Close() - scanner := bufio.NewScanner(file) - for scanner.Scan() { - printLogLine(scanner.Text()) - } - - if err := scanner.Err(); err != nil { - return fmt.Errorf("failed to read log file: %v", err) + reader := bufio.NewReader(file) + for { + line, err := reader.ReadString('\n') + if err != nil { + if err == io.EOF && line != "" { + // Handle last line without newline + printLogLine(line) + } + break + } + // Remove trailing newline + line = strings.TrimSuffix(line, "\n") + printLogLine(line) } } From bdeece2c5a3520aab7b511bc4c4fb72dacffb551 Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Fri, 11 Jul 2025 12:12:21 -0700 Subject: [PATCH 4/4] add ringbuffer tailing --- cmd/logs.go | 62 +++++++++++++++++++++++++++++++++++++++++ internal/config/load.go | 2 +- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/cmd/logs.go b/cmd/logs.go index 94ba0509d27bb8cefdbf9ffcef65409f2074557a..bb0aaf9d7b8c2cbcf1da8823c2848002f6d2e252 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -32,6 +32,11 @@ var logsCmd = &cobra.Command{ return fmt.Errorf("failed to get follow flag: %v", err) } + tailLines, err := cmd.Flags().GetInt("tail") + if err != nil { + return fmt.Errorf("failed to get tail flag: %v", err) + } + log.SetLevel(log.DebugLevel) // Configure log to output to stdout instead of stderr log.SetOutput(os.Stdout) @@ -58,6 +63,15 @@ var logsCmd = &cobra.Command{ for line := range t.Lines { printLogLine(line.Text) } + } else if tailLines > 0 { + // Tail mode - show last N lines + lines, err := readLastNLines(logsFile, tailLines) + if err != nil { + return fmt.Errorf("failed to read last %d lines: %v", tailLines, err) + } + for _, line := range lines { + printLogLine(line) + } } else { // Oneshot mode - read the entire file once file, err := os.Open(logsFile) @@ -88,9 +102,57 @@ var logsCmd = &cobra.Command{ func init() { logsCmd.Flags().BoolP("follow", "f", false, "Follow log output") + logsCmd.Flags().IntP("tail", "t", 0, "Show only the last N lines") rootCmd.AddCommand(logsCmd) } +// readLastNLines reads the last N lines from a file using a simple circular buffer approach +func readLastNLines(filename string, n int) ([]string, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + // Use a circular buffer to keep only the last N lines + lines := make([]string, n) + count := 0 + index := 0 + + reader := bufio.NewReader(file) + for { + line, err := reader.ReadString('\n') + if err != nil { + if err == io.EOF && line != "" { + // Handle last line without newline + line = strings.TrimSuffix(line, "\n") + lines[index] = line + count++ + index = (index + 1) % n + } + break + } + // Remove trailing newline + line = strings.TrimSuffix(line, "\n") + lines[index] = line + count++ + index = (index + 1) % n + } + + // Extract the last N lines in correct order + if count <= n { + // We have fewer lines than requested, return them all + return lines[:count], nil + } + + // We have more lines than requested, extract from circular buffer + result := make([]string, n) + for i := range n { + result[i] = lines[(index+i)%n] + } + return result, nil +} + func printLogLine(lineText string) { var data map[string]any if err := json.Unmarshal([]byte(lineText), &data); err != nil { diff --git a/internal/config/load.go b/internal/config/load.go index cc9191fcda5ebfb875fefbac899b21c3597ef0e2..9f2b5e55f1ccc0a687d46083b67e81d6e5fa212a 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -388,7 +388,7 @@ func (cfg *Config) configureSelectedModels(knownProviders []provider.Provider) e } model := cfg.GetModel(large.Provider, large.Model) slog.Info("Configuring selected large model", "provider", large.Provider, "model", large.Model) - slog.Info("MOdel configured", "model", model) + slog.Info("Model configured", "model", model) if model == nil { large = defaultLarge // override the model type to large