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/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}