package logger import ( "os" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // Logger wraps zap.SugaredLogger for structured logging. // Corresponds to Chatwoot's Rails Logger but with structured output. var globalLogger *zap.SugaredLogger // 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 { 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)) globalLogger = zapLogger.Sugar() return nil } // L returns the global sugared logger func L() *zap.SugaredLogger { if globalLogger == nil { // Fallback: if Init wasn't called, use a default logger zapLogger, _ := zap.NewProduction() globalLogger = zapLogger.Sugar() } return globalLogger } // Sync flushes any buffered log entries. Should be called before program exit. func Sync() { if globalLogger != nil { _ = globalLogger.Sync() } } 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) } }