11package logger
22
33import (
4- "encoding/json"
54 "fmt"
6- "log"
75 "os"
6+ "path/filepath"
87 "runtime"
98 "strings"
109 "sync"
11- "time"
10+
11+ "github.com/rs/zerolog"
1212)
1313
14- type LogLevel int
14+ type LogLevel = zerolog. Level
1515
1616const (
17- DEBUG LogLevel = iota
18- INFO
19- WARN
20- ERROR
21- FATAL
17+ DEBUG = zerolog . DebugLevel
18+ INFO = zerolog . InfoLevel
19+ WARN = zerolog . WarnLevel
20+ ERROR = zerolog . ErrorLevel
21+ FATAL = zerolog . FatalLevel
2222)
2323
2424var (
@@ -31,34 +31,32 @@ var (
3131 }
3232
3333 currentLevel = INFO
34- logger * Logger
34+ logger zerolog.Logger
35+ fileLogger zerolog.Logger
36+ logFile * os.File
3537 once sync.Once
3638 mu sync.RWMutex
3739)
3840
39- type Logger struct {
40- file * os.File
41- }
42-
43- type LogEntry struct {
44- Level string `json:"level"`
45- Timestamp string `json:"timestamp"`
46- Component string `json:"component,omitempty"`
47- Message string `json:"message"`
48- Fields map [string ]any `json:"fields,omitempty"`
49- Caller string `json:"caller,omitempty"`
50- }
51-
5241func init () {
5342 once .Do (func () {
54- logger = & Logger {}
43+ zerolog .SetGlobalLevel (zerolog .InfoLevel )
44+
45+ consoleWriter := zerolog.ConsoleWriter {
46+ Out : os .Stdout ,
47+ TimeFormat : "15:04:05" , // TODO: make it configurable???
48+ }
49+
50+ logger = zerolog .New (consoleWriter ).With ().Timestamp ().Logger ()
51+ fileLogger = zerolog.Logger {}
5552 })
5653}
5754
5855func SetLevel (level LogLevel ) {
5956 mu .Lock ()
6057 defer mu .Unlock ()
6158 currentLevel = level
59+ zerolog .SetGlobalLevel (level )
6260}
6361
6462func GetLevel () LogLevel {
@@ -71,93 +69,121 @@ func EnableFileLogging(filePath string) error {
7169 mu .Lock ()
7270 defer mu .Unlock ()
7371
74- file , err := os .OpenFile (filePath , os .O_CREATE | os .O_WRONLY | os .O_APPEND , 0o644 )
72+ if err := os .MkdirAll (filepath .Dir (filePath ), 0o755 ); err != nil {
73+ return fmt .Errorf ("failed to create log directory: %w" , err )
74+ }
75+
76+ newFile , err := os .OpenFile (filePath , os .O_CREATE | os .O_WRONLY | os .O_APPEND , 0o644 )
7577 if err != nil {
7678 return fmt .Errorf ("failed to open log file: %w" , err )
7779 }
7880
79- if logger .file != nil {
80- logger .file .Close ()
81+ // Close old file if exists
82+ if logFile != nil {
83+ logFile .Close ()
8184 }
8285
83- logger . file = file
84- log . Println ( "File logging enabled:" , filePath )
86+ logFile = newFile
87+ fileLogger = zerolog . New ( logFile ). With (). Timestamp (). Caller (). Logger ( )
8588 return nil
8689}
8790
8891func DisableFileLogging () {
8992 mu .Lock ()
9093 defer mu .Unlock ()
9194
92- if logger .file != nil {
93- logger .file .Close ()
94- logger .file = nil
95- log .Println ("File logging disabled" )
95+ if logFile != nil {
96+ logFile .Close ()
97+ logFile = nil
9698 }
99+ fileLogger = zerolog.Logger {}
97100}
98101
99- func logMessage (level LogLevel , component string , message string , fields map [string ]any ) {
100- if level < currentLevel {
101- return
102- }
103-
104- entry := LogEntry {
105- Level : logLevelNames [level ],
106- Timestamp : time .Now ().UTC ().Format (time .RFC3339 ),
107- Component : component ,
108- Message : message ,
109- Fields : fields ,
110- }
102+ func getCallerInfo () (string , int , string ) {
103+ for i := 2 ; i < 15 ; i ++ {
104+ pc , file , line , ok := runtime .Caller (i )
105+ if ! ok {
106+ continue
107+ }
111108
112- if pc , file , line , ok := runtime .Caller (2 ); ok {
113109 fn := runtime .FuncForPC (pc )
114- if fn ! = nil {
115- entry . Caller = fmt . Sprintf ( "%s:%d (%s)" , file , line , fn . Name ())
110+ if fn = = nil {
111+ continue
116112 }
117- }
118113
119- if logger . file != nil {
120- jsonData , err := json . Marshal ( entry )
121- if err == nil {
122- logger . file . Write ( append ( jsonData , '\n' ))
114+ // bypass common loggers
115+ if strings . HasSuffix ( file , "/logger.go" ) ||
116+ strings . HasSuffix ( file , "/log.go" ) {
117+ continue
123118 }
124- }
125119
126- var fieldStr string
127- if len (fields ) > 0 {
128- fieldStr = " " + formatFields (fields )
129- } else {
130- fieldStr = ""
131- }
120+ funcName := fn .Name ()
121+ if strings .HasPrefix (funcName , "runtime." ) {
122+ continue
123+ }
132124
133- logLine := fmt .Sprintf ("[%s] [%s]%s %s%s" ,
134- entry .Timestamp ,
135- logLevelNames [level ],
136- formatComponent (component ),
137- message ,
138- fieldStr ,
139- )
125+ return filepath .Base (file ), line , filepath .Base (funcName )
126+ }
140127
141- log .Println (logLine )
128+ return "???" , 0 , "???"
129+ }
142130
143- if level == FATAL {
144- os .Exit (1 )
131+ //nolint:zerologlint
132+ func getEvent (logger zerolog.Logger , level LogLevel ) * zerolog.Event {
133+ switch level {
134+ case zerolog .DebugLevel :
135+ return logger .Debug ()
136+ case zerolog .InfoLevel :
137+ return logger .Info ()
138+ case zerolog .WarnLevel :
139+ return logger .Warn ()
140+ case zerolog .ErrorLevel :
141+ return logger .Error ()
142+ case zerolog .FatalLevel :
143+ return logger .Fatal ()
144+ default :
145+ return logger .Info ()
145146 }
146147}
147148
148- func formatComponent (component string ) string {
149- if component == "" {
150- return ""
149+ func logMessage (level LogLevel , component string , message string , fields map [string ]any ) {
150+ if level < currentLevel {
151+ return
152+ }
153+
154+ callerFile , callerLine , callerFunc := getCallerInfo ()
155+
156+ event := getEvent (logger , level )
157+
158+ // Build combined field with component and caller
159+ if component != "" {
160+ event .Str ("caller" , fmt .Sprintf ("%-6s %s:%d (%s)" , component , callerFile , callerLine , callerFunc ))
161+ } else {
162+ event .Str ("caller" , fmt .Sprintf ("<none> %s:%d (%s)" , callerFile , callerLine , callerFunc ))
151163 }
152- return fmt .Sprintf (" %s:" , component )
153- }
154164
155- func formatFields (fields map [string ]any ) string {
156- parts := make ([]string , 0 , len (fields ))
157165 for k , v := range fields {
158- parts = append (parts , fmt .Sprintf ("%s=%v" , k , v ))
166+ event .Interface (k , v )
167+ }
168+
169+ event .Msg (message )
170+
171+ // Also log to file if enabled
172+ if fileLogger .GetLevel () != zerolog .NoLevel {
173+ fileEvent := getEvent (fileLogger , level )
174+
175+ if component != "" {
176+ fileEvent .Str ("component" , component )
177+ }
178+ for k , v := range fields {
179+ fileEvent .Interface (k , v )
180+ }
181+ fileEvent .Msg (message )
182+ }
183+
184+ if level == FATAL {
185+ os .Exit (1 )
159186 }
160- return fmt .Sprintf ("{%s}" , strings .Join (parts , ", " ))
161187}
162188
163189func Debug (message string ) {
@@ -232,6 +258,10 @@ func FatalC(component string, message string) {
232258 logMessage (FATAL , component , message , nil )
233259}
234260
261+ func Fatalf (message string , ss ... any ) {
262+ logMessage (FATAL , "" , fmt .Sprintf (message , ss ... ), nil )
263+ }
264+
235265func FatalF (message string , fields map [string ]any ) {
236266 logMessage (FATAL , "" , message , fields )
237267}
0 commit comments