// Package logging provides structured logging with zap package logging import ( "fmt" "os" "path/filepath" "strings" "go.uber.org/zap" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" "github.com/konduktor/konduktor/internal/config" ) // Config is a simple configuration for basic logger setup type Config struct { Level string TimestampFormat string } // Logger wraps zap.SugaredLogger with additional functionality type Logger struct { *zap.SugaredLogger zap *zap.Logger config *config.LoggingConfig name string } // New creates a new Logger with basic configuration func New(cfg Config) (*Logger, error) { level := parseLevel(cfg.Level) timestampFormat := cfg.TimestampFormat if timestampFormat == "" { timestampFormat = "2006-01-02 15:04:05" } encoderConfig := zap.NewProductionEncoderConfig() encoderConfig.TimeKey = "timestamp" encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout(timestampFormat) encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder core := zapcore.NewCore( zapcore.NewConsoleEncoder(encoderConfig), zapcore.AddSync(os.Stdout), level, ) zapLogger := zap.New(core) return &Logger{ SugaredLogger: zapLogger.Sugar(), zap: zapLogger, name: "konduktor", }, nil } // NewFromConfig creates a Logger from full LoggingConfig func NewFromConfig(cfg config.LoggingConfig) (*Logger, error) { var cores []zapcore.Core // Parse main level mainLevel := parseLevel(cfg.Level) // Add console core if enabled if cfg.ConsoleOutput { consoleLevel := mainLevel if cfg.Console != nil && cfg.Console.Level != "" { consoleLevel = parseLevel(cfg.Console.Level) } var consoleEncoder zapcore.Encoder formatConfig := cfg.Format if cfg.Console != nil { formatConfig = mergeFormatConfig(cfg.Format, cfg.Console.Format) } encoderCfg := createEncoderConfig(formatConfig) if formatConfig.Type == "json" { consoleEncoder = zapcore.NewJSONEncoder(encoderCfg) } else { if formatConfig.UseColors { encoderCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder } consoleEncoder = zapcore.NewConsoleEncoder(encoderCfg) } consoleSyncer := zapcore.AddSync(os.Stdout) cores = append(cores, zapcore.NewCore(consoleEncoder, consoleSyncer, consoleLevel)) } // Add file cores for _, fileConfig := range cfg.Files { fileCore, err := createFileCore(fileConfig, cfg.Format, mainLevel) if err != nil { return nil, fmt.Errorf("failed to create file logger for %s: %w", fileConfig.Path, err) } // If specific loggers are configured, wrap with filter if len(fileConfig.Loggers) > 0 { fileCore = &filteredCore{ Core: fileCore, loggers: fileConfig.Loggers, } } cores = append(cores, fileCore) } // If no cores configured, add default console if len(cores) == 0 { encoderCfg := zap.NewProductionEncoderConfig() encoderCfg.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05") encoderCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder cores = append(cores, zapcore.NewCore( zapcore.NewConsoleEncoder(encoderCfg), zapcore.AddSync(os.Stdout), mainLevel, )) } // Combine all cores core := zapcore.NewTee(cores...) zapLogger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)) return &Logger{ SugaredLogger: zapLogger.Sugar(), zap: zapLogger, config: &cfg, name: "konduktor", }, nil } // Named returns a logger with a specific name (for filtering) func (l *Logger) Named(name string) *Logger { return &Logger{ SugaredLogger: l.SugaredLogger.Named(name), zap: l.zap.Named(name), config: l.config, name: name, } } // With returns a logger with additional fields func (l *Logger) With(args ...interface{}) *Logger { return &Logger{ SugaredLogger: l.SugaredLogger.With(args...), zap: l.zap.Sugar().With(args...).Desugar(), config: l.config, name: l.name, } } // Sync flushes any buffered log entries func (l *Logger) Sync() error { return l.zap.Sync() } // GetZap returns the underlying zap.Logger func (l *Logger) GetZap() *zap.Logger { return l.zap } // Debug logs a debug message func (l *Logger) Debug(msg string, keysAndValues ...interface{}) { l.SugaredLogger.Debugw(msg, keysAndValues...) } // Info logs an info message func (l *Logger) Info(msg string, keysAndValues ...interface{}) { l.SugaredLogger.Infow(msg, keysAndValues...) } // Warn logs a warning message func (l *Logger) Warn(msg string, keysAndValues ...interface{}) { l.SugaredLogger.Warnw(msg, keysAndValues...) } // Error logs an error message func (l *Logger) Error(msg string, keysAndValues ...interface{}) { l.SugaredLogger.Errorw(msg, keysAndValues...) } // Fatal logs a fatal message and exits func (l *Logger) Fatal(msg string, keysAndValues ...interface{}) { l.SugaredLogger.Fatalw(msg, keysAndValues...) } // --- Helper functions --- func parseLevel(level string) zapcore.Level { switch strings.ToUpper(level) { case "DEBUG": return zapcore.DebugLevel case "INFO": return zapcore.InfoLevel case "WARN", "WARNING": return zapcore.WarnLevel case "ERROR": return zapcore.ErrorLevel case "CRITICAL", "FATAL": return zapcore.FatalLevel default: return zapcore.InfoLevel } } func createEncoderConfig(format config.LogFormatConfig) zapcore.EncoderConfig { timestampFormat := format.TimestampFormat if timestampFormat == "" { timestampFormat = "2006-01-02 15:04:05" } cfg := zapcore.EncoderConfig{ TimeKey: "timestamp", LevelKey: "level", NameKey: "logger", CallerKey: "caller", FunctionKey: zapcore.OmitKey, MessageKey: "msg", StacktraceKey: "stacktrace", LineEnding: zapcore.DefaultLineEnding, EncodeLevel: zapcore.CapitalLevelEncoder, EncodeTime: zapcore.TimeEncoderOfLayout(timestampFormat), EncodeDuration: zapcore.SecondsDurationEncoder, EncodeCaller: zapcore.ShortCallerEncoder, } if !format.ShowModule { cfg.NameKey = zapcore.OmitKey } return cfg } func mergeFormatConfig(base, override config.LogFormatConfig) config.LogFormatConfig { result := base if override.Type != "" { result.Type = override.Type } if override.TimestampFormat != "" { result.TimestampFormat = override.TimestampFormat } // UseColors and ShowModule are bool - check if override has non-default result.UseColors = override.UseColors result.ShowModule = override.ShowModule return result } func createFileCore(fileConfig config.FileLogConfig, defaultFormat config.LogFormatConfig, defaultLevel zapcore.Level) (zapcore.Core, error) { // Ensure directory exists dir := filepath.Dir(fileConfig.Path) if dir != "" && dir != "." { if err := os.MkdirAll(dir, 0755); err != nil { return nil, fmt.Errorf("failed to create log directory %s: %w", dir, err) } } // Configure log rotation with lumberjack maxSize := 10 // MB if fileConfig.MaxBytes > 0 { maxSize = int(fileConfig.MaxBytes / (1024 * 1024)) if maxSize < 1 { maxSize = 1 } } backupCount := 5 if fileConfig.BackupCount > 0 { backupCount = fileConfig.BackupCount } rotator := &lumberjack.Logger{ Filename: fileConfig.Path, MaxSize: maxSize, MaxBackups: backupCount, MaxAge: 30, // days Compress: true, } // Determine level level := defaultLevel if fileConfig.Level != "" { level = parseLevel(fileConfig.Level) } // Create encoder format := defaultFormat if fileConfig.Format.Type != "" { format = mergeFormatConfig(defaultFormat, fileConfig.Format) } // Files should not use colors format.UseColors = false encoderConfig := createEncoderConfig(format) var encoder zapcore.Encoder if format.Type == "json" { encoder = zapcore.NewJSONEncoder(encoderConfig) } else { encoder = zapcore.NewConsoleEncoder(encoderConfig) } return zapcore.NewCore(encoder, zapcore.AddSync(rotator), level), nil } // filteredCore wraps a Core to filter by logger name type filteredCore struct { zapcore.Core loggers []string } func (c *filteredCore) Check(entry zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { if !c.shouldLog(entry.LoggerName) { return ce } return c.Core.Check(entry, ce) } func (c *filteredCore) shouldLog(loggerName string) bool { if len(c.loggers) == 0 { return true } for _, allowed := range c.loggers { if loggerName == allowed || strings.HasPrefix(loggerName, allowed+".") { return true } } return false } func (c *filteredCore) With(fields []zapcore.Field) zapcore.Core { return &filteredCore{ Core: c.Core.With(fields), loggers: c.loggers, } }