日志按天分割并自动清理:app-YYYYMMDD.log,保留天数可配
- dailyFileWriter 按天轮转:跨天自动切换到新文件(app-20260804.log) - LOG_RETAIN_DAYS 控制保留天数(默认 30),到期自动删除 - docker-compose 透传 LOG_RETAIN_DAYS,README 更新环境变量说明 - 补充轮转/清理单测
This commit is contained in:
@@ -111,7 +111,8 @@ make docker-down # 停止并移除
|
|||||||
| `OPEN_API_SECRET` | 开放接口签名密钥 | 需修改 |
|
| `OPEN_API_SECRET` | 开放接口签名密钥 | 需修改 |
|
||||||
| `OPEN_SIGN_SKEW` | 签名时间戳偏差(秒) | `300` |
|
| `OPEN_SIGN_SKEW` | 签名时间戳偏差(秒) | `300` |
|
||||||
| `OPEN_API_DEBUG` | 开放接口调试日志 | debug 模式默认开启 |
|
| `OPEN_API_DEBUG` | 开放接口调试日志 | debug 模式默认开启 |
|
||||||
| `LOG_FILE` | 日志文件路径 | `logs/app.log` |
|
| `LOG_FILE` | 日志文件路径(按天分割为 `app-YYYYMMDD.log`) | `logs/app.log` |
|
||||||
|
| `LOG_RETAIN_DAYS` | 日志文件保留天数,到期自动清理 | `30` |
|
||||||
| `FULFILLMENT_PROCESSING_TIMEOUT_MINUTES` | 发货中订单自动标记失败的超时分钟数,<=0 关闭 | `30` |
|
| `FULFILLMENT_PROCESSING_TIMEOUT_MINUTES` | 发货中订单自动标记失败的超时分钟数,<=0 关闭 | `30` |
|
||||||
| `FULFILLMENT_TIMEOUT_SCAN_INTERVAL_SECONDS` | 发货超时巡检间隔秒数 | `60` |
|
| `FULFILLMENT_TIMEOUT_SCAN_INTERVAL_SECONDS` | 发货超时巡检间隔秒数 | `60` |
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ func main() {
|
|||||||
time.Local = timeutil.Location()
|
time.Local = timeutil.Location()
|
||||||
|
|
||||||
// 日志:控制台 + 文件
|
// 日志:控制台 + 文件
|
||||||
|
applog.SetRetainDays(cfg.LogRetainDays)
|
||||||
logFile, err := applog.Setup(cfg.LogFile)
|
logFile, err := applog.Setup(cfg.LogFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("setup log: %v", err)
|
log.Fatalf("setup log: %v", err)
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ type Config struct {
|
|||||||
OpenSignSkew int64
|
OpenSignSkew int64
|
||||||
// OpenAPIDebug 开放接口详细调试日志
|
// OpenAPIDebug 开放接口详细调试日志
|
||||||
OpenAPIDebug bool
|
OpenAPIDebug bool
|
||||||
// LogFile 日志文件路径;非空时同时写控制台与文件
|
// LogFile 日志文件路径;非空时同时写控制台与文件(按天分割)
|
||||||
LogFile string
|
LogFile string
|
||||||
|
// LogRetainDays 日志文件保留天数(按天分割后自动清理旧文件)
|
||||||
|
LogRetainDays int
|
||||||
// DeliveryBaseURL 用户侧发货页面基础地址;为空时前端可自行按当前域名拼接。
|
// DeliveryBaseURL 用户侧发货页面基础地址;为空时前端可自行按当前域名拼接。
|
||||||
DeliveryBaseURL string
|
DeliveryBaseURL string
|
||||||
// DeliveryBFFBaseURL 上游发货 BFF 地址。
|
// DeliveryBFFBaseURL 上游发货 BFF 地址。
|
||||||
@@ -66,6 +68,7 @@ func Load() *Config {
|
|||||||
OpenSignSkew: int64(getEnvInt("OPEN_SIGN_SKEW", 300)),
|
OpenSignSkew: int64(getEnvInt("OPEN_SIGN_SKEW", 300)),
|
||||||
OpenAPIDebug: debugOpen,
|
OpenAPIDebug: debugOpen,
|
||||||
LogFile: getEnv("LOG_FILE", "logs/app.log"),
|
LogFile: getEnv("LOG_FILE", "logs/app.log"),
|
||||||
|
LogRetainDays: getEnvInt("LOG_RETAIN_DAYS", 30),
|
||||||
DeliveryBaseURL: getEnv("DELIVERY_BASE_URL", ""),
|
DeliveryBaseURL: getEnv("DELIVERY_BASE_URL", ""),
|
||||||
DeliveryBFFBaseURL: getEnv("DELIVERY_BFF_BASE_URL", "https://www.jxya.top/bff"),
|
DeliveryBFFBaseURL: getEnv("DELIVERY_BFF_BASE_URL", "https://www.jxya.top/bff"),
|
||||||
DeliveryChannel: getEnv("DELIVERY_CHANNEL", "dlc"),
|
DeliveryChannel: getEnv("DELIVERY_CHANNEL", "dlc"),
|
||||||
|
|||||||
@@ -1,32 +1,141 @@
|
|||||||
package applog
|
package applog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Setup 将标准库 log、Gin 请求日志同时写到控制台与文件。
|
// logRetainDays 保留最近多少天的日志文件;<=0 不清理。
|
||||||
|
var logRetainDays = 30
|
||||||
|
|
||||||
|
// SetRetainDays 设置日志文件保留天数(可在 main 里按配置覆盖)。
|
||||||
|
func SetRetainDays(days int) {
|
||||||
|
if days > 0 {
|
||||||
|
logRetainDays = days
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dailyFileWriter 按天轮转的日志 writer。
|
||||||
|
// 文件命名:<前缀>-20060102<扩展>,如 app-20260803.log;跨天自动切换并清理过期文件。
|
||||||
|
type dailyFileWriter struct {
|
||||||
|
dir string
|
||||||
|
prefix string
|
||||||
|
ext string
|
||||||
|
mu sync.Mutex
|
||||||
|
current *os.File
|
||||||
|
currentDate string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDailyFileWriter(logFile string) (*dailyFileWriter, error) {
|
||||||
|
abs, err := filepath.Abs(logFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dir := filepath.Dir(abs)
|
||||||
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
base := filepath.Base(abs)
|
||||||
|
ext := filepath.Ext(base)
|
||||||
|
prefix := strings.TrimSuffix(base, ext)
|
||||||
|
w := &dailyFileWriter{dir: dir, prefix: prefix, ext: ext}
|
||||||
|
if err := w.rotateIfNeeded(time.Now()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathFor 当天日志文件路径。
|
||||||
|
func (w *dailyFileWriter) pathFor(t time.Time) string {
|
||||||
|
return filepath.Join(w.dir, fmt.Sprintf("%s-%s%s", w.prefix, t.Format("20060102"), w.ext))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *dailyFileWriter) rotateIfNeeded(now time.Time) error {
|
||||||
|
date := now.Format("20060102")
|
||||||
|
if w.current != nil && w.currentDate == date {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if w.current != nil {
|
||||||
|
_ = w.current.Close()
|
||||||
|
w.current = nil
|
||||||
|
}
|
||||||
|
f, err := os.OpenFile(w.pathFor(now), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.current = f
|
||||||
|
w.currentDate = date
|
||||||
|
go w.cleanup(now)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup 删除超出保留天数的历史日志文件。
|
||||||
|
func (w *dailyFileWriter) cleanup(now time.Time) {
|
||||||
|
if logRetainDays <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cutoff := now.AddDate(0, 0, -logRetainDays).Format("20060102")
|
||||||
|
entries, err := os.ReadDir(w.dir)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := entry.Name()
|
||||||
|
if !strings.HasPrefix(name, w.prefix+"-") || !strings.HasSuffix(name, w.ext) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
datePart := strings.TrimSuffix(strings.TrimPrefix(name, w.prefix+"-"), w.ext)
|
||||||
|
if len(datePart) != 8 || datePart >= cutoff {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_ = os.Remove(filepath.Join(w.dir, name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *dailyFileWriter) Write(p []byte) (int, error) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
if err := w.rotateIfNeeded(time.Now()); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return w.current.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *dailyFileWriter) Close() error {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
if w.current != nil {
|
||||||
|
err := w.current.Close()
|
||||||
|
w.current = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup 将标准库 log、Gin 请求日志同时写到控制台与按天分割的文件。
|
||||||
// logFile 为空则仅控制台。返回文件句柄供进程结束时 Close(可为 nil)。
|
// logFile 为空则仅控制台。返回文件句柄供进程结束时 Close(可为 nil)。
|
||||||
func Setup(logFile string) (*os.File, error) {
|
func Setup(logFile string) (*os.File, error) {
|
||||||
writers := []io.Writer{os.Stdout}
|
writers := []io.Writer{os.Stdout}
|
||||||
var f *os.File
|
var f *os.File
|
||||||
if logFile != "" {
|
if logFile != "" {
|
||||||
dir := filepath.Dir(logFile)
|
daily, err := newDailyFileWriter(logFile)
|
||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
f, err = os.OpenFile(logFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
writers = append(writers, f)
|
f = daily.current
|
||||||
|
writers = append(writers, daily)
|
||||||
}
|
}
|
||||||
|
|
||||||
mw := io.MultiWriter(writers...)
|
mw := io.MultiWriter(writers...)
|
||||||
@@ -39,7 +148,7 @@ func Setup(logFile string) (*os.File, error) {
|
|||||||
|
|
||||||
if logFile != "" {
|
if logFile != "" {
|
||||||
abs, _ := filepath.Abs(logFile)
|
abs, _ := filepath.Abs(logFile)
|
||||||
log.Printf("[applog] 日志同时输出到控制台与文件: %s", abs)
|
log.Printf("[applog] 日志按天分割输出到 %s-YYYYMMDD.log(保留 %d 天)", strings.TrimSuffix(abs, filepath.Ext(abs)), logRetainDays)
|
||||||
}
|
}
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package applog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDailyFileWriterRotatesAndCleans(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
logFile := filepath.Join(dir, "app.log")
|
||||||
|
|
||||||
|
w, err := newDailyFileWriter(logFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("new writer: %v", err)
|
||||||
|
}
|
||||||
|
defer w.Close()
|
||||||
|
|
||||||
|
// 写入当天日志,文件应按 app-YYYYMMDD.log 命名
|
||||||
|
today := time.Now().Format("20060102")
|
||||||
|
if _, err := w.Write([]byte("line1\n")); err != nil {
|
||||||
|
t.Fatalf("write: %v", err)
|
||||||
|
}
|
||||||
|
todayPath := filepath.Join(dir, "app-"+today+".log")
|
||||||
|
if _, err := os.Stat(todayPath); err != nil {
|
||||||
|
t.Fatalf("today log file missing: %v", err)
|
||||||
|
}
|
||||||
|
// 旧格式 app.log 不应被创建
|
||||||
|
if _, err := os.Stat(logFile); err == nil {
|
||||||
|
t.Fatalf("old-style app.log should not be created")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造过期文件,cleanup 应删除(保留 30 天,这里用 3 天验证)
|
||||||
|
old := filepath.Join(dir, "app-20260701.log")
|
||||||
|
if err := os.WriteFile(old, []byte("old"), 0o644); err != nil {
|
||||||
|
t.Fatalf("create old file: %v", err)
|
||||||
|
}
|
||||||
|
w.cleanup(time.Now())
|
||||||
|
if _, err := os.Stat(old); err == nil {
|
||||||
|
t.Fatalf("old log file should be cleaned")
|
||||||
|
}
|
||||||
|
// 当天的文件应保留
|
||||||
|
if _, err := os.Stat(todayPath); err != nil {
|
||||||
|
t.Fatalf("today log file should be kept")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ services:
|
|||||||
- OPEN_SIGN_SKEW=${OPEN_SIGN_SKEW:-300}
|
- OPEN_SIGN_SKEW=${OPEN_SIGN_SKEW:-300}
|
||||||
- OPEN_API_DEBUG=${OPEN_API_DEBUG:-false}
|
- OPEN_API_DEBUG=${OPEN_API_DEBUG:-false}
|
||||||
- LOG_FILE=${LOG_FILE:-logs/app.log}
|
- LOG_FILE=${LOG_FILE:-logs/app.log}
|
||||||
|
- LOG_RETAIN_DAYS=${LOG_RETAIN_DAYS:-30}
|
||||||
- DELIVERY_BASE_URL=${DELIVERY_BASE_URL:-}
|
- DELIVERY_BASE_URL=${DELIVERY_BASE_URL:-}
|
||||||
- DELIVERY_BFF_BASE_URL=${DELIVERY_BFF_BASE_URL:-https://www.jxya.top/bff}
|
- DELIVERY_BFF_BASE_URL=${DELIVERY_BFF_BASE_URL:-https://www.jxya.top/bff}
|
||||||
- DELIVERY_CHANNEL=${DELIVERY_CHANNEL:-dlc}
|
- DELIVERY_CHANNEL=${DELIVERY_CHANNEL:-dlc}
|
||||||
|
|||||||
Reference in New Issue
Block a user