新增单行文本编码器与结构化 GORM 日志,统一错误记录与请求日志策略,收紧日志文件权限并修复按天切分与压缩,支付回调参数脱敏,生产强制阿里云短信,RequestID 校验防注入,日志文案中文化。
249 lines
5.5 KiB
Go
249 lines
5.5 KiB
Go
package logging
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type dailyWriter struct {
|
|
mu sync.Mutex
|
|
dir string
|
|
prefix string
|
|
location *time.Location
|
|
retainDays int
|
|
day string
|
|
file *os.File
|
|
}
|
|
|
|
func (w *dailyWriter) Open() error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
return w.rotateIfNeeded(time.Now().In(w.location))
|
|
}
|
|
|
|
func newDailyWriter(dir string, prefix string, location *time.Location, retainDays int) *dailyWriter {
|
|
if retainDays < 0 {
|
|
retainDays = 0
|
|
}
|
|
return &dailyWriter{
|
|
dir: dir,
|
|
prefix: prefix,
|
|
location: location,
|
|
retainDays: retainDays,
|
|
}
|
|
}
|
|
|
|
func (w *dailyWriter) Write(p []byte) (int, error) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
if err := w.rotateIfNeeded(time.Now().In(w.location)); err != nil {
|
|
return 0, err
|
|
}
|
|
return w.file.Write(p)
|
|
}
|
|
|
|
func (w *dailyWriter) Sync() error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
if w.file == nil {
|
|
return nil
|
|
}
|
|
return w.file.Sync()
|
|
}
|
|
|
|
func (w *dailyWriter) rotateIfNeeded(now time.Time) error {
|
|
day := now.Format("2006-01-02")
|
|
if w.file != nil && w.day == day {
|
|
return nil
|
|
}
|
|
|
|
if err := os.MkdirAll(w.dir, 0o700); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Chmod(w.dir, 0o700); err != nil {
|
|
return err
|
|
}
|
|
|
|
if w.file != nil {
|
|
if err := w.file.Close(); err != nil {
|
|
return err
|
|
}
|
|
w.file = nil
|
|
}
|
|
|
|
path := filepath.Join(w.dir, fmt.Sprintf("%s-%s.log", w.prefix, day))
|
|
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := file.Chmod(0o600); err != nil {
|
|
_ = file.Close()
|
|
return err
|
|
}
|
|
w.file = file
|
|
w.day = day
|
|
|
|
// 异步补压缩全部历史日志并清理过期文件,避免漏掉停机期间的日期。
|
|
go w.maintain(now)
|
|
return nil
|
|
}
|
|
|
|
func (w *dailyWriter) maintain(now time.Time) {
|
|
if err := w.compressHistorical(now); err != nil {
|
|
writeMaintenanceError(err)
|
|
}
|
|
if w.retainDays > 0 {
|
|
if err := w.purgeOlderThan(now); err != nil {
|
|
writeMaintenanceError(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *dailyWriter) compressHistorical(now time.Time) error {
|
|
entries, err := os.ReadDir(w.dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
today := now.In(w.location).Format("2006-01-02")
|
|
for _, entry := range entries {
|
|
day, ok := w.dayFromFilename(entry.Name(), ".log")
|
|
if !ok || day >= today {
|
|
continue
|
|
}
|
|
if err := w.compressDay(day); err != nil {
|
|
return fmt.Errorf("压缩 %s 日志失败: %w", day, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *dailyWriter) compressDay(day string) error {
|
|
srcPath := filepath.Join(w.dir, fmt.Sprintf("%s-%s.log", w.prefix, day))
|
|
dstPath := srcPath + ".gz"
|
|
if _, err := os.Stat(dstPath); err == nil {
|
|
if err := validateGzip(dstPath); err == nil {
|
|
if err := os.Remove(srcPath); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
if err := os.Remove(dstPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
src, err := os.Open(srcPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
defer src.Close()
|
|
|
|
dst, err := os.CreateTemp(w.dir, "."+filepath.Base(dstPath)+".tmp-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmpPath := dst.Name()
|
|
defer func() { _ = os.Remove(tmpPath) }()
|
|
if err := dst.Chmod(0o600); err != nil {
|
|
_ = dst.Close()
|
|
return err
|
|
}
|
|
gz := gzip.NewWriter(dst)
|
|
if _, err := io.Copy(gz, src); err != nil {
|
|
_ = gz.Close()
|
|
_ = dst.Close()
|
|
return err
|
|
}
|
|
if err := gz.Close(); err != nil {
|
|
_ = dst.Close()
|
|
return err
|
|
}
|
|
if err := dst.Sync(); err != nil {
|
|
_ = dst.Close()
|
|
return err
|
|
}
|
|
if err := dst.Close(); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Rename(tmpPath, dstPath); err != nil {
|
|
return err
|
|
}
|
|
return os.Remove(srcPath)
|
|
}
|
|
|
|
func validateGzip(path string) error {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
reader, err := gzip.NewReader(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, copyErr := io.Copy(io.Discard, reader)
|
|
closeErr := reader.Close()
|
|
return errors.Join(copyErr, closeErr)
|
|
}
|
|
|
|
func (w *dailyWriter) purgeOlderThan(now time.Time) error {
|
|
// 按「日历天」比较:保留最近 retainDays 天(含当天),更早的 .log / .log.gz 删除。
|
|
today := time.Date(now.In(w.location).Year(), now.In(w.location).Month(), now.In(w.location).Day(), 0, 0, 0, 0, w.location)
|
|
cutoff := today.AddDate(0, 0, -(w.retainDays - 1))
|
|
entries, err := os.ReadDir(w.dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
prefix := w.prefix + "-"
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
name := entry.Name()
|
|
if !strings.HasPrefix(name, prefix) ||
|
|
(!strings.HasSuffix(name, ".log") && !strings.HasSuffix(name, ".log.gz")) {
|
|
continue
|
|
}
|
|
rest := strings.TrimPrefix(name, prefix)
|
|
rest = strings.TrimSuffix(rest, ".gz")
|
|
rest = strings.TrimSuffix(rest, ".log")
|
|
day, err := time.ParseInLocation("2006-01-02", rest, w.location)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if day.Before(cutoff) {
|
|
if err := os.Remove(filepath.Join(w.dir, name)); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *dailyWriter) dayFromFilename(name string, suffix string) (string, bool) {
|
|
prefix := w.prefix + "-"
|
|
if !strings.HasPrefix(name, prefix) || !strings.HasSuffix(name, suffix) {
|
|
return "", false
|
|
}
|
|
day := strings.TrimSuffix(strings.TrimPrefix(name, prefix), suffix)
|
|
if _, err := time.ParseInLocation("2006-01-02", day, w.location); err != nil {
|
|
return "", false
|
|
}
|
|
return day, true
|
|
}
|
|
|
|
func writeMaintenanceError(err error) {
|
|
_, _ = fmt.Fprintf(os.Stderr, "%s | ERROR | 日志维护失败 | error=%q\n", time.Now().Format("2006-01-02 15:04:05.000"), err.Error())
|
|
}
|