1package log
2
3import (
4 "fmt"
5 "io"
6 "log/slog"
7 "os"
8 "runtime/debug"
9 "sync"
10 "sync/atomic"
11 "time"
12
13 "github.com/charmbracelet/crush/internal/event"
14 "github.com/charmbracelet/x/term"
15 "gopkg.in/natefinch/lumberjack.v2"
16)
17
18var (
19 initOnce sync.Once
20 initialized atomic.Bool
21)
22
23func Setup(logFile string, debug bool, ws ...io.Writer) {
24 initOnce.Do(func() {
25 logRotator := &lumberjack.Logger{
26 Filename: logFile,
27 MaxSize: 10, // Max size in MB
28 MaxBackups: 0, // Number of backups
29 MaxAge: 30, // Days
30 Compress: false, // Enable compression
31 }
32
33 level := slog.LevelInfo
34 if debug {
35 level = slog.LevelDebug
36 }
37
38 opts := &slog.HandlerOptions{
39 Level: level,
40 AddSource: true,
41 }
42
43 var handlers []slog.Handler
44 handlers = append(handlers, slog.NewJSONHandler(logRotator, opts))
45
46 for _, w := range ws {
47 if w == nil {
48 continue
49 }
50 if f, ok := w.(term.File); ok && term.IsTerminal(f.Fd()) {
51 handlers = append(handlers, slog.NewTextHandler(w, opts))
52 } else {
53 handlers = append(handlers, slog.NewJSONHandler(w, opts))
54 }
55 }
56
57 slog.SetDefault(slog.New(slog.NewMultiHandler(handlers...)))
58 initialized.Store(true)
59 })
60}
61
62func Initialized() bool {
63 return initialized.Load()
64}
65
66func RecoverPanic(name string, cleanup func()) {
67 if r := recover(); r != nil {
68 event.Error(r, "panic", true, "name", name)
69
70 // Create a timestamped panic log file
71 timestamp := time.Now().Format("20060102-150405")
72 filename := fmt.Sprintf("crush-panic-%s-%s.log", name, timestamp)
73
74 file, err := os.Create(filename)
75 if err == nil {
76 defer file.Close()
77
78 // Write panic information and stack trace
79 fmt.Fprintf(file, "Panic in %s: %v\n\n", name, r)
80 fmt.Fprintf(file, "Time: %s\n\n", time.Now().Format(time.RFC3339))
81 fmt.Fprintf(file, "Stack Trace:\n%s\n", debug.Stack())
82
83 // Execute cleanup function if provided
84 if cleanup != nil {
85 cleanup()
86 }
87 }
88 }
89}