1package cmd
2
3import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "path/filepath"
8 "slices"
9 "time"
10
11 "github.com/charmbracelet/crush/internal/config"
12 "github.com/charmbracelet/log/v2"
13 "github.com/nxadm/tail"
14 "github.com/spf13/cobra"
15)
16
17func init() {
18 rootCmd.AddCommand(logsCmd)
19}
20
21var logsCmd = &cobra.Command{
22 Use: "logs",
23 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.`,
25 RunE: func(cmd *cobra.Command, args []string) error {
26 cwd, err := cmd.Flags().GetString("cwd")
27 if err != nil {
28 return fmt.Errorf("failed to get current working directory: %v", err)
29 }
30 log.SetLevel(log.DebugLevel)
31 cfg, err := config.Load(cwd, false)
32 if err != nil {
33 return fmt.Errorf("failed to load configuration: %v", err)
34 }
35 logsFile := filepath.Join(cfg.WorkingDir(), cfg.Options.DataDirectory, "logs", "crush.log")
36 _, err = os.Stat(logsFile)
37 if os.IsNotExist(err) {
38 log.Warn("Looks like you are not in a crush project. No logs found.")
39 return nil
40 }
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
46 // Print the text of each received line
47 for line := range t.Lines {
48 var data map[string]any
49 if err := json.Unmarshal([]byte(line.Text), &data); err != nil {
50 continue
51 }
52 msg := data["msg"]
53 level := data["level"]
54 otherData := []any{}
55 keys := []string{}
56 for k := range data {
57 keys = append(keys, k)
58 }
59 slices.Sort(keys)
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
72 default:
73 otherData = append(otherData, k, data[k])
74 }
75 }
76 log.SetTimeFunction(func(_ time.Time) time.Time {
77 // parse the timestamp from the log line if available
78 t, err := time.Parse(time.RFC3339, data["time"].(string))
79 if err != nil {
80 return time.Now() // fallback to current time if parsing fails
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 }
96 }
97 return nil
98 },
99}