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, 0750)
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 if _, err := output.Write([]byte(formatted)); err != nil {
162 fmt.Fprintf(os.Stderr, "failed to write log output: %v\n", err)
163 }
164 }
165}
166
167func (l *Logger) WriteTagged(source string, message string) {
168 l.WriteFileOnly(INFO, message, source)
169}
170
171// WriteFileOnly writes a log entry only to the file (not stdout) to avoid interfering with CLI operations
172func (l *Logger) WriteFileOnly(level LogLevel, message string, source string) {
173 l.mu.RLock()
174 defer l.mu.RUnlock()
175
176 if level < l.level {
177 return
178 }
179
180 formatted := l.formatter.Format(level, message, source)
181
182 // Write only to file outputs (skip stdout)
183 for _, output := range l.outputs {
184 if output != os.Stdout {
185 if _, err := output.Write([]byte(formatted)); err != nil {
186 fmt.Fprintf(os.Stderr, "failed to write log output: %v\n", err)
187 }
188 }
189 }
190}
191
192func Debug(message string) {
193 if l := GetLogger(); l != nil {
194 l.writeLog(DEBUG, message, "")
195 }
196}
197
198func Info(message string) {
199 if l := GetLogger(); l != nil {
200 l.writeLog(INFO, message, "")
201 }
202}
203
204func Warn(message string) {
205 if l := GetLogger(); l != nil {
206 l.writeLog(WARN, message, "")
207 }
208}
209
210func Error(message string) {
211 if l := GetLogger(); l != nil {
212 l.writeLog(ERROR, message, "")
213 }
214}
215
216func Fatal(message string) {
217 if l := GetLogger(); l != nil {
218 l.writeLog(FATAL, message, "")
219 }
220 os.Exit(1)
221}
222
223func CaptureWebKitLog(message string) {
224 if l := GetLogger(); l != nil {
225 l.WriteTagged("CONSOLE", message)
226 }
227}
228
229// StartConsoleCapture starts capturing stdout for console messages
230// StartConsoleCapture starts capturing stdout for console messages and writing to console.log
231// StartConsoleCapture - placeholder for console message capture
232func StartConsoleCapture() {
233 // Placeholder - console message capture will be implemented differently
234}
235
236// LogLevelInfo returns the INFO log level constant for external packages
237func LogLevelInfo() LogLevel {
238 return INFO
239}