Files
codit/backend/internal/util/logger.go

317 lines
6.7 KiB
Go

package util
import "fmt"
import "io"
import "os"
import "path/filepath"
import "runtime"
import "strings"
import "sync"
import "sync/atomic"
import "syscall"
import "time"
type LogLevel int
type LogMask int
const (
LOG_DEBUG LogLevel = 1 << iota
LOG_INFO
LOG_WARN
LOG_ERROR
)
const LOG_ALL LogMask = LogMask(LOG_DEBUG | LOG_INFO | LOG_WARN | LOG_ERROR)
const LOG_NONE LogMask = LogMask(0)
type logger_msg_t struct {
code int
data string
}
type Logger struct {
id string
out io.Writer
mask 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 logger_msg_t
wg sync.WaitGroup
use_color bool
closed atomic.Bool
}
func _is_ansi_tty(fd uintptr) bool {
var st syscall.Stat_t
var err error
err = syscall.Fstat(int(fd), &st)
if err != nil { return false }
if (st.Mode & syscall.S_IFMT) == syscall.S_IFCHR {
var term string
// i assume this fd is bound to the current terminal if it's a character device
// if the assumption is wrong, you simply get extraneous ansi code in the output.
term = os.Getenv("TERM")
if term != "" && term != "dumb" { return true }
}
return false
}
func NewLogger(id string, w io.Writer, mask LogMask) *Logger {
var l *Logger
var f *os.File
var ok bool
var use_color bool
use_color = false
f, ok = w.(*os.File)
if ok { use_color = _is_ansi_tty(f.Fd()) }
l = &Logger{
id: id,
out: w,
mask: mask,
msg_chan: make(chan logger_msg_t, 256),
use_color: use_color,
}
l.closed.Store(false)
l.wg.Add(1)
go l.logger_task()
return l
}
func NewLoggerToFile(id string, file_name string, max_size int64, rotate int, mask LogMask) (*Logger, error) {
var l *Logger
var f *os.File
var fi os.FileInfo
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 }
fi, err = f.Stat()
if err != nil || !fi.Mode().IsRegular() {
// disable rotation if the log file is not a regular file
max_size = 0
rotate = 0
}
l = &Logger{
id: id,
out: f,
mask: mask,
file: f,
file_name: file_name,
file_max_size: max_size,
file_rotate: rotate,
msg_chan: make(chan logger_msg_t, 256),
use_color: _is_ansi_tty(f.Fd()),
}
l.closed.Store(false)
l.wg.Add(1)
go l.logger_task()
return l, nil
}
func (l *Logger) Close() {
if l.closed.CompareAndSwap(false, true) {
l.msg_chan <- logger_msg_t{code: 1}
l.wg.Wait()
if l.file != nil { l.file.Close() }
}
}
func (l *Logger) Rotate() {
l.msg_chan <- logger_msg_t{code: 2}
}
func (l *Logger) logger_task() {
var msg 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 *Logger) Write(id string, level LogLevel, fmtstr string, args ...interface{}) {
if l.mask & LogMask(level) == 0 { return }
l.write(id, level, 1, fmtstr, args...)
}
func (l *Logger) WriteWithCallDepth(id string, level LogLevel, call_depth int, fmtstr string, args ...interface{}) {
if l.mask & LogMask(level) == 0 { return }
l.write(id, level, call_depth + 1, fmtstr, args...)
}
func (l *Logger) write(id string, level 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 & 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...)
if (l.use_color) {
var code string
code = l.log_level_to_ansi_code(level)
sb.WriteString(code)
sb.WriteString(msg)
if code != "" { sb.WriteString("\x1B[0m") }
} else {
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 <- logger_msg_t{ code: 0, data: sb.String() }
}
func (l *Logger) 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
}
}
func (l* Logger) log_level_to_ansi_code(level LogLevel) string {
switch level {
case LOG_ERROR:
return "\x1B[31m" // red
case LOG_WARN:
return "\x1B[33m" // yellow
case LOG_INFO:
if (l.mask & LogMask(LOG_DEBUG)) != 0 {
// if debug is enabled, change the color of info.
// otherwisse no color
return "\x1B[32m" // green
}
fallthrough
default:
return ""
}
}
func LogStrsToMask(str []string) LogMask {
var mask LogMask
if len(str) > 0 {
var name string
mask = LogMask(0)
for _, name = range str {
switch name {
case "all":
mask = LOG_ALL
case "none":
mask = LOG_NONE
case "debug":
mask |= LogMask(LOG_DEBUG)
case "info":
mask |= LogMask(LOG_INFO)
case "warn":
mask |= LogMask(LOG_WARN)
case "error":
mask |= LogMask(LOG_ERROR)
}
}
} else {
// if not specified, log messages of all levels
mask = LOG_ALL
}
return mask
}