1package logging
2
3import (
4 "io"
5 "os"
6 "path/filepath"
7 "time"
8
9 "github.com/rs/zerolog"
10)
11
12const (
13 logFormatConsole = "console"
14 logFormatJSON = "json"
15 logFormatText = "text"
16)
17
18// Config holds logging configuration
19type Config struct {
20 Level zerolog.Level
21 Format string // "json" or "console"
22 TimeFormat string
23}
24
25// FileConfig holds file logging configuration
26type FileConfig struct {
27 Enabled bool
28 LogDir string
29 SessionID string
30 WriteToStderr bool // if false, logs go to file only (not stderr)
31}
32
33// DefaultConfig returns sensible defaults
34func DefaultConfig() Config {
35 return Config{
36 Level: zerolog.InfoLevel,
37 Format: logFormatConsole,
38 TimeFormat: time.RFC3339,
39 }
40}
41
42// New creates a new zerolog logger with the given configuration
43func New(cfg Config) zerolog.Logger {
44 var output io.Writer = os.Stderr
45
46 switch cfg.Format {
47 case logFormatConsole:
48 output = zerolog.ConsoleWriter{
49 Out: os.Stderr,
50 TimeFormat: cfg.TimeFormat,
51 }
52 case logFormatJSON:
53 // JSON is the default zerolog format
54 output = os.Stderr
55 }
56
57 return zerolog.New(output).
58 Level(cfg.Level).
59 With().
60 Timestamp().
61 Logger()
62}
63
64// NewWithFile creates a logger that writes to stderr and/or a session log file.
65// The session file uses JSON format for easy parsing by the CLI logs command.
66// LogDir must exist before calling this function (handled by config.EnsureDirectories).
67// Returns the logger and a cleanup function to close the file.
68func NewWithFile(cfg Config, fileCfg FileConfig) (zerolog.Logger, func(), error) {
69 const (
70 logDirPerm = 0o755
71 logFilePerm = 0o644
72 )
73 var writers []io.Writer
74 cleanup := func() {}
75
76 // Stderr output (only if enabled)
77 if fileCfg.WriteToStderr {
78 switch cfg.Format {
79 case logFormatConsole, logFormatText, "":
80 writers = append(writers, zerolog.ConsoleWriter{
81 Out: os.Stderr,
82 TimeFormat: cfg.TimeFormat,
83 })
84 case logFormatJSON:
85 writers = append(writers, os.Stderr)
86 default:
87 writers = append(writers, zerolog.ConsoleWriter{
88 Out: os.Stderr,
89 TimeFormat: cfg.TimeFormat,
90 })
91 }
92 }
93
94 // Session file output (JSON format for parsing)
95 if fileCfg.Enabled && fileCfg.LogDir != "" && fileCfg.SessionID != "" {
96 // Ensure log directory exists (LogDir is from config, may not have logs subdir)
97 if err := os.MkdirAll(fileCfg.LogDir, logDirPerm); err != nil {
98 return zerolog.Logger{}, nil, err
99 }
100
101 filename := filepath.Join(fileCfg.LogDir, SessionFilename(fileCfg.SessionID))
102 file, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, logFilePerm)
103 if err != nil {
104 return zerolog.Logger{}, nil, err
105 }
106
107 writers = append(writers, file)
108 cleanup = func() { _ = file.Close() }
109 }
110
111 // Fallback to discard if no writers configured
112 if len(writers) == 0 {
113 writers = append(writers, io.Discard)
114 }
115
116 multi := zerolog.MultiLevelWriter(writers...)
117 ctx := zerolog.New(multi).
118 Level(cfg.Level).
119 With().
120 Timestamp()
121
122 return ctx.Logger(), cleanup, nil
123}
124
125// NewFromEnv creates a logger based on environment variables
126// DUMBER_LOG_LEVEL: trace, debug, info, warn, error (default: info)
127// DUMBER_LOG_FORMAT: json, console (default: console)
128func NewFromEnv() zerolog.Logger {
129 cfg := DefaultConfig()
130
131 if level := os.Getenv("DUMBER_LOG_LEVEL"); level != "" {
132 cfg.Level = ParseLevel(level)
133 }
134
135 if format := os.Getenv("DUMBER_LOG_FORMAT"); format != "" {
136 switch format {
137 case logFormatJSON, logFormatConsole, logFormatText:
138 cfg.Format = format
139 }
140 }
141
142 return New(cfg)
143}
144
145// NewFromConfigValues creates a logger from level and format strings.
146// This is used by main.go to create a logger from the config package's LoggingConfig
147// without creating an import cycle.
148func NewFromConfigValues(level, format string) zerolog.Logger {
149 cfg := DefaultConfig()
150 cfg.Level = ParseLevel(level)
151
152 switch format {
153 case logFormatJSON:
154 cfg.Format = logFormatJSON
155 case logFormatConsole, logFormatText, "":
156 cfg.Format = logFormatConsole
157 default:
158 cfg.Format = logFormatConsole
159 }
160
161 return New(cfg)
162}
163
164// ParseLevel converts a level string to zerolog.Level
165func ParseLevel(level string) zerolog.Level {
166 switch level {
167 case "trace":
168 return zerolog.TraceLevel
169 case "debug":
170 return zerolog.DebugLevel
171 case "info", "":
172 return zerolog.InfoLevel
173 case "warn":
174 return zerolog.WarnLevel
175 case "error":
176 return zerolog.ErrorLevel
177 case "fatal":
178 return zerolog.FatalLevel
179 default:
180 return zerolog.InfoLevel
181 }
182}