on main help sign in
update logs command to have oneshot + follow mode, write to stdout
5711e08Tai Groot
cmd/logs.go+89−53
@@ -1,6 +1,7 @@
1package cmd
1package cmd
2
2
3import (
3import (
4+ "bufio"
4 "encoding/json"
5 "encoding/json"
5 "fmt"
6 "fmt"
6 "os"
7 "os"
@@ -14,10 +15,6 @@ import (
14 "github.com/spf13/cobra"
15 "github.com/spf13/cobra"
15)
16)
16
17
17−func init() {
18− rootCmd.AddCommand(logsCmd)
19−}
20−
21var logsCmd = &cobra.Command{
18var logsCmd = &cobra.Command{
22 Use: "logs",
19 Use: "logs",
23 Short: "View crush logs",
20 Short: "View crush logs",
@@ -27,7 +24,16 @@ var logsCmd = &cobra.Command{
27 if err != nil {
24 if err != nil {
28 return fmt.Errorf("failed to get current working directory: %v", err)
25 return fmt.Errorf("failed to get current working directory: %v", err)
29 }
26 }
27+
28+ follow, err := cmd.Flags().GetBool("follow")
29+ if err != nil {
30+ return fmt.Errorf("failed to get follow flag: %v", err)
31+ }
32+
30 log.SetLevel(log.DebugLevel)
33 log.SetLevel(log.DebugLevel)
34+ // Configure log to output to stdout instead of stderr
35+ log.SetOutput(os.Stdout)
36+
31 cfg, err := config.Load(cwd, false)
37 cfg, err := config.Load(cwd, false)
32 if err != nil {
38 if err != nil {
33 return fmt.Errorf("failed to load configuration: %v", err)
39 return fmt.Errorf("failed to load configuration: %v", err)
@@ -38,62 +44,92 @@ var logsCmd = &cobra.Command{
38 log.Warn("Looks like you are not in a crush project. No logs found.")
44 log.Warn("Looks like you are not in a crush project. No logs found.")
39 return nil
45 return nil
40 }
46 }
41− t, err := tail.TailFile(logsFile, tail.Config{Follow: true, ReOpen: true, Logger: tail.DiscardingLogger})
42− if err != nil {
43− return fmt.Errorf("failed to tail log file: %v", err)
44− }
45
47
48+ if follow {
49+ // Follow mode - tail the file continuously
50+ t, err := tail.TailFile(logsFile, tail.Config{Follow: true, ReOpen: true, Logger: tail.DiscardingLogger})
51+ if err != nil {
52+ return fmt.Errorf("failed to tail log file: %v", err)
53+ }
54+
46− // Print the text of each received line
55+ // Print the text of each received line
47− for line := range t.Lines {
56+ for line := range t.Lines {
48− var data map[string]any
57+ printLogLine(line.Text)
49− if err := json.Unmarshal([]byte(line.Text), &data); err != nil {
50− continue
51 }
58 }
52− msg := data["msg"]
59+ } else {
53− level := data["level"]
60+ // Oneshot mode - read the entire file once
54− otherData := []any{}
61+ file, err := os.Open(logsFile)
55− keys := []string{}
62+ if err != nil {
56− for k := range data {
63+ return fmt.Errorf("failed to open log file: %v", err)
57− keys = append(keys, k)
58 }
64 }
59− slices.Sort(keys)
65+ defer file.Close()
60− for _, k := range keys {
61− switch k {
62− case "msg", "level", "time":
63− continue
64− case "source":
65− source, ok := data[k].(map[string]any)
66− if !ok {
67− continue
68− }
69− sourceFile := fmt.Sprintf("%s:%d", source["file"], int(source["line"].(float64)))
70− otherData = append(otherData, "source", sourceFile)
71
66
72− default:
67+ scanner := bufio.NewScanner(file)
73− otherData = append(otherData, k, data[k])
68+ for scanner.Scan() {
74− }
69+ printLogLine(scanner.Text())
75 }
70 }
76− log.SetTimeFunction(func(_ time.Time) time.Time {
71+
77− // parse the timestamp from the log line if available
78− t, err := time.Parse(time.RFC3339, data["time"].(string))
79− if err != nil {
72+ if err := scanner.Err(); err != nil {
80− return time.Now() // fallback to current time if parsing fails
73+ return fmt.Errorf("failed to read log file: %v", err)
81− }
82− return t
83− })
84− switch level {
85− case "INFO":
86− log.Info(msg, otherData...)
87− case "DEBUG":
88− log.Debug(msg, otherData...)
89− case "ERROR":
90− log.Error(msg, otherData...)
91− case "WARN":
92− log.Warn(msg, otherData...)
93− default:
94− log.Info(msg, otherData...)
95 }
74 }
96 }
75 }
76+
97 return nil
77 return nil
98 },
78 },
99}
79}
80+
81+func init() {
82+ logsCmd.Flags().BoolP("follow", "f", false, "Follow log output")
83+ rootCmd.AddCommand(logsCmd)
84+}
85+
86+func printLogLine(lineText string) {
87+ var data map[string]any
88+ if err := json.Unmarshal([]byte(lineText), &data); err != nil {
89+ return
90+ }
91+ msg := data["msg"]
92+ level := data["level"]
93+ otherData := []any{}
94+ keys := []string{}
95+ for k := range data {
96+ keys = append(keys, k)
97+ }
98+ slices.Sort(keys)
99+ for _, k := range keys {
100+ switch k {
101+ case "msg", "level", "time":
102+ continue
103+ case "source":
104+ source, ok := data[k].(map[string]any)
105+ if !ok {
106+ continue
107+ }
108+ sourceFile := fmt.Sprintf("%s:%d", source["file"], int(source["line"].(float64)))
109+ otherData = append(otherData, "source", sourceFile)
110+
111+ default:
112+ otherData = append(otherData, k, data[k])
113+ }
114+ }
115+ log.SetTimeFunction(func(_ time.Time) time.Time {
116+ // parse the timestamp from the log line if available
117+ t, err := time.Parse(time.RFC3339, data["time"].(string))
118+ if err != nil {
119+ return time.Now() // fallback to current time if parsing fails
120+ }
121+ return t
122+ })
123+ switch level {
124+ case "INFO":
125+ log.Info(msg, otherData...)
126+ case "DEBUG":
127+ log.Debug(msg, otherData...)
128+ case "ERROR":
129+ log.Error(msg, otherData...)
130+ case "WARN":
131+ log.Warn(msg, otherData...)
132+ default:
133+ log.Info(msg, otherData...)
134+ }
135+}