logs.go

 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}