update logs command to have oneshot + follow mode, write to stdout

Tai Groot created

Change summary

cmd/logs.go | 142 ++++++++++++++++++++++++++++++++++--------------------
1 file changed, 89 insertions(+), 53 deletions(-)

Detailed changes

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...)
+	}
+}