package starlog import ( "context" "errors" "os" "path/filepath" "time" "b612.me/starlog/internal/archivex" ) var archiveStore = archivex.NewStore() type Archive interface { ShouldArchiveNow(*StarLogger, string, os.FileInfo) bool NextLogFilePath(*StarLogger, string, os.FileInfo) string ArchiveLogFilePath(*StarLogger, string, os.FileInfo) string Interval() int64 // Deprecated: use HookBeforeArchive on concrete implementations. HookBeforArchive() func(*StarLogger, string, string, os.FileInfo) error //archivePath;currentPath HookAfterArchive() func(*StarLogger, string, string, os.FileInfo) error //archivePath;currentPath DoArchive() func(*StarLogger, string, string, os.FileInfo) error } type archiveStrategy struct { interval int64 shouldRotate func(string, os.FileInfo, *Entry) bool nextLogPath func(string, os.FileInfo, time.Time) string archivePath func(string, os.FileInfo, time.Time) string beforeHook func(*StarLogger, string, string, os.FileInfo) error afterHook func(*StarLogger, string, string, os.FileInfo) error doArchive func(*StarLogger, string, string, os.FileInfo) error } type archiveBeforeHookProvider interface { HookBeforeArchive() func(*StarLogger, string, string, os.FileInfo) error } func SetLogFile(path string, logger *StarLogger, appendMode bool) error { var fileMode int if appendMode { fileMode = os.O_APPEND | os.O_CREATE | os.O_WRONLY } else { fileMode = os.O_CREATE | os.O_WRONLY | os.O_TRUNC } fullpath, err := filepath.Abs(path) if err != nil { return err } if !appendMode && Exists(fullpath) { os.Remove(fullpath) } fp, err := os.OpenFile(fullpath, fileMode, 0644) if err != nil { return err } if current, ok := archiveStore.GetFile(logger.logcore.id); ok { logger.SetSwitching(true) err := current.Pointer.Close() if err != nil { logger.SetWriter(nil) logger.SetSwitching(false) return err } err = archiveStore.DeleteFile(logger.logcore.id) if err != nil { logger.SetWriter(nil) logger.SetSwitching(false) return err } } err = archiveStore.SetFile(logger.logcore.id, archivex.FileRecord{ FullPath: fullpath, Pointer: fp, }) if err != nil { fp.Close() logger.SetWriter(nil) logger.SetSwitching(false) return err } logger.SetSwitching(true) logger.SetWriter(fp) logger.SetSwitching(false) return nil } func CloseLogFileWithSwitching(logger *StarLogger) error { if current, ok := archiveStore.GetFile(logger.logcore.id); ok { logger.SetSwitching(true) err := current.Pointer.Close() if err != nil { logger.SetWriter(nil) return err } err = archiveStore.DeleteFile(logger.logcore.id) if err != nil { return err } logger.logcore.mu.Lock() if logger.logcore.output == current.Pointer { logger.logcore.output = nil } logger.logcore.mu.Unlock() } return nil } func CloseLogFile(logger *StarLogger) error { defer logger.SetSwitching(false) return CloseLogFileWithSwitching(logger) } // Deprecated: use CloseLogFileWithSwitching. func CloseWithSwitching(logger *StarLogger) error { return CloseLogFileWithSwitching(logger) } // Deprecated: use (*StarLogger).Close or CloseLogFile. func Close(logger *StarLogger) error { return CloseLogFile(logger) } func GetLogFileInfo(logger *StarLogger) (os.FileInfo, error) { if current, ok := archiveStore.GetFile(logger.logcore.id); ok { return current.Pointer.Stat() } return nil, errors.New("logger don't have a register logfile") } func resolveBeforeHook(arch Archive) func(*StarLogger, string, string, os.FileInfo) error { if arch == nil { return nil } if hookProvider, ok := arch.(archiveBeforeHookProvider); ok { return hookProvider.HookBeforeArchive() } return arch.HookBeforArchive() } func startArchiveWithStrategy(logger *StarLogger, strategy archiveStrategy) error { archiveKey := "arch" + logger.logcore.id if _, ok := archiveStore.GetRunner(archiveKey); ok { return errors.New("already running") } interval := strategy.interval if interval <= 0 { interval = 1 } ctx, cancel := context.WithCancel(context.Background()) runner := &archivex.Runner{ Cancel: cancel, Done: make(chan struct{}), } if err := archiveStore.SetRunner(archiveKey, runner); err != nil { cancel() return err } go func(ctx context.Context, runner *archivex.Runner, logger *StarLogger) { defer close(runner.Done) defer archiveStore.DeleteRunner(archiveKey) ticker := time.NewTicker(time.Second * time.Duration(interval)) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: } fileinfo, err := GetLogFileInfo(logger) if err != nil { logger.Errorf("cannot get log file info,reason is %v\n", err) continue } current, ok := archiveStore.GetFile(logger.logcore.id) if !ok { logger.Errorf("cannot get log core info from the map:no such keys\n") continue } fullpath := current.FullPath now := time.Now() entry := &Entry{ Time: now, LoggerName: logger.GetName(), } if strategy.shouldRotate == nil || !strategy.shouldRotate(fullpath, fileinfo, entry) { continue } newLogPath := fullpath if strategy.nextLogPath != nil { newLogPath = strategy.nextLogPath(fullpath, fileinfo, now) } if newLogPath == "" { newLogPath = fullpath } archiveLogPath := fullpath if strategy.archivePath != nil { archiveLogPath = strategy.archivePath(fullpath, fileinfo, now) } if archiveLogPath == "" { logger.Errorf("archive path cannot be empty\n") continue } if strategy.doArchive == nil && archiveLogPath == fullpath { logger.Errorf("archive path equals current path, skip rotate\n") continue } if strategy.beforeHook != nil { if err := strategy.beforeHook(logger, archiveLogPath, fullpath, fileinfo); err != nil { logger.Errorf("error occur while executing hook before archive,detail is %v\n", err) continue } } err = CloseLogFileWithSwitching(logger) if err != nil { continue } if strategy.doArchive == nil { err = os.Rename(fullpath, archiveLogPath) if err != nil { continue } } else { err = strategy.doArchive(logger, fullpath, archiveLogPath, fileinfo) if err != nil { logger.Errorf("error occur while executing archive log file,detail is %v\n", err) continue } } if err := SetLogFile(newLogPath, logger, false); err != nil { logger.Errorf("error occur while executing coverting new log file,detail is %v\n", err) continue } else { logger.Debugln("Archive Log Success") } fileinfo, err = GetLogFileInfo(logger) if err != nil { logger.Errorf("cannot get new log core info from the map:no such keys\n") continue } if strategy.afterHook != nil { if err := strategy.afterHook(logger, archiveLogPath, newLogPath, fileinfo); err != nil { logger.Errorf("error occur while executing hook after archive,detail is %v\n", err) continue } } } }(ctx, runner, logger) return nil } func buildRotateStrategy(policy RotatePolicy, checkInterval int64) archiveStrategy { return archiveStrategy{ interval: checkInterval, shouldRotate: func(fullpath string, info os.FileInfo, entry *Entry) bool { return policy.ShouldRotate(info, entry) }, nextLogPath: func(oldpath string, info os.FileInfo, now time.Time) string { return oldpath }, archivePath: func(oldpath string, info os.FileInfo, now time.Time) string { return resolveRotateArchivePath(policy, oldpath, now) }, } } func buildRotateArchiveStrategy(archive *RotatePolicyArchive) archiveStrategy { strategy := buildRotateStrategy(archive.policy, archive.checkInterval) strategy.beforeHook = archive.HookBeforeArchive() strategy.afterHook = archive.HookAfterArchive() if archive.DoArchive() != nil { strategy.doArchive = archive.DoArchive() } if archive.Interval() > 0 { strategy.interval = archive.Interval() } return strategy } func buildLegacyArchiveStrategy(logger *StarLogger, arch Archive) archiveStrategy { return archiveStrategy{ interval: arch.Interval(), shouldRotate: func(fullpath string, info os.FileInfo, entry *Entry) bool { return arch.ShouldArchiveNow(logger, fullpath, info) }, nextLogPath: func(oldpath string, info os.FileInfo, now time.Time) string { return arch.NextLogFilePath(logger, oldpath, info) }, archivePath: func(oldpath string, info os.FileInfo, now time.Time) string { return arch.ArchiveLogFilePath(logger, oldpath, info) }, beforeHook: resolveBeforeHook(arch), afterHook: arch.HookAfterArchive(), doArchive: arch.DoArchive(), } } func StartRotatePolicy(logger *StarLogger, policy RotatePolicy, checkInterval int64) error { if policy == nil { return errors.New("rotate policy is nil") } return startArchiveWithStrategy(logger, buildRotateStrategy(policy, checkInterval)) } // Deprecated: prefer StartRotatePolicy or StartManagedRotatePolicy. func StartArchive(logger *StarLogger, arch Archive) error { if arch == nil { return errors.New("archive is nil") } if rotateArchive, ok := arch.(*RotatePolicyArchive); ok && rotateArchive != nil && rotateArchive.policy != nil { return startArchiveWithStrategy(logger, buildRotateArchiveStrategy(rotateArchive)) } return startArchiveWithStrategy(logger, buildLegacyArchiveStrategy(logger, arch)) } func IsArchiveRun(logger *StarLogger) bool { _, ok := archiveStore.GetRunner("arch" + logger.logcore.id) return ok } func StopArchive(logger *StarLogger) { archiveKey := "arch" + logger.logcore.id runner, ok := archiveStore.GetRunner(archiveKey) if !ok || runner == nil { return } runner.Cancel() <-runner.Done _ = archiveStore.DeleteRunner(archiveKey) } type ArchiveByDate struct { interval int64 checkInterval int64 baseFileStyle string archiveStyle string lastSwitchTime time.Time changeArchiveName bool hookBefore func(*StarLogger, string, string, os.FileInfo) error hookAfter func(*StarLogger, string, string, os.FileInfo) error } func (abd *ArchiveByDate) ShouldArchiveNow(l *StarLogger, fullpath string, info os.FileInfo) bool { if abd.lastSwitchTime.IsZero() { abd.lastSwitchTime = GetFileCreationTime(info) } sub := time.Now().Unix() - abd.lastSwitchTime.Unix() if sub >= abd.interval || abd.interval-sub <= abd.checkInterval/2 { return true } return false } func (abd *ArchiveByDate) NextLogFilePath(l *StarLogger, oldpath string, info os.FileInfo) string { var newName string dir := filepath.Dir(oldpath) if !abd.changeArchiveName { base := filepath.Base(abd.baseFileStyle) newName = base[:len(base)-len(filepath.Ext(base))] + time.Now().Format(abd.archiveStyle) } else { newName = abd.baseFileStyle } return filepath.Join(dir, newName) } func (abd *ArchiveByDate) ArchiveLogFilePath(l *StarLogger, oldpath string, info os.FileInfo) string { var newName string dir := filepath.Dir(oldpath) if abd.changeArchiveName { base := filepath.Base(abd.baseFileStyle) newName = base[:len(base)-len(filepath.Ext(base))] + time.Now().Format(abd.archiveStyle) } else { newName = abd.baseFileStyle } return filepath.Join(dir, newName) } func (abd *ArchiveByDate) Interval() int64 { return abd.checkInterval } func (abd *ArchiveByDate) HookBeforArchive() func(*StarLogger, string, string, os.FileInfo) error { return abd.HookBeforeArchive() } func (abd *ArchiveByDate) HookBeforeArchive() func(*StarLogger, string, string, os.FileInfo) error { return abd.hookBefore } func (abd *ArchiveByDate) HookAfterArchive() func(*StarLogger, string, string, os.FileInfo) error { return func(logger *StarLogger, s string, s2 string, info os.FileInfo) error { abd.lastSwitchTime = time.Now() if abd.hookAfter != nil { return abd.hookAfter(logger, s, s2, info) } return nil } } func (abd *ArchiveByDate) DoArchive() func(*StarLogger, string, string, os.FileInfo) error { return nil } func (abd *ArchiveByDate) SetHookBeforArchive(f func(*StarLogger, string, string, os.FileInfo) error) { abd.SetHookBeforeArchive(f) } func (abd *ArchiveByDate) SetHookBeforeArchive(f func(*StarLogger, string, string, os.FileInfo) error) { abd.hookBefore = f } func (abd *ArchiveByDate) SetHookAfterArchive(f func(*StarLogger, string, string, os.FileInfo) error) { abd.hookAfter = f } func NewArchiveByDate(archInterval int64, checkInterval int64, baseFileName string, archiveFileName string, changeArchiveName bool, hookbefore func(*StarLogger, string, string, os.FileInfo) error, hookafter func(*StarLogger, string, string, os.FileInfo) error) *ArchiveByDate { return &ArchiveByDate{ interval: archInterval, checkInterval: checkInterval, changeArchiveName: changeArchiveName, baseFileStyle: baseFileName, archiveStyle: archiveFileName, hookBefore: hookbefore, hookAfter: hookafter, } } type ArchiveBySize struct { size int64 checkInterval int64 changeArchiveName bool baseFileStyle string archiveStyle string hookBefore func(*StarLogger, string, string, os.FileInfo) error hookAfter func(*StarLogger, string, string, os.FileInfo) error } func (abd *ArchiveBySize) ShouldArchiveNow(l *StarLogger, fullpath string, info os.FileInfo) bool { if info.Size() > abd.size { return true } return false } func (abd *ArchiveBySize) NextLogFilePath(l *StarLogger, oldpath string, info os.FileInfo) string { var newName string dir := filepath.Dir(oldpath) if !abd.changeArchiveName { base := filepath.Base(abd.baseFileStyle) newName = base[:len(base)-len(filepath.Ext(base))] + time.Now().Format(abd.archiveStyle) } else { newName = abd.baseFileStyle } return filepath.Join(dir, newName) } func (abd *ArchiveBySize) ArchiveLogFilePath(l *StarLogger, oldpath string, info os.FileInfo) string { var newName string dir := filepath.Dir(oldpath) if abd.changeArchiveName { base := filepath.Base(abd.baseFileStyle) newName = base[:len(base)-len(filepath.Ext(base))] + time.Now().Format(abd.archiveStyle) } else { newName = abd.baseFileStyle } return filepath.Join(dir, newName) } func (abd *ArchiveBySize) Interval() int64 { return abd.checkInterval } func (abd *ArchiveBySize) HookBeforArchive() func(*StarLogger, string, string, os.FileInfo) error { return abd.HookBeforeArchive() } func (abd *ArchiveBySize) HookBeforeArchive() func(*StarLogger, string, string, os.FileInfo) error { return abd.hookBefore } func (abd *ArchiveBySize) HookAfterArchive() func(*StarLogger, string, string, os.FileInfo) error { return abd.hookAfter } func (abd *ArchiveBySize) SetHookBeforArchive(f func(*StarLogger, string, string, os.FileInfo) error) { abd.SetHookBeforeArchive(f) } func (abd *ArchiveBySize) SetHookBeforeArchive(f func(*StarLogger, string, string, os.FileInfo) error) { abd.hookBefore = f } func (abd *ArchiveBySize) SetHookAfterArchive(f func(*StarLogger, string, string, os.FileInfo) error) { abd.hookAfter = f } func (abd *ArchiveBySize) DoArchive() func(*StarLogger, string, string, os.FileInfo) error { return nil } func NewArchiveBySize(size int64, checkInterval int64, baseFileStyle, archiveFileStyle string, changeArchiveFileName bool, hookbefore func(*StarLogger, string, string, os.FileInfo) error, hookafter func(*StarLogger, string, string, os.FileInfo) error) *ArchiveBySize { return &ArchiveBySize{ size: size, checkInterval: checkInterval, baseFileStyle: baseFileStyle, archiveStyle: archiveFileStyle, hookBefore: hookbefore, hookAfter: hookafter, changeArchiveName: changeArchiveFileName, } } type ArchiveByDateSize struct { interval int64 size int64 checkInterval int64 changeArchiveName bool lastSwitchTime time.Time baseFileStyle string archiveStyle string hookBefore func(*StarLogger, string, string, os.FileInfo) error hookAfter func(*StarLogger, string, string, os.FileInfo) error } func (abd *ArchiveByDateSize) ShouldArchiveNow(l *StarLogger, fullpath string, info os.FileInfo) bool { if abd.lastSwitchTime.IsZero() { abd.lastSwitchTime = GetFileCreationTime(info) } if info.Size() > abd.size { return true } sub := time.Now().Unix() - abd.lastSwitchTime.Unix() if sub >= abd.interval || abd.interval-sub <= abd.checkInterval/2 { return true } return false } func (abd *ArchiveByDateSize) NextLogFilePath(l *StarLogger, oldpath string, info os.FileInfo) string { var newName string dir := filepath.Dir(oldpath) if !abd.changeArchiveName { base := filepath.Base(abd.baseFileStyle) newName = base[:len(base)-len(filepath.Ext(base))] + time.Now().Format(abd.archiveStyle) } else { newName = abd.baseFileStyle } return filepath.Join(dir, newName) } func (abd *ArchiveByDateSize) ArchiveLogFilePath(l *StarLogger, oldpath string, info os.FileInfo) string { var newName string dir := filepath.Dir(oldpath) if abd.changeArchiveName { base := filepath.Base(abd.baseFileStyle) newName = base[:len(base)-len(filepath.Ext(base))] + time.Now().Format(abd.archiveStyle) } else { newName = abd.baseFileStyle } return filepath.Join(dir, newName) } func (abd *ArchiveByDateSize) Interval() int64 { return abd.checkInterval } func (abd *ArchiveByDateSize) HookBeforArchive() func(*StarLogger, string, string, os.FileInfo) error { return abd.HookBeforeArchive() } func (abd *ArchiveByDateSize) HookBeforeArchive() func(*StarLogger, string, string, os.FileInfo) error { return abd.hookBefore } func (abd *ArchiveByDateSize) HookAfterArchive() func(*StarLogger, string, string, os.FileInfo) error { return func(logger *StarLogger, s string, s2 string, info os.FileInfo) error { abd.lastSwitchTime = time.Now() if abd.hookAfter != nil { return abd.hookAfter(logger, s, s2, info) } return nil } } func (abd *ArchiveByDateSize) SetHookBeforArchive(f func(*StarLogger, string, string, os.FileInfo) error) { abd.SetHookBeforeArchive(f) } func (abd *ArchiveByDateSize) SetHookBeforeArchive(f func(*StarLogger, string, string, os.FileInfo) error) { abd.hookBefore = f } func (abd *ArchiveByDateSize) SetHookAfterArchive(f func(*StarLogger, string, string, os.FileInfo) error) { abd.hookAfter = f } func (abd *ArchiveByDateSize) DoArchive() func(*StarLogger, string, string, os.FileInfo) error { return nil } func NewArchiveByDateSize(size int64, interval int64, checkInterval int64, baseFileStyle, archiveFileStyle string, changeArchiveFileName bool, hookbefore func(*StarLogger, string, string, os.FileInfo) error, hookafter func(*StarLogger, string, string, os.FileInfo) error) *ArchiveByDateSize { return &ArchiveByDateSize{ size: size, interval: interval, checkInterval: checkInterval, baseFileStyle: baseFileStyle, archiveStyle: archiveFileStyle, hookBefore: hookbefore, hookAfter: hookafter, changeArchiveName: changeArchiveFileName, } }