Files
opsx-vpn/backend/internal/service/logrotate.go
T
opsx-tools a14d9e62e6 feat: 日志中心 + GRE协议支持 + 流量监控 + 部署脚本
- 新增日志中心功能 (api/logapi.go + Logs.vue)
- 新增GRE协议服务支持 (service/gre/)
- 新增流量监控组件 (TrafficChart.vue)
- 新增L2TP/IPSec测试用例
- 新增日志轮转服务 (logrotate.go)
- 新增部署脚本 (scripts/install.sh, uninstall.sh, service文件)
- 更新i18n国际化文件
- 优化后端API和前端路由
2026-07-25 08:50:34 +08:00

149 lines
3.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}