160 lines
3.8 KiB
Go
160 lines
3.8 KiB
Go
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))
|
|
}
|