logs.go

 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}