package main import "fmt" import "hodu" import "io" import "os" import "path/filepath" import "runtime" import "strings" import "sync" import "sync/atomic" import "time" type app_logger_msg_t struct { code int data string } type AppLogger struct { id string out io.Writer mask hodu.LogMask file *os.File file_name string // you can get the file name from file but this is to preserve the original. file_rotate int file_max_size int64 msg_chan chan app_logger_msg_t wg sync.WaitGroup closed atomic.Bool } func NewAppLogger (id string, w io.Writer, mask hodu.LogMask) *AppLogger { var l *AppLogger l = &AppLogger{ id: id, out: w, mask: mask, msg_chan: make(chan app_logger_msg_t, 256), } l.closed.Store(false) l.wg.Add(1) go l.logger_task() return l } func NewAppLoggerToFile (id string, file_name string, max_size int64, rotate int, mask hodu.LogMask) (*AppLogger, error) { var l *AppLogger var f *os.File var matched bool var err error f, err = os.OpenFile(file_name, os.O_CREATE | os.O_APPEND | os.O_WRONLY, 0666) if err != nil { return nil, err } if os.PathSeparator == '/' { // this check is performed only on systems where the path separator is /. matched, _ = filepath.Match("/dev/*", file_name) if matched { // if the log file is under /dev, disable rotation max_size = 0 rotate = 0 } } l = &AppLogger{ id: id, out: f, mask: mask, file: f, file_name: file_name, file_max_size: max_size, file_rotate: rotate, msg_chan: make(chan app_logger_msg_t, 256), } l.closed.Store(false) l.wg.Add(1) go l.logger_task() return l, nil } func (l *AppLogger) Close() { if l.closed.CompareAndSwap(false, true) { l.msg_chan <- app_logger_msg_t{code: 1} l.wg.Wait() if l.file != nil { l.file.Close() } } } func (l *AppLogger) Rotate() { l.msg_chan <- app_logger_msg_t{code: 2} } func (l *AppLogger) logger_task() { var msg app_logger_msg_t defer l.wg.Done() main_loop: for { select { case msg = <-l.msg_chan: if msg.code == 0 { //l.out.Write([]byte(msg)) io.WriteString(l.out, msg.data) if l.file_max_size > 0 && l.file != nil { var fi os.FileInfo var err error fi, err = l.file.Stat() if err == nil && fi.Size() >= l.file_max_size { l.rotate() } } } else if msg.code == 1 { break main_loop } else if msg.code == 2 { l.rotate() } // other code must not appear here. } } } func (l *AppLogger) Write(id string, level hodu.LogLevel, fmtstr string, args ...interface{}) { if l.mask & hodu.LogMask(level) == 0 { return } l.write(id, level, 1, fmtstr, args...) } func (l *AppLogger) WriteWithCallDepth(id string, level hodu.LogLevel, call_depth int, fmtstr string, args ...interface{}) { if l.mask & hodu.LogMask(level) == 0 { return } l.write(id, level, call_depth + 1, fmtstr, args...) } func (l *AppLogger) write(id string, level hodu.LogLevel, call_depth int, fmtstr string, args ...interface{}) { var now time.Time var off_m int var off_h int var off_s int var msg string var callerfile string var caller_line int var caller_ok bool var sb strings.Builder //if l.mask & hodu.LogMask(level) == 0 { return } now = time.Now() _, off_s = now.Zone() off_m = off_s / 60; off_h = off_m / 60; off_m = off_m % 60; if off_m < 0 { off_m = -off_m; } sb.WriteString( fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d %+03d%02d ", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second(), off_h, off_m)) _, callerfile, caller_line, caller_ok = runtime.Caller(1 + call_depth) if caller_ok { sb.WriteString(fmt.Sprintf("[%s:%d] ", filepath.Base(callerfile), caller_line)) } sb.WriteString(l.id) if id != "" { sb.WriteString("(") sb.WriteString(id) sb.WriteString(")") } sb.WriteString(": ") msg = fmt.Sprintf(fmtstr, args...) sb.WriteString(msg) if msg[len(msg) - 1] != '\n' { sb.WriteRune('\n') } // use queue to avoid blocking operation as much as possible l.msg_chan <- app_logger_msg_t{ code: 0, data: sb.String() } } func (l *AppLogger) rotate() { var f *os.File var fi os.FileInfo var i int var last_rot_no int var err error if l.file == nil { return } if l.file_rotate <= 0 { return } fi, err = l.file.Stat() if err == nil && fi.Size() <= 0 { return } for i = l.file_rotate - 1; i > 0; i-- { if os.Rename(fmt.Sprintf("%s.%d", l.file_name, i), fmt.Sprintf("%s.%d", l.file_name, i + 1)) == nil { if last_rot_no == 0 { last_rot_no = i + 1 } } } if os.Rename(l.file_name, fmt.Sprintf("%s.%d", l.file_name, 1)) == nil { if last_rot_no == 0 { last_rot_no = 1 } } f, err = os.OpenFile(l.file_name, os.O_CREATE | os.O_TRUNC | os.O_APPEND | os.O_WRONLY, 0666) if err != nil { l.file.Close() l.file = nil l.out = os.Stderr // don't reset l.file_name. you can derive that there was an error // if l.file_name is not blank, and if l.out is os.Stderr, } else { l.file.Close() l.file = f l.out = l.file } }