Files
2026-08-15 00:04:01 +08:00

145 lines
3.2 KiB
Go

package logger
import (
"os"
"sync/atomic"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// loggerState keeps a logger and its output handles together for atomic replacement.
type loggerState struct {
logger *zap.SugaredLogger
outputs []*os.File
}
var globalLogger atomic.Pointer[loggerState]
// Config for logger initialization
type Config struct {
Level string // debug, info, warn, error
Format string // json, console
Output string // stdout, stderr, or file path
ErrorOutput string // stderr or file path
}
// Init initializes the global logger based on config.
func Init(cfg Config) error {
level, err := parseLevel(cfg.Level)
if err != nil {
return err
}
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.TimeKey = "timestamp"
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
var encoder zapcore.Encoder
if cfg.Format == "console" {
encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
encoder = zapcore.NewConsoleEncoder(encoderConfig)
} else {
encoder = zapcore.NewJSONEncoder(encoderConfig)
}
outputWriter, err := getOutput(cfg.Output)
if err != nil {
return err
}
errorWriter, err := getOutput(cfg.ErrorOutput)
if err != nil {
closeOutput(outputWriter)
return err
}
core := zapcore.NewCore(
encoder,
zapcore.NewMultiWriteSyncer(zapcore.AddSync(outputWriter)),
level,
)
// Also send error-level logs to error output
errorCore := zapcore.NewCore(
encoder,
zapcore.AddSync(errorWriter),
zapcore.ErrorLevel,
)
combinedCore := zapcore.NewTee(core, errorCore)
zapLogger := zap.New(combinedCore, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
old := globalLogger.Swap(&loggerState{
logger: zapLogger.Sugar(),
outputs: []*os.File{outputWriter, errorWriter},
})
closeState(old)
return nil
}
// L returns the global sugared logger
func L() *zap.SugaredLogger {
if state := globalLogger.Load(); state != nil {
return state.logger
}
// Fallback: if Init wasn't called, use a default logger.
zapLogger, _ := zap.NewProduction()
state := &loggerState{logger: zapLogger.Sugar()}
if globalLogger.CompareAndSwap(nil, state) {
return state.logger
}
return globalLogger.Load().logger
}
// Sync flushes any buffered log entries. Should be called before program exit.
func Sync() {
if state := globalLogger.Load(); state != nil {
_ = state.logger.Sync()
}
}
func closeState(state *loggerState) {
if state == nil {
return
}
_ = state.logger.Sync()
for _, output := range state.outputs {
closeOutput(output)
}
}
func closeOutput(file *os.File) {
if file != nil && file != os.Stdout && file != os.Stderr {
_ = file.Close()
}
}
func parseLevel(levelStr string) (zapcore.Level, error) {
switch levelStr {
case "debug":
return zapcore.DebugLevel, nil
case "info":
return zapcore.InfoLevel, nil
case "warn":
return zapcore.WarnLevel, nil
case "error":
return zapcore.ErrorLevel, nil
default:
return zapcore.InfoLevel, nil
}
}
func getOutput(output string) (*os.File, error) {
switch output {
case "stdout":
return os.Stdout, nil
case "stderr":
return os.Stderr, nil
default:
return os.OpenFile(output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
}
}