logs.go

 1package cmd
 2
 3import (
 4	"encoding/json"
 5	"fmt"
 6	"path/filepath"
 7	"time"
 8
 9	"github.com/charmbracelet/crush/pkg/config"
10	"github.com/charmbracelet/crush/pkg/env"
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, env.New())
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			for k, v := range data {
48				switch k {
49				case "msg", "level", "time":
50					continue
51				case "source":
52					source, ok := v.(map[string]any)
53					if !ok {
54						continue
55					}
56					sourceFile := fmt.Sprintf("%s:%d", source["file"], int(source["line"].(float64)))
57					otherData = append(otherData, "source", sourceFile)
58
59				default:
60					otherData = append(otherData, k, v)
61				}
62			}
63			log.SetTimeFunction(func(_ time.Time) time.Time {
64				// parse the timestamp from the log line if available
65				t, err := time.Parse(time.RFC3339, data["time"].(string))
66				if err != nil {
67					return time.Now() // fallback to current time if parsing fails
68				}
69				return t
70			})
71			switch level {
72			case "INFO":
73				log.Info(msg, otherData...)
74			case "DEBUG":
75				log.Debug(msg, otherData...)
76			case "ERROR":
77				log.Error(msg, otherData...)
78			case "WARN":
79				log.Warn(msg, otherData...)
80			default:
81				log.Info(msg, otherData...)
82			}
83		}
84		return nil
85	},
86}