package starlog import ( "errors" "fmt" "os" "path/filepath" "strings" "sync" "time" ) var ErrRotatingFileSinkClosed = errors.New("rotating file sink closed") type RotatingFileSink struct { mu sync.Mutex path string policy RotatePolicy checkInterval time.Duration options RotateManageOptions appendMode bool file *os.File lastCheck time.Time closed bool } func normalizeRotateCheckInterval(interval time.Duration) time.Duration { if interval <= 0 { return time.Second } return interval } func ensureLogDir(path string) error { dir := filepath.Dir(path) if dir == "" || dir == "." { return nil } return os.MkdirAll(dir, 0755) } func newRotatePolicySink(path string, appendMode bool, policy RotatePolicy, checkInterval time.Duration, options RotateManageOptions) (*RotatingFileSink, error) { if policy == nil { return nil, errors.New("rotate policy is nil") } fullpath, err := filepath.Abs(path) if err != nil { return nil, err } sink := &RotatingFileSink{ path: fullpath, policy: policy, checkInterval: normalizeRotateCheckInterval(checkInterval), options: options, appendMode: appendMode, } if err = sink.openFileLocked(appendMode); err != nil { return nil, err } return sink, nil } func NewRotatePolicySink(path string, appendMode bool, policy RotatePolicy, checkInterval time.Duration) (*RotatingFileSink, error) { return newRotatePolicySink(path, appendMode, policy, checkInterval, RotateManageOptions{}) } func NewManagedRotatePolicySink(path string, appendMode bool, policy RotatePolicy, checkInterval time.Duration, options RotateManageOptions) (*RotatingFileSink, error) { return newRotatePolicySink(path, appendMode, policy, checkInterval, options) } func NewRotateByTimeSink(path string, appendMode bool, interval time.Duration, checkInterval time.Duration) (*RotatingFileSink, error) { return NewRotatePolicySink(path, appendMode, NewRotateByTimePolicy(interval), checkInterval) } func NewManagedRotateByTimeSink(path string, appendMode bool, interval time.Duration, checkInterval time.Duration, options RotateManageOptions) (*RotatingFileSink, error) { return NewManagedRotatePolicySink(path, appendMode, NewRotateByTimePolicy(interval), checkInterval, options) } func NewRotateBySizeSink(path string, appendMode bool, maxSizeBytes int64, checkInterval time.Duration) (*RotatingFileSink, error) { return NewRotatePolicySink(path, appendMode, NewRotateBySizePolicy(maxSizeBytes), checkInterval) } func NewManagedRotateBySizeSink(path string, appendMode bool, maxSizeBytes int64, checkInterval time.Duration, options RotateManageOptions) (*RotatingFileSink, error) { return NewManagedRotatePolicySink(path, appendMode, NewRotateBySizePolicy(maxSizeBytes), checkInterval, options) } func NewRotateByTimeSizeSink(path string, appendMode bool, interval time.Duration, maxSizeBytes int64, checkInterval time.Duration) (*RotatingFileSink, error) { return NewRotatePolicySink(path, appendMode, NewRotateByTimeSizePolicy(interval, maxSizeBytes), checkInterval) } func NewManagedRotateByTimeSizeSink(path string, appendMode bool, interval time.Duration, maxSizeBytes int64, checkInterval time.Duration, options RotateManageOptions) (*RotatingFileSink, error) { return NewManagedRotatePolicySink(path, appendMode, NewRotateByTimeSizePolicy(interval, maxSizeBytes), checkInterval, options) } func (sink *RotatingFileSink) openFileLocked(appendMode bool) error { if sink == nil { return nil } if err := ensureLogDir(sink.path); err != nil { return err } flags := os.O_CREATE | os.O_WRONLY if appendMode { flags |= os.O_APPEND } else { flags |= os.O_TRUNC } fp, err := os.OpenFile(sink.path, flags, 0644) if err != nil { return err } sink.file = fp return nil } func (sink *RotatingFileSink) shouldCheckRotateLocked(now time.Time) bool { if sink == nil { return false } if sink.lastCheck.IsZero() || now.Sub(sink.lastCheck) >= sink.checkInterval { sink.lastCheck = now return true } return false } func (sink *RotatingFileSink) rotateIfNeededLocked(now time.Time) error { if sink == nil || sink.file == nil || sink.policy == nil { return nil } info, err := sink.file.Stat() if err != nil { return err } entry := &Entry{Time: now} if !sink.policy.ShouldRotate(info, entry) { return nil } archivePath := strings.TrimSpace(resolveRotateArchivePath(sink.policy, sink.path, now)) if archivePath == "" || archivePath == sink.path { return nil } if err = ensureLogDir(archivePath); err != nil { return err } if err = sink.file.Close(); err != nil { return err } sink.file = nil if err = os.Rename(sink.path, archivePath); err != nil { reopenErr := sink.openFileLocked(true) if reopenErr != nil { return fmt.Errorf("rotate rename failed: %v; reopen failed: %w", err, reopenErr) } return err } if err = sink.openFileLocked(false); err != nil { return err } if err = ApplyRotateManageOptions(archivePath, sink.path, sink.options); err != nil { return err } return nil } func (sink *RotatingFileSink) Write(data []byte) error { if sink == nil { return nil } sink.mu.Lock() defer sink.mu.Unlock() if sink.closed { return ErrRotatingFileSinkClosed } if sink.file == nil { if err := sink.openFileLocked(true); err != nil { return err } } now := time.Now() if sink.shouldCheckRotateLocked(now) { if err := sink.rotateIfNeededLocked(now); err != nil { return err } } _, err := sink.file.Write(data) return err } func (sink *RotatingFileSink) Sync() error { if sink == nil { return nil } sink.mu.Lock() defer sink.mu.Unlock() if sink.file == nil { return nil } return sink.file.Sync() } func (sink *RotatingFileSink) Close() error { if sink == nil { return nil } sink.mu.Lock() defer sink.mu.Unlock() if sink.closed { return nil } sink.closed = true if sink.file == nil { return nil } err := sink.file.Close() sink.file = nil return err } func (sink *RotatingFileSink) Path() string { if sink == nil { return "" } sink.mu.Lock() defer sink.mu.Unlock() return sink.path }