package logging import ( "os" "path/filepath" "strings" "sync" "time" ) // dailyWriter는 날짜별 로그 파일(/-YYYY-MM-DD)에 기록하고, // 날짜가 바뀌면 새 파일로 전환하며 retention일보다 오래된 파일을 삭제한다. // 외부 의존성 없이 표준 라이브러리만 사용(저사양 타깃 정책 — 회전을 위해 lumberjack 등을 쓰지 않음). type dailyWriter struct { mu sync.Mutex dir string prefix string // 예: "app" ext string // 예: ".log" retention int // 보존 일수(오늘 포함) curDate string file *os.File } // newDailyWriter는 logFile(예: logs/app.log)을 기준으로 날짜별 writer를 만든다. // → logs/app-2026-06-11.log 형태로 기록. func newDailyWriter(logFile string, retention int) (*dailyWriter, error) { if retention <= 0 { retention = 7 } base := filepath.Base(logFile) ext := filepath.Ext(base) // ".log" (없으면 "") w := &dailyWriter{ dir: filepath.Dir(logFile), prefix: strings.TrimSuffix(base, ext), ext: ext, retention: retention, } if err := w.rotate(time.Now()); err != nil { return nil, err } return w, nil } func (w *dailyWriter) filename(date string) string { return filepath.Join(w.dir, w.prefix+"-"+date+w.ext) } func (w *dailyWriter) Write(p []byte) (int, error) { w.mu.Lock() defer w.mu.Unlock() if err := w.rotate(time.Now()); err != nil { return 0, err } return w.file.Write(p) } // rotate은 now 날짜의 파일을 연다. 이미 같은 날짜 파일이 열려 있으면 아무것도 안 한다. // (newDailyWriter에서 최초 1회, 이후 Write에서 lock 보유 상태로 호출) func (w *dailyWriter) rotate(now time.Time) error { date := now.Format("2006-01-02") if w.file != nil && date == w.curDate { return nil } if w.dir != "" && w.dir != "." { if err := os.MkdirAll(w.dir, 0o755); err != nil { return err } } f, err := os.OpenFile(w.filename(date), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) if err != nil { return err } if w.file != nil { _ = w.file.Close() } w.file = f w.curDate = date w.cleanup(now) return nil } // cleanup은 retention일치(오늘 포함)만 남기고 오래된 날짜 파일을 삭제한다. func (w *dailyWriter) cleanup(now time.Time) { cutoff := now.AddDate(0, 0, -(w.retention - 1)).Format("2006-01-02") matches, err := filepath.Glob(filepath.Join(w.dir, w.prefix+"-*"+w.ext)) if err != nil { return } for _, path := range matches { name := filepath.Base(path) ds := strings.TrimSuffix(strings.TrimPrefix(name, w.prefix+"-"), w.ext) if _, err := time.Parse("2006-01-02", ds); err != nil { continue // 날짜 형식이 아니면 우리 파일이 아님 — 건드리지 않음 } if ds < cutoff { // YYYY-MM-DD는 사전식 비교 = 날짜순 비교 _ = os.Remove(path) } } }