- 新增日志中心功能 (api/logapi.go + Logs.vue) - 新增GRE协议服务支持 (service/gre/) - 新增流量监控组件 (TrafficChart.vue) - 新增L2TP/IPSec测试用例 - 新增日志轮转服务 (logrotate.go) - 新增部署脚本 (scripts/install.sh, uninstall.sh, service文件) - 更新i18n国际化文件 - 优化后端API和前端路由
149 lines
3.4 KiB
Go
149 lines
3.4 KiB
Go
// Package service 提供日志轮转工具
|
||
package service
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strconv"
|
||
"sync"
|
||
)
|
||
|
||
// RotateConfig 配置日志轮转参数
|
||
type RotateConfig struct {
|
||
MaxSize int64 // 单个日志文件最大字节数
|
||
MaxFiles int // 最多保留的轮转文件数
|
||
}
|
||
|
||
// DefaultRotateConfig 从环境变量读取轮转配置
|
||
func DefaultRotateConfig(envPrefix string, defaultMaxSize int64, defaultMaxFiles int) RotateConfig {
|
||
cfg := RotateConfig{
|
||
MaxSize: defaultMaxSize,
|
||
MaxFiles: defaultMaxFiles,
|
||
}
|
||
if v := os.Getenv(envPrefix + "_MAX_SIZE"); v != "" {
|
||
if n, err := parseSize(v); err == nil && n > 0 {
|
||
cfg.MaxSize = n
|
||
}
|
||
}
|
||
if v := os.Getenv(envPrefix + "_MAX_FILES"); v != "" {
|
||
if n, err := strconv.Atoi(v); err == nil && n > 0 {
|
||
cfg.MaxFiles = n
|
||
}
|
||
}
|
||
return cfg
|
||
}
|
||
|
||
// parseSize 解析带单位的字节数(如 "50MB", "20M", "1048576")
|
||
func parseSize(s string) (int64, error) {
|
||
suffixes := map[string]int64{
|
||
"KB": 1024,
|
||
"MB": 1024 * 1024,
|
||
"GB": 1024 * 1024 * 1024,
|
||
"K": 1024,
|
||
"M": 1024 * 1024,
|
||
"G": 1024 * 1024 * 1024,
|
||
}
|
||
for suffix, mult := range suffixes {
|
||
if len(s) > len(suffix) && s[len(s)-len(suffix):] == suffix {
|
||
n, err := strconv.ParseInt(s[:len(s)-len(suffix)], 10, 64)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return n * mult, nil
|
||
}
|
||
}
|
||
return strconv.ParseInt(s, 10, 64)
|
||
}
|
||
|
||
// RotateIfNeeded 检查日志文件大小并在需要时轮转
|
||
func RotateIfNeeded(logPath string, cfg RotateConfig) error {
|
||
info, err := os.Stat(logPath)
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
return nil
|
||
}
|
||
return err
|
||
}
|
||
if info.Size() < cfg.MaxSize {
|
||
return nil
|
||
}
|
||
return rotateFiles(logPath, cfg.MaxFiles)
|
||
}
|
||
|
||
// rotateFiles 执行日志轮转:logPath -> logPath.1, logPath.N-1 -> delete
|
||
func rotateFiles(logPath string, maxFiles int) error {
|
||
// 删除最旧的文件
|
||
oldest := fmt.Sprintf("%s.%d", logPath, maxFiles)
|
||
os.Remove(oldest) // 忽略不存在的错误
|
||
|
||
// 逐个重命名:N-1 -> N, N-2 -> N-1, ... 1 -> 2
|
||
for i := maxFiles - 1; i >= 1; i-- {
|
||
src := fmt.Sprintf("%s.%d", logPath, i)
|
||
dst := fmt.Sprintf("%s.%d", logPath, i+1)
|
||
if _, err := os.Stat(src); err == nil {
|
||
os.Rename(src, dst)
|
||
}
|
||
}
|
||
// 当前文件 -> .1
|
||
if _, err := os.Stat(logPath); err == nil {
|
||
return os.Rename(logPath, logPath+".1")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// RotatingWriter 是一个带轮转功能的 io.Writer
|
||
type RotatingWriter struct {
|
||
mu sync.Mutex
|
||
path string
|
||
cfg RotateConfig
|
||
f *os.File
|
||
}
|
||
|
||
// NewRotatingWriter 创建一个带轮转功能的写入器
|
||
func NewRotatingWriter(path string, cfg RotateConfig) (*RotatingWriter, error) {
|
||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||
return nil, err
|
||
}
|
||
rw := &RotatingWriter{path: path, cfg: cfg}
|
||
if err := rw.open(); err != nil {
|
||
return nil, err
|
||
}
|
||
return rw, nil
|
||
}
|
||
|
||
func (w *RotatingWriter) open() error {
|
||
// 先尝试轮转
|
||
RotateIfNeeded(w.path, w.cfg)
|
||
f, err := os.OpenFile(w.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
w.f = f
|
||
return nil
|
||
}
|
||
|
||
func (w *RotatingWriter) Write(p []byte) (int, error) {
|
||
w.mu.Lock()
|
||
defer w.mu.Unlock()
|
||
n, err := w.f.Write(p)
|
||
if err != nil {
|
||
return n, err
|
||
}
|
||
// 每次写入后检查大小(懒轮转)
|
||
if info, e := w.f.Stat(); e == nil && info.Size() >= w.cfg.MaxSize {
|
||
w.f.Close()
|
||
w.open()
|
||
}
|
||
return n, nil
|
||
}
|
||
|
||
func (w *RotatingWriter) Close() error {
|
||
w.mu.Lock()
|
||
defer w.mu.Unlock()
|
||
if w.f != nil {
|
||
return w.f.Close()
|
||
}
|
||
return nil
|
||
}
|