1package cmd
2
3import (
4 "encoding/json"
5 "fmt"
6 "path/filepath"
7 "slices"
8 "time"
9
10 "github.com/charmbracelet/crush/pkg/config"
11 "github.com/charmbracelet/log/v2"
12 "github.com/nxadm/tail"
13 "github.com/spf13/cobra"
14)
15
16func init() {
17 rootCmd.AddCommand(logsCmd)
18}
19
20var logsCmd = &cobra.Command{
21 Use: "logs",
22 Short: "View crush logs",
23 Long: `View the logs generated by Crush. This command allows you to see the log output for debugging and monitoring purposes.`,
24 RunE: func(cmd *cobra.Command, args []string) error {
25 cwd, err := cmd.Flags().GetString("cwd")
26 if err != nil {
27 return fmt.Errorf("failed to get current working directory: %v", err)
28 }
29 cfg, err := config.Load(cwd, false)
30 if err != nil {
31 return fmt.Errorf("failed to load configuration: %v", err)
32 }
33 t, err := tail.TailFile(filepath.Join(cfg.Options.DataDirectory, "logs", "crush.log"), tail.Config{Follow: true, ReOpen: true, Logger: tail.DiscardingLogger})
34 if err != nil {
35 return fmt.Errorf("failed to tail log file: %v", err)
36 }
37
38 // Print the text of each received line
39 for line := range t.Lines {
40 var data map[string]any
41 if err := json.Unmarshal([]byte(line.Text), &data); err != nil {
42 continue
43 }
44 msg := data["msg"]
45 level := data["level"]
46 otherData := []any{}
47 keys := []string{}
48 for k := range data {
49 keys = append(keys, k)
50 }
51 slices.Sort(keys)
52 for _, k := range keys {
53 switch k {
54 case "msg", "level", "time":
55 continue
56 case "source":
57 source, ok := data[k].(map[string]any)
58 if !ok {
59 continue
60 }
61 sourceFile := fmt.Sprintf("%s:%d", source["file"], int(source["line"].(float64)))
62 otherData = append(otherData, "source", sourceFile)
63
64 default:
65 otherData = append(otherData, k, data[k])
66 }
67 }
68 log.SetTimeFunction(func(_ time.Time) time.Time {
69 // parse the timestamp from the log line if available
70 t, err := time.Parse(time.RFC3339, data["time"].(string))
71 if err != nil {
72 return time.Now() // fallback to current time if parsing fails
73 }
74 return t
75 })
76 switch level {
77 case "INFO":
78 log.Info(msg, otherData...)
79 case "DEBUG":
80 log.Debug(msg, otherData...)
81 case "ERROR":
82 log.Error(msg, otherData...)
83 case "WARN":
84 log.Warn(msg, otherData...)
85 default:
86 log.Info(msg, otherData...)
87 }
88 }
89 return nil
90 },
91}