on main help sign in
chore: implement new logs
commit70666363d1cd6c155a98a0f866852aefcc6333b1 authorKujtim Hoxha <kujtimii.h@gmail.com> dateFri 4 Jul 2025, 21:52 (1 year ago) parentd4927ac
7 files changed+143−2
.gitignore+1−1 cmd/logs.go+86 cmd/root.go+2−1 go.mod+7 go.sum+14 pkg/config/resolve.go+1 pkg/log/log.go+32
.gitignore+1−1
@@ -43,7 +43,7 @@ Thumbs.db
4343
4444**/.crush/**
4545
46−crush
46+/crush
4747
4848manpages/
4949completions/
cmd/logs.gonew file+86
@@ -0,0 +1,86 @@
1+package cmd
2+
3+import (
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+
16+func init() {
17+ rootCmd.AddCommand(logsCmd)
18+}
19+
20+var 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+}
cmd/root.go+2−1
@@ -292,9 +292,10 @@ func Execute() {
292292}
293293
294294func init() {
295+ rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
296+
295297 rootCmd.Flags().BoolP("help", "h", false, "Help")
296298 rootCmd.Flags().BoolP("debug", "d", false, "Debug")
297− rootCmd.Flags().StringP("cwd", "c", "", "Current working directory")
298299 rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode")
299300
300301 // Add format flag with validation logic
go.mod+7
@@ -41,9 +41,16 @@ require (
4141)
4242
4343require (
44+ github.com/charmbracelet/lipgloss v1.1.0 // indirect
45+ github.com/charmbracelet/log v0.4.2 // indirect
46+ github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 // indirect
4447 github.com/joho/godotenv v1.5.1 // indirect
48+ github.com/nxadm/tail v1.4.11 // indirect
4549 github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c // indirect
4650 github.com/spf13/cast v1.7.1 // indirect
51+ golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
52+ gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
53+ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
4754)
4855
4956require (
go.sum+14
@@ -82,10 +82,16 @@ github.com/charmbracelet/fang v0.1.0 h1:SlZS2crf3/zQh7Mr4+W+7QR1k+L08rrPX5rm5z3d
8282github.com/charmbracelet/fang v0.1.0/go.mod h1:Zl/zeUQ8EtQuGyiV0ZKZlZPDowKRTzu8s/367EpN/fc=
8383github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe h1:i6ce4CcAlPpTj2ER69m1DBeLZ3RRcHnKExuwhKa3GfY=
8484github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe/go.mod h1:p3Q+aN4eQKeM5jhrmXPMgPrlKbmc59rWSnMsSA3udhk=
85+github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
86+github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
8587github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c h1:177KMz8zHRlEZJsWzafbKYh6OdjgvTspoH+UjaxgIXY=
8688github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c/go.mod h1:EJWvaCrhOhNGVZMvcjc0yVryl4qqpMs8tz0r9WyEkdQ=
8789github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.2.0.20250703152125-8e1c474f8a71 h1:X0tsNa2UHCKNw+illiavosasVzqioRo32SRV35iwr2I=
8890github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.2.0.20250703152125-8e1c474f8a71/go.mod h1:EJWvaCrhOhNGVZMvcjc0yVryl4qqpMs8tz0r9WyEkdQ=
91+github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
92+github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
93+github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 h1:WkwO6Ks3mSIGnGuSdKl9qDSyfbYK50z2wc2gGMggegE=
94+github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706/go.mod h1:mjJGp00cxcfvD5xdCa+bso251Jt4owrQvuimJtVmEmM=
8995github.com/charmbracelet/x/ansi v0.9.3-0.20250602153603-fb931ed90413 h1:L07QkDqRF274IZ2UJ/mCTL8DR95efU9BNWLYCDXEjvQ=
9096github.com/charmbracelet/x/ansi v0.9.3-0.20250602153603-fb931ed90413/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
9197github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa h1:lphz0Z3rsiOtMYiz8axkT24i9yFiueDhJbzyNUADmME=
@@ -120,6 +126,7 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
120126github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
121127github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
122128github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
129+github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
123130github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
124131github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
125132github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
@@ -201,6 +208,8 @@ github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt
201208github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
202209github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
203210github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
211+github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
212+github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
204213github.com/openai/openai-go v1.8.2 h1:UqSkJ1vCOPUpz9Ka5tS0324EJFEuOvMc+lA/EarJWP8=
205214github.com/openai/openai-go v1.8.2/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
206215github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
@@ -322,6 +331,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
322331golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
323332golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
324333golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
334+golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
325335golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
326336golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
327337golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -368,6 +378,10 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
368378gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
369379gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
370380gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
381+gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
382+gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
383+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
384+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
371385gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
372386gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
373387gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
pkg/config/resolve.go+1
@@ -25,6 +25,7 @@ type shellVariableResolver struct {
2525
2626func NewShellVariableResolver(env env.Env) VariableResolver {
2727 return &shellVariableResolver{
28+ env: env,
2829 shell: shell.NewShell(
2930 &shell.Options{
3031 Env: env.Env(),
pkg/log/log.gonew file+32
@@ -0,0 +1,32 @@
1+package log
2+
3+import (
4+ "log/slog"
5+ "path/filepath"
6+
7+ "github.com/charmbracelet/crush/pkg/config"
8+
9+ "gopkg.in/natefinch/lumberjack.v2"
10+)
11+
12+func Init(cfg *config.Config) {
13+ logRotator := &lumberjack.Logger{
14+ Filename: filepath.Join(cfg.Options.DataDirectory, "logs", "crush.log"),
15+ MaxSize: 10, // Max size in MB
16+ MaxBackups: 0, // Number of backups
17+ MaxAge: 30, // Days
18+ Compress: false, // Enable compression
19+ }
20+
21+ level := slog.LevelInfo
22+ if cfg.Options.Debug {
23+ level = slog.LevelDebug
24+ }
25+
26+ logger := slog.NewJSONHandler(logRotator, &slog.HandlerOptions{
27+ Level: level,
28+ AddSource: true,
29+ })
30+
31+ slog.SetDefault(slog.New(logger))
32+}