1package logging
2
3import (
4 "fmt"
5 "io"
6 "log"
7 "os"
8 "strings"
9 "sync"
10 "time"
11)
12
13type LogLevel int
14
15const (
16 DEBUG LogLevel = iota
17 INFO
18 WARN
19 ERROR
20 FATAL
21)
22
23var levelNames = map[LogLevel]string{
24 DEBUG: "DEBUG",
25 INFO: "INFO",
26 WARN: "WARN",
27 ERROR: "ERROR",
28 FATAL: "FATAL",
29}
30
31type Logger struct {
32 mu sync.RWMutex
33 level LogLevel
34 outputs []io.Writer
35 rotator *LogRotator
36 formatter LogFormatter
37}
38
39type LogFormatter interface {
40 Format(level LogLevel, message string, source string) string
41}
42
43type TextFormatter struct{}
44
45func (f *TextFormatter) Format(level LogLevel, message string, source string) string {
46 timestamp := time.Now().Format("2006-01-02 15:04:05")
47 levelName := levelNames[level]
48 if source != "" {
49 return fmt.Sprintf("[%s] %s [%s] %s\n", timestamp, levelName, source, message)
50 }
51 return fmt.Sprintf("[%s] %s %s\n", timestamp, levelName, message)
52}
53
54type JSONFormatter struct{}
55
56func (f *JSONFormatter) Format(level LogLevel, message string, source string) string {
57 timestamp := time.Now().Format(time.RFC3339)
58 levelName := levelNames[level]
59 if source != "" {
60 return fmt.Sprintf(`{"timestamp":"%s","level":"%s","source":"%s","message":"%s"}`+"\n",
61 timestamp, levelName, source, strings.ReplaceAll(message, `"`, `\"`))
62 }
63 return fmt.Sprintf(`{"timestamp":"%s","level":"%s","message":"%s"}`+"\n",
64 timestamp, levelName, strings.ReplaceAll(message, `"`, `\"`))
65}
66
67func NewFormatter(format string) LogFormatter {
68 switch strings.ToLower(format) {
69 case "json":
70 return &JSONFormatter{}
71 default:
72 return &TextFormatter{}
73 }
74}
75
76func parseLevel(levelStr string) LogLevel {
77 switch strings.ToLower(levelStr) {
78 case "debug":
79 return DEBUG
80 case "info":
81 return INFO
82 case "warn":
83 return WARN
84 case "error":
85 return ERROR
86 case "fatal":
87 return FATAL
88 default:
89 return INFO
90 }
91}
92
93var globalLogger *Logger
94var globalLoggerMux sync.RWMutex
95
96func Init(logDir string, level string, format string, enableFileLog bool, maxSize, maxBackups, maxAge int, compress bool) error {
97 globalLoggerMux.Lock()
98 defer globalLoggerMux.Unlock()
99
100 err := os.MkdirAll(logDir, 0755)
101 if err != nil {
102 return fmt.Errorf("failed to create log directory: %w", err)
103 }
104
105 outputs := []io.Writer{}
106
107 // Always include stdout
108 outputs = append(outputs, os.Stdout)
109
110 var rotator *LogRotator
111 if enableFileLog {
112 rotator, err = NewLogRotator(logDir, maxSize, maxBackups, maxAge, compress)
113 if err != nil {
114 return fmt.Errorf("failed to create log rotator: %w", err)
115 }
116 outputs = append(outputs, rotator)
117 }
118
119 globalLogger = &Logger{
120 level: parseLevel(level),
121 outputs: outputs,
122 rotator: rotator,
123 formatter: NewFormatter(format),
124 }
125
126 // Redirect standard log package
127 log.SetOutput(globalLogger)
128 log.SetFlags(0) // We'll handle formatting
129
130 return nil
131}
132
133func GetLogger() *Logger {
134 globalLoggerMux.RLock()
135 defer globalLoggerMux.RUnlock()
136 return globalLogger
137}
138
139// Implement io.Writer interface for standard log package redirection
140func (l *Logger) Write(p []byte) (n int, err error) {
141 message := strings.TrimSpace(string(p))
142 if message == "" {
143 return len(p), nil
144 }
145
146 l.writeLog(INFO, message, "")
147 return len(p), nil
148}
149
150func (l *Logger) writeLog(level LogLevel, message string, source string) {
151 l.mu.RLock()
152 defer l.mu.RUnlock()
153
154 if level < l.level {
155 return
156 }
157
158 formatted := l.formatter.Format(level, message, source)
159
160 for _, output := range l.outputs {
161 output.Write([]byte(formatted))
162 }
163}
164
165func (l *Logger) WriteTagged(source string, message string) {
166 l.WriteFileOnly(INFO, message, source)
167}
168
169// WriteFileOnly writes a log entry only to the file (not stdout) to avoid interfering with CLI operations
170func (l *Logger) WriteFileOnly(level LogLevel, message string, source string) {
171 l.mu.RLock()
172 defer l.mu.RUnlock()
173
174 if level < l.level {
175 return
176 }
177
178 formatted := l.formatter.Format(level, message, source)
179
180 // Write only to file outputs (skip stdout)
181 for _, output := range l.outputs {
182 if output != os.Stdout {
183 output.Write([]byte(formatted))
184 }
185 }
186}
187
188func Debug(message string) {
189 if l := GetLogger(); l != nil {
190 l.writeLog(DEBUG, message, "")
191 }
192}
193
194func Info(message string) {
195 if l := GetLogger(); l != nil {
196 l.writeLog(INFO, message, "")
197 }
198}
199
200func Warn(message string) {
201 if l := GetLogger(); l != nil {
202 l.writeLog(WARN, message, "")
203 }
204}
205
206func Error(message string) {
207 if l := GetLogger(); l != nil {
208 l.writeLog(ERROR, message, "")
209 }
210}
211
212func Fatal(message string) {
213 if l := GetLogger(); l != nil {
214 l.writeLog(FATAL, message, "")
215 }
216 os.Exit(1)
217}
218
219func CaptureWebKitLog(message string) {
220 if l := GetLogger(); l != nil {
221 l.WriteTagged("CONSOLE", message)
222 }
223}
224
225// StartConsoleCapture starts capturing stdout for console messages
226// StartConsoleCapture starts capturing stdout for console messages and writing to console.log
227// StartConsoleCapture - placeholder for console message capture
228func StartConsoleCapture() {
229 // Placeholder - console message capture will be implemented differently
230}
231
232// LogLevelInfo returns the INFO log level constant for external packages
233func LogLevelInfo() LogLevel {
234 return INFO
235}