package logger import ( "io" "os" "path/filepath" "go.uber.org/zap" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" ) // Logger interface defines logging methods type Logger interface { Debug(msg string, fields map[string]interface{}) Info(msg string, fields map[string]interface{}) Warn(msg string, fields map[string]interface{}) Error(msg string, fields map[string]interface{}) Fatal(msg string, fields map[string]interface{}) WithFields(fields map[string]interface{}) Logger Sync() error // Flush any buffered log entries } // ZapLogger wraps zap logger type ZapLogger struct { logger *zap.Logger sugar *zap.SugaredLogger fields map[string]interface{} } type Config struct { Level string `mapstructure:"level"` Format string `mapstructure:"format"` Output string `mapstructure:"output"` File FileConfig `mapstructure:"file"` } type FileConfig struct { Path string `mapstructure:"path"` MaxSize int `mapstructure:"max_size"` // MB MaxBackups int `mapstructure:"max_backups"` MaxAge int `mapstructure:"max_age"` // days Compress bool `mapstructure:"compress"` } // NewLogger creates a new logger instance with Zap and Lumberjack func NewLogger(config *Config) Logger { // Parse log level level := parseLogLevel(config.Level) // Create encoder config encoderConfig := createEncoderConfig(config.Format) // Create encoder encoder := createEncoder(config.Format, encoderConfig) // Create writer writer := createWriter(config) // Create core core := zapcore.NewCore(encoder, writer, level) // Create logger with caller information logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1), zap.AddStacktrace(zapcore.ErrorLevel), ) return &ZapLogger{ logger: logger, sugar: logger.Sugar(), fields: make(map[string]interface{}), } } // createWriter creates the appropriate writer based on configuration func createWriter(config *Config) zapcore.WriteSyncer { switch config.Output { case "stdout": return zapcore.AddSync(os.Stdout) case "stderr": return zapcore.AddSync(os.Stderr) case "file": return createFileWriter(config.File) default: // Default to stdout return zapcore.AddSync(os.Stdout) } } // createFileWriter creates a file writer with rotation using lumberjack func createFileWriter(fileConfig FileConfig) zapcore.WriteSyncer { // Ensure log directory exists logDir := filepath.Dir(fileConfig.Path) if err := os.MkdirAll(logDir, 0755); err != nil { // Fallback to stdout if can't create directory return zapcore.AddSync(os.Stdout) } // Create lumberjack logger for rotation lumberjackLogger := &lumberjack.Logger{ Filename: fileConfig.Path, MaxSize: fileConfig.MaxSize, // MB MaxBackups: fileConfig.MaxBackups, MaxAge: fileConfig.MaxAge, // days Compress: fileConfig.Compress, } // Create multi-writer to write to both file and stdout multiWriter := io.MultiWriter(lumberjackLogger, os.Stdout) return zapcore.AddSync(multiWriter) } // createEncoderConfig creates encoder configuration func createEncoderConfig(format string) zapcore.EncoderConfig { config := zap.NewProductionEncoderConfig() // Customize timestamp format config.TimeKey = "timestamp" config.EncodeTime = zapcore.ISO8601TimeEncoder // Customize level encoding config.EncodeLevel = zapcore.LowercaseLevelEncoder // Customize caller encoding config.EncodeCaller = zapcore.ShortCallerEncoder // Customize duration encoding config.EncodeDuration = zapcore.StringDurationEncoder return config } // createEncoder creates the appropriate encoder func createEncoder(format string, config zapcore.EncoderConfig) zapcore.Encoder { switch format { case "json": return zapcore.NewJSONEncoder(config) case "console", "text": return zapcore.NewConsoleEncoder(config) default: return zapcore.NewJSONEncoder(config) } } // parseLogLevel converts string level to zapcore.Level func parseLogLevel(level string) zapcore.Level { switch level { case "debug": return zapcore.DebugLevel case "info": return zapcore.InfoLevel case "warn", "warning": return zapcore.WarnLevel case "error": return zapcore.ErrorLevel case "fatal": return zapcore.FatalLevel case "panic": return zapcore.PanicLevel default: return zapcore.InfoLevel } } // convertFields converts map[string]interface{} to zap fields func convertFields(fields map[string]interface{}) []zap.Field { zapFields := make([]zap.Field, 0, len(fields)) for key, value := range fields { zapFields = append(zapFields, zap.Any(key, value)) } return zapFields } // Debug logs a debug message func (l *ZapLogger) Debug(msg string, fields map[string]interface{}) { allFields := l.mergeFields(fields) l.logger.Debug(msg, convertFields(allFields)...) } // Info logs an info message func (l *ZapLogger) Info(msg string, fields map[string]interface{}) { allFields := l.mergeFields(fields) l.logger.Info(msg, convertFields(allFields)...) } // Warn logs a warning message func (l *ZapLogger) Warn(msg string, fields map[string]interface{}) { allFields := l.mergeFields(fields) l.logger.Warn(msg, convertFields(allFields)...) } // Error logs an error message func (l *ZapLogger) Error(msg string, fields map[string]interface{}) { allFields := l.mergeFields(fields) l.logger.Error(msg, convertFields(allFields)...) } // Fatal logs a fatal message and exits func (l *ZapLogger) Fatal(msg string, fields map[string]interface{}) { allFields := l.mergeFields(fields) l.logger.Fatal(msg, convertFields(allFields)...) } // WithFields returns a logger with predefined fields func (l *ZapLogger) WithFields(fields map[string]interface{}) Logger { newFields := make(map[string]interface{}) // Copy existing fields for k, v := range l.fields { newFields[k] = v } // Add new fields for k, v := range fields { newFields[k] = v } return &ZapLogger{ logger: l.logger, sugar: l.sugar, fields: newFields, } } // Sync flushes any buffered log entries func (l *ZapLogger) Sync() error { return l.logger.Sync() } // mergeFields merges instance fields with provided fields func (l *ZapLogger) mergeFields(fields map[string]interface{}) map[string]interface{} { if len(l.fields) == 0 { return fields } merged := make(map[string]interface{}) // Add instance fields first for k, v := range l.fields { merged[k] = v } // Add provided fields (they override instance fields) for k, v := range fields { merged[k] = v } return merged }