package logger import ( "fmt" "io" "log" "os" "path/filepath" "runtime" "strings" "sync" "time" ) type Level int const ( DEBUG Level = iota INFO WARN ERROR ) const maxLogAge = 30 * 24 * time.Hour var levelNames = map[Level]string{DEBUG: "DEBUG", INFO: "INFO", WARN: "WARN", ERROR: "ERROR"} var ( mu sync.Mutex std *log.Logger logDir string logLevel Level = INFO logFile *os.File todayDate string ) // Init sets up logging with daily rotation to dir/app-YYYY-MM-DD.log. func Init(dir, level string) { logDir = dir os.MkdirAll(dir, 0755) switch strings.ToUpper(level) { case "DEBUG": logLevel = DEBUG case "WARN": logLevel = WARN case "ERROR": logLevel = ERROR default: logLevel = INFO } openToday() go dailyCleanup() Info("Logger initialized, level=%s, dir=%s", level, dir) } func openToday() { if logFile != nil { logFile.Close() } todayDate = time.Now().Format("2006-01-02") f, err := os.OpenFile(filepath.Join(logDir, "app-"+todayDate+".log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { log.Printf("Failed to open log file: %v, logging to stdout only", err) std = log.New(os.Stdout, "", 0) logFile = nil return } logFile = f multi := io.MultiWriter(os.Stdout, f) std = log.New(multi, "", 0) } func dailyCleanup() { for { time.Sleep(1 * time.Hour) // Delete old logs cutoff := time.Now().Add(-maxLogAge) entries, _ := os.ReadDir(logDir) for _, e := range entries { if strings.HasPrefix(e.Name(), "app-") && strings.HasSuffix(e.Name(), ".log") { dateStr := strings.TrimPrefix(strings.TrimSuffix(e.Name(), ".log"), "app-") if t, err := time.Parse("2006-01-02", dateStr); err == nil && t.Before(cutoff) { os.Remove(filepath.Join(logDir, e.Name())) } } } } } func Close() { if logFile != nil { logFile.Close() } } func rotateIfNeeded() { d := time.Now().Format("2006-01-02") if d != todayDate { mu.Lock() if d != todayDate { openToday() } mu.Unlock() } } func output(level Level, format string, args ...any) { if level < logLevel { return } rotateIfNeeded() msg := fmt.Sprintf(format, args...) now := time.Now().Format("2006-01-02 15:04:05.000") _, file, line, _ := runtime.Caller(2) short := file[strings.LastIndex(file, "/")+1:] lineStr := fmt.Sprintf("%s [%s] %s:%d %s", now, levelNames[level], short, line, msg) mu.Lock() if std != nil { std.Println(lineStr) } mu.Unlock() } func Debug(format string, args ...any) { output(DEBUG, format, args...) } func Info(format string, args ...any) { output(INFO, format, args...) } func Warn(format string, args ...any) { output(WARN, format, args...) } func Error(format string, args ...any) { output(ERROR, format, args...) } func StdLogger() *log.Logger { if std != nil { return std } return log.New(os.Stdout, "", 0) }