package logging import ( "fmt" "io" "os" "path/filepath" "strings" "sync" "time" "go.uber.org/zap" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" ) type Config struct { AppName string Environment string BasePath string Level string MaxSizeMB int MaxBackups int MaxAgeDays int Compress bool ConsoleEnabled bool } func New(cfg Config) (*zap.Logger, func() error, error) { cfg = finalizeConfig(cfg) infoWriter, err := newDailyRollingWriter(cfg, "info.log") if err != nil { return nil, nil, err } errorWriter, err := newDailyRollingWriter(cfg, "error.log") if err != nil { _ = infoWriter.Close() return nil, nil, err } level := parseLevel(cfg.Level) encoderConfig := zapcore.EncoderConfig{ TimeKey: "time", LevelKey: "level", NameKey: "logger", CallerKey: "caller", MessageKey: "msg", StacktraceKey: "stacktrace", LineEnding: zapcore.DefaultLineEnding, EncodeLevel: zapcore.CapitalLevelEncoder, EncodeTime: zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05.000"), EncodeDuration: zapcore.StringDurationEncoder, EncodeCaller: zapcore.ShortCallerEncoder, } jsonEncoder := zapcore.NewJSONEncoder(encoderConfig) consoleEncoder := zapcore.NewConsoleEncoder(encoderConfig) cores := make([]zapcore.Core, 0, 3) cores = append(cores, zapcore.NewCore( jsonEncoder, zapcore.AddSync(io.Writer(infoWriter)), zap.LevelEnablerFunc(func(l zapcore.Level) bool { return l >= level && l < zapcore.ErrorLevel }), )) cores = append(cores, zapcore.NewCore( jsonEncoder, zapcore.AddSync(io.Writer(errorWriter)), zap.LevelEnablerFunc(func(l zapcore.Level) bool { return l >= maxLevel(level, zapcore.ErrorLevel) }), )) if cfg.ConsoleEnabled { cores = append(cores, zapcore.NewCore( consoleEncoder, zapcore.AddSync(os.Stdout), zap.LevelEnablerFunc(func(l zapcore.Level) bool { return l >= level }), )) } logger := zap.New( zapcore.NewTee(cores...), zap.AddCaller(), zap.AddCallerSkip(1), zap.AddStacktrace(zapcore.ErrorLevel), zap.Fields( zap.String("app", cfg.AppName), zap.String("env", cfg.Environment), ), ) cleanup := func() error { var finalErr error if err := infoWriter.Close(); err != nil && finalErr == nil { finalErr = err } if err := errorWriter.Close(); err != nil && finalErr == nil { finalErr = err } if err := logger.Sync(); err != nil && finalErr == nil { finalErr = err } return finalErr } return logger, cleanup, nil } type dailyRollingWriter struct { mu sync.Mutex cfg Config fileName string dayKey string writer *lumberjack.Logger } func newDailyRollingWriter(cfg Config, fileName string) (*dailyRollingWriter, error) { w := &dailyRollingWriter{ cfg: cfg, fileName: fileName, } if err := w.ensureWriter(time.Now()); err != nil { return nil, err } return w, nil } func (w *dailyRollingWriter) Write(p []byte) (int, error) { w.mu.Lock() defer w.mu.Unlock() if err := w.ensureWriter(time.Now()); err != nil { return 0, err } return w.writer.Write(p) } func (w *dailyRollingWriter) Close() error { w.mu.Lock() defer w.mu.Unlock() if w.writer == nil { return nil } err := w.writer.Close() w.writer = nil return err } func (w *dailyRollingWriter) ensureWriter(now time.Time) error { dayKey := now.Format("2006-01-02") if w.writer != nil && w.dayKey == dayKey { return nil } monthDir := now.Format("2006-01") logDir := filepath.Join(w.cfg.BasePath, monthDir, dayKey) if err := os.MkdirAll(logDir, 0o755); err != nil { return fmt.Errorf("mkdir log dir failed: %w", err) } next := &lumberjack.Logger{ Filename: filepath.Join(logDir, w.fileName), MaxSize: w.cfg.MaxSizeMB, MaxBackups: w.cfg.MaxBackups, MaxAge: w.cfg.MaxAgeDays, Compress: w.cfg.Compress, } if w.writer != nil { _ = w.writer.Close() } w.writer = next w.dayKey = dayKey return nil } func finalizeConfig(cfg Config) Config { if strings.TrimSpace(cfg.AppName) == "" { cfg.AppName = "lsp-gateway" } if strings.TrimSpace(cfg.Environment) == "" { cfg.Environment = "dev" } if strings.TrimSpace(cfg.BasePath) == "" { cfg.BasePath = filepath.Join(".", "logs", cfg.AppName) } if strings.TrimSpace(cfg.Level) == "" { cfg.Level = "info" } if cfg.MaxSizeMB <= 0 { cfg.MaxSizeMB = 100 } if cfg.MaxBackups <= 0 { cfg.MaxBackups = 31 } if cfg.MaxAgeDays <= 0 { cfg.MaxAgeDays = 31 } return cfg } func parseLevel(level string) zapcore.Level { switch strings.ToLower(strings.TrimSpace(level)) { case "debug": return zapcore.DebugLevel case "info": return zapcore.InfoLevel case "warn", "warning": return zapcore.WarnLevel case "error": return zapcore.ErrorLevel default: return zapcore.InfoLevel } } func maxLevel(a, b zapcore.Level) zapcore.Level { if a > b { return a } return b }