package logger import ( "errors" "fmt" "os" "path/filepath" "sync" "time" ) const ( defaultMaxFileBytes = 5 << 20 // 单个日志文件上限 5MB defaultBackups = 5 // 最多保留 5 个滚动文件 ) // fileWriter 负责把日志追加写入实例独占的文件,超过大小上限时按 .1/.2/... 滚动。 // // 文件名在创建时就用 O_EXCL 抢占,因此同一实例目录下不会出现两个进程写同一个 // 文件的情况(多开时每个实例有各自的目录,重启则拿到新的时间戳文件)。 type fileWriter struct { mu sync.Mutex dir string path string file *os.File size int64 maxBytes int64 backups int } // newFileWriter 在 dir 下创建一个独占的日志文件。 func newFileWriter(dir, instanceID string) (*fileWriter, error) { if dir == "" { return nil, errors.New("日志目录为空") } if err := os.MkdirAll(dir, 0o755); err != nil { return nil, err } if instanceID == "" { instanceID = "default" } base := filepath.Join(dir, fmt.Sprintf("huya-go_%s_%s", instanceID, time.Now().Format("20060102_150405"))) file, path, err := createUniqueLogFile(base) if err != nil { return nil, err } w := &fileWriter{ dir: dir, path: path, file: file, maxBytes: defaultMaxFileBytes, backups: defaultBackups, } if info, err := file.Stat(); err == nil { w.size = info.Size() } return w, nil } // createUniqueLogFile 以 O_EXCL 递增后缀创建日志文件。 // 秒级时间戳相同(例如重启)时自动追加 -2 / -3 后缀,避免两个进程写进同一个文件。 func createUniqueLogFile(base string) (*os.File, string, error) { for i := 1; i <= 50; i++ { path := base + ".log" if i > 1 { path = fmt.Sprintf("%s-%d.log", base, i) } file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644) if err == nil { return file, path, nil } if !errors.Is(err, os.ErrExist) { return nil, "", err } } return nil, "", fmt.Errorf("日志文件名连续冲突: %s", base) } // Dir 返回日志目录。 func (w *fileWriter) Dir() string { return w.dir } // Path 返回当前日志文件路径。 func (w *fileWriter) Path() string { w.mu.Lock() defer w.mu.Unlock() return w.path } // Write 追加一段日志,必要时先滚动。 func (w *fileWriter) Write(p []byte) (int, error) { w.mu.Lock() defer w.mu.Unlock() if w.file == nil { return 0, errors.New("日志文件已关闭") } if w.maxBytes > 0 && w.size+int64(len(p)) > w.maxBytes { if err := w.rotateLocked(); err != nil { return 0, err } } n, err := w.file.Write(p) w.size += int64(n) return n, err } // rotateLocked 关闭当前文件并重命名成 .1/.2/...,然后重新打开一个同名文件。 // 调用方必须已持有 w.mu。 func (w *fileWriter) rotateLocked() error { _ = w.file.Close() w.file = nil for i := w.backups - 1; i >= 1; i-- { from := fmt.Sprintf("%s.%d", w.path, i) if _, err := os.Stat(from); err != nil { continue } _ = os.Rename(from, fmt.Sprintf("%s.%d", w.path, i+1)) } if _, err := os.Stat(w.path); err == nil { _ = os.Rename(w.path, w.path+".1") } file, err := os.OpenFile(w.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) if err != nil { return err } w.file = file w.size = 0 return nil } // Close 关闭当前文件。 func (w *fileWriter) Close() error { if w == nil { return nil } w.mu.Lock() defer w.mu.Unlock() if w.file == nil { return nil } err := w.file.Close() w.file = nil return err } // writeHeader 把实例信息写在文件开头,确保排在任何日志之前。 func (w *fileWriter) writeHeader(instanceName, instanceID, dataDir string) { w.mu.Lock() defer w.mu.Unlock() if w.file == nil { return } header := logFileHeader(instanceName, instanceID, dataDir) if _, err := w.file.WriteString(header); err != nil { return } w.size += int64(len(header)) }