starlog/rotating_sink.go

229 lines
6.0 KiB
Go
Raw Permalink Normal View History

2026-03-19 16:37:57 +08:00
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
}