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) } }