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/crush/pkg/env"
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 cfg, err := config.Load(cwd, env.New())
31 if err != nil {
32 return fmt.Errorf("failed to load configuration: %v", err)
33 }
34 t, err := tail.TailFile(filepath.Join(cfg.Options.DataDirectory, "logs", "crush.log"), tail.Config{Follow: true, ReOpen: true, Logger: tail.DiscardingLogger})
35 if err != nil {
36 return fmt.Errorf("failed to tail log file: %v", err)
37 }
38
39 // Print the text of each received line
40 for line := range t.Lines {
41 var data map[string]any
42 if err := json.Unmarshal([]byte(line.Text), &data); err != nil {
43 continue
44 }
45 msg := data["msg"]
46 level := data["level"]
47 otherData := []any{}
48 keys := []string{}
49 for k := range data {
50 keys = append(keys, k)
51 }
52 slices.Sort(keys)
53 for _, k := range keys {
54 switch k {
55 case "msg", "level", "time":
56 continue
57 case "source":
58 source, ok := data[k].(map[string]any)
59 if !ok {
60 continue
61 }
62 sourceFile := fmt.Sprintf("%s:%d", source["file"], int(source["line"].(float64)))
63 otherData = append(otherData, "source", sourceFile)
64
65 default:
66 otherData = append(otherData, k, data[k])
67 }
68 }
69 log.SetTimeFunction(func(_ time.Time) time.Time {
70 // parse the timestamp from the log line if available
71 t, err := time.Parse(time.RFC3339, data["time"].(string))
72 if err != nil {
73 return time.Now() // fallback to current time if parsing fails
74 }
75 return t
76 })
77 switch level {
78 case "INFO":
79 log.Info(msg, otherData...)
80 case "DEBUG":
81 log.Debug(msg, otherData...)
82 case "ERROR":
83 log.Error(msg, otherData...)
84 case "WARN":
85 log.Warn(msg, otherData...)
86 default:
87 log.Info(msg, otherData...)
88 }
89 }
90 return nil
91 },
92}