// Package logging supplies readable console output and rotating structured files. package logging import ( "fmt" "os" "path/filepath" "go.uber.org/zap" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" ) type Options struct { Directory, Level string MaxSizeMB, MaxBackups, MaxAgeDays int } func New(o Options) (*zap.Logger, func(), error) { level, err := zapcore.ParseLevel(o.Level) if err != nil { return nil, nil, fmt.Errorf("invalid LOG_LEVEL") } if o.Directory == "" || o.MaxSizeMB < 1 || o.MaxBackups < 1 || o.MaxAgeDays < 1 { return nil, nil, fmt.Errorf("invalid logging configuration") } if err := os.MkdirAll(o.Directory, 0750); err != nil { return nil, nil, fmt.Errorf("create log directory: %w", err) } path := filepath.Join(o.Directory, "veyra.log") f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) if err != nil { return nil, nil, fmt.Errorf("open log file: %w", err) } if err := f.Close(); err != nil { return nil, nil, fmt.Errorf("close initial log file: %w", err) } file := &lumberjack.Logger{Filename: path, MaxSize: o.MaxSizeMB, MaxBackups: o.MaxBackups, MaxAge: o.MaxAgeDays, Compress: true, LocalTime: true} encoder := zap.NewProductionEncoderConfig() encoder.TimeKey = "time" encoder.MessageKey = "message" encoder.EncodeTime = zapcore.ISO8601TimeEncoder console := encoder console.EncodeLevel = zapcore.CapitalLevelEncoder core := zapcore.NewTee( zapcore.NewCore(zapcore.NewConsoleEncoder(console), zapcore.Lock(zapcore.AddSync(os.Stdout)), level), zapcore.NewCore(zapcore.NewJSONEncoder(encoder), zapcore.AddSync(file), level), ) logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.DPanicLevel)) return logger, func() { _ = logger.Sync(); _ = file.Close() }, nil }