Skip to content

Commit 100e30b

Browse files
committed
feat(log): 支持日志轮转
1 parent bd0dd05 commit 100e30b

6 files changed

Lines changed: 279 additions & 14 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ sudo ./anssl daemon -c config.yaml
175175
| `ssl.feiNiuEnabled` | ❌ | 飞牛 OS 证书部署开关,默认 false |
176176
| `ssl.onePanel.url` | ❌ | 1Panel 面板地址(如 http://localhost:10000) |
177177
| `ssl.onePanel.apiKey` | ❌ | 1Panel API 密钥,在面板设置中生成 |
178+
| `log.maxSizeMB` | ❌ | 单个日志文件最大体积,默认 20 MB |
179+
| `log.maxBackups` | ❌ | 最多保留的轮转日志数量,默认 5 |
180+
| `log.maxAgeDays` | ❌ | 轮转日志最长保留天数,默认 30 |
178181
| `provider` | ❌ | 云服务配置(阿里云/七牛云/腾讯云) |
179182

180183
## 故障排除

README_EN.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ sudo ./anssl daemon -c config.yaml
175175
| `ssl.feiNiuEnabled` | ❌ | FeiNiu OS deployment switch, default `false` |
176176
| `ssl.onePanel.url` | ❌ | 1Panel URL (e.g. `http://localhost:10000`) |
177177
| `ssl.onePanel.apiKey` | ❌ | 1Panel API key generated from panel settings |
178+
| `log.maxSizeMB` | ❌ | Max size per log file, default `20` MB |
179+
| `log.maxBackups` | ❌ | Max rotated log files to keep, default `5` |
180+
| `log.maxAgeDays` | ❌ | Max age for rotated logs, default `30` days |
178181
| `provider` | ❌ | Cloud provider config (Alibaba Cloud / Qiniu Cloud / Tencent Cloud) |
179182

180183
## Troubleshooting

cmd/daemon.go

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"syscall"
1414
"time"
1515

16+
"github.com/https-cert/deploy/internal/config"
1617
"github.com/https-cert/deploy/internal/updater"
1718
"github.com/spf13/cobra"
1819
)
@@ -84,25 +85,30 @@ func createSupervisorCmd() *cobra.Command {
8485

8586
// runSupervisor 运行守护进程监控器
8687
func runSupervisor() {
88+
if err := config.Init(ConfigFile); err != nil {
89+
fmt.Printf("初始化配置失败: %v\n", err)
90+
return
91+
}
92+
8793
execPath, err := os.Executable()
8894
if err != nil {
8995
fmt.Printf("获取可执行文件路径失败: %v\n", err)
9096
return
9197
}
9298

93-
supervisorLogFile, err := os.OpenFile(GetLogFile(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
99+
logWriter, err := newRotatingLogWriter(GetLogFile(), logRotationOptionsFromConfig())
94100
if err != nil {
95101
fmt.Printf("打开日志文件失败: %v\n", err)
96102
return
97103
}
98-
defer supervisorLogFile.Close()
104+
defer logWriter.Close()
99105

100106
logSupervisor := func(format string, args ...interface{}) {
101107
msg := fmt.Sprintf("[Supervisor %s] ", time.Now().Format("15:04:05"))
102108
msg += fmt.Sprintf(format, args...)
103109
msg += "\n"
104-
supervisorLogFile.WriteString(msg)
105-
supervisorLogFile.Sync()
110+
logWriter.Write([]byte(msg))
111+
logWriter.Sync()
106112
}
107113

108114
// 将 supervisor 自身的 PID 写入 PID 文件
@@ -156,18 +162,11 @@ func runSupervisor() {
156162

157163
cmd := exec.Command(execPath, "start", "-c", ConfigFile)
158164

159-
logFile, err := os.OpenFile(GetLogFile(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
160-
if err != nil {
161-
time.Sleep(restartDelay)
162-
continue
163-
}
164-
165-
cmd.Stdout = logFile
166-
cmd.Stderr = logFile
165+
cmd.Stdout = logWriter
166+
cmd.Stderr = logWriter
167167

168168
startTime := time.Now()
169169
if err := cmd.Start(); err != nil {
170-
logFile.Close()
171170
consecutiveFailures++
172171
time.Sleep(restartDelay)
173172
continue
@@ -178,7 +177,6 @@ func runSupervisor() {
178177
childMu.Unlock()
179178

180179
err = cmd.Wait()
181-
logFile.Close()
182180

183181
childMu.Lock()
184182
currentChild = nil

cmd/log_rotate.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"sort"
8+
"strings"
9+
"sync"
10+
"time"
11+
12+
"github.com/https-cert/deploy/internal/config"
13+
)
14+
15+
type logRotationOptions struct {
16+
// maxSizeBytes 是单个日志文件最大体积,0 表示不按大小轮转。
17+
maxSizeBytes int64
18+
// maxBackups 是最多保留的轮转文件数量,0 表示不按数量清理。
19+
maxBackups int
20+
// maxAge 是轮转文件最长保留时间,0 表示不按时间清理。
21+
maxAge time.Duration
22+
}
23+
24+
type rotatingLogWriter struct {
25+
// mu 保护文件句柄和轮转流程。
26+
mu sync.Mutex
27+
// path 是当前日志文件路径。
28+
path string
29+
// options 是日志轮转参数。
30+
options logRotationOptions
31+
// file 是当前打开的日志文件。
32+
file *os.File
33+
}
34+
35+
type rotatedLogFile struct {
36+
// path 是轮转日志文件路径。
37+
path string
38+
// modTime 是轮转日志文件修改时间。
39+
modTime time.Time
40+
}
41+
42+
// newRotatingLogWriter 创建带轮转能力的日志 writer。
43+
func newRotatingLogWriter(path string, options logRotationOptions) (*rotatingLogWriter, error) {
44+
writer := &rotatingLogWriter{
45+
path: path,
46+
options: options,
47+
}
48+
if err := writer.openLocked(); err != nil {
49+
return nil, err
50+
}
51+
return writer, nil
52+
}
53+
54+
// logRotationOptionsFromConfig 从当前配置读取日志轮转参数。
55+
func logRotationOptionsFromConfig() logRotationOptions {
56+
cfg := config.GetConfig()
57+
if cfg == nil || cfg.Log == nil {
58+
return logRotationOptions{
59+
maxSizeBytes: 20 * 1024 * 1024,
60+
maxBackups: 5,
61+
maxAge: 30 * 24 * time.Hour,
62+
}
63+
}
64+
65+
return logRotationOptions{
66+
maxSizeBytes: int64(cfg.Log.MaxSizeMB) * 1024 * 1024,
67+
maxBackups: cfg.Log.MaxBackups,
68+
maxAge: time.Duration(cfg.Log.MaxAgeDays) * 24 * time.Hour,
69+
}
70+
}
71+
72+
// Write 写入日志,并在超过大小限制时轮转。
73+
func (w *rotatingLogWriter) Write(data []byte) (int, error) {
74+
w.mu.Lock()
75+
defer w.mu.Unlock()
76+
77+
if err := w.rotateIfNeededLocked(int64(len(data))); err != nil {
78+
return 0, err
79+
}
80+
return w.file.Write(data)
81+
}
82+
83+
// Close 关闭当前日志文件。
84+
func (w *rotatingLogWriter) Close() error {
85+
w.mu.Lock()
86+
defer w.mu.Unlock()
87+
88+
if w.file == nil {
89+
return nil
90+
}
91+
err := w.file.Close()
92+
w.file = nil
93+
return err
94+
}
95+
96+
// Sync 将当前日志文件刷盘。
97+
func (w *rotatingLogWriter) Sync() error {
98+
w.mu.Lock()
99+
defer w.mu.Unlock()
100+
101+
if w.file == nil {
102+
return nil
103+
}
104+
return w.file.Sync()
105+
}
106+
107+
// openLocked 打开当前日志文件。
108+
func (w *rotatingLogWriter) openLocked() error {
109+
if err := os.MkdirAll(filepath.Dir(w.path), 0755); err != nil {
110+
return fmt.Errorf("创建日志目录失败: %w", err)
111+
}
112+
file, err := os.OpenFile(w.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
113+
if err != nil {
114+
return fmt.Errorf("打开日志文件失败: %w", err)
115+
}
116+
w.file = file
117+
return nil
118+
}
119+
120+
// rotateIfNeededLocked 在需要时轮转日志文件。
121+
func (w *rotatingLogWriter) rotateIfNeededLocked(incomingSize int64) error {
122+
if w.options.maxSizeBytes <= 0 || w.file == nil {
123+
return nil
124+
}
125+
126+
info, err := w.file.Stat()
127+
if err != nil {
128+
return fmt.Errorf("读取日志文件状态失败: %w", err)
129+
}
130+
if info.Size()+incomingSize <= w.options.maxSizeBytes {
131+
return nil
132+
}
133+
134+
if err := w.file.Close(); err != nil {
135+
return fmt.Errorf("关闭日志文件失败: %w", err)
136+
}
137+
w.file = nil
138+
139+
rotatedPath := fmt.Sprintf("%s.%s", w.path, time.Now().Format("20060102150405.000000000"))
140+
if err := os.Rename(w.path, rotatedPath); err != nil && !os.IsNotExist(err) {
141+
return fmt.Errorf("轮转日志文件失败: %w", err)
142+
}
143+
if err := cleanupRotatedLogs(w.path, w.options); err != nil {
144+
return err
145+
}
146+
return w.openLocked()
147+
}
148+
149+
// cleanupRotatedLogs 清理超出保留策略的轮转日志。
150+
func cleanupRotatedLogs(basePath string, options logRotationOptions) error {
151+
files, err := listRotatedLogFiles(basePath)
152+
if err != nil {
153+
return err
154+
}
155+
156+
now := time.Now()
157+
for _, file := range files {
158+
if options.maxAge > 0 && now.Sub(file.modTime) > options.maxAge {
159+
if err := os.Remove(file.path); err != nil && !os.IsNotExist(err) {
160+
return fmt.Errorf("删除过期日志失败: %w", err)
161+
}
162+
}
163+
}
164+
165+
files, err = listRotatedLogFiles(basePath)
166+
if err != nil {
167+
return err
168+
}
169+
if options.maxBackups <= 0 || len(files) <= options.maxBackups {
170+
return nil
171+
}
172+
173+
sort.Slice(files, func(i, j int) bool {
174+
return files[i].modTime.Before(files[j].modTime)
175+
})
176+
for _, file := range files[:len(files)-options.maxBackups] {
177+
if err := os.Remove(file.path); err != nil && !os.IsNotExist(err) {
178+
return fmt.Errorf("删除多余日志失败: %w", err)
179+
}
180+
}
181+
return nil
182+
}
183+
184+
// listRotatedLogFiles 返回指定日志文件的轮转文件列表。
185+
func listRotatedLogFiles(basePath string) ([]rotatedLogFile, error) {
186+
dir := filepath.Dir(basePath)
187+
prefix := filepath.Base(basePath) + "."
188+
entries, err := os.ReadDir(dir)
189+
if err != nil {
190+
if os.IsNotExist(err) {
191+
return nil, nil
192+
}
193+
return nil, fmt.Errorf("读取日志目录失败: %w", err)
194+
}
195+
196+
files := make([]rotatedLogFile, 0)
197+
for _, entry := range entries {
198+
if entry.IsDir() || !strings.HasPrefix(entry.Name(), prefix) {
199+
continue
200+
}
201+
info, err := entry.Info()
202+
if err != nil {
203+
return nil, fmt.Errorf("读取轮转日志状态失败: %w", err)
204+
}
205+
files = append(files, rotatedLogFile{
206+
path: filepath.Join(dir, entry.Name()),
207+
modTime: info.ModTime(),
208+
})
209+
}
210+
return files, nil
211+
}

config.example.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ update:
2727
# HTTP 代理地址(可选)
2828
proxy: ""
2929

30+
log:
31+
# 单个日志文件最大体积(MB),0 表示使用默认值 20
32+
maxSizeMB: 20
33+
# 最多保留的轮转日志文件数量,0 表示使用默认值 5
34+
maxBackups: 5
35+
# 轮转日志最长保留天数,0 表示使用默认值 30
36+
maxAgeDays: 30
37+
3038
# 云服务配置(可选)
3139
# provider:
3240
# - name: "aliyun"

internal/config/config.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type Configuration struct {
3030
Server *ServerConfig `yaml:"server"` // Server 服务端连接和本地 HTTP-01 配置
3131
SSL *DeployConfig `yaml:"ssl"` // SSL 本地部署目标配置
3232
Update *UpdateConfig `yaml:"update"` // Update 自更新配置
33+
Log *LogConfig `yaml:"log"` // Log 日志轮转配置
3334
Provider []*Provider `yaml:"provider"` // Provider 云服务提供商配置
3435
}
3536

@@ -66,6 +67,13 @@ type (
6667
Proxy string `yaml:"proxy"`
6768
}
6869

70+
// LogConfig 日志轮转配置
71+
LogConfig struct {
72+
MaxSizeMB int `yaml:"maxSizeMB"` // MaxSizeMB 单个日志文件最大体积,单位 MB
73+
MaxBackups int `yaml:"maxBackups"` // MaxBackups 最多保留的轮转文件数量
74+
MaxAgeDays int `yaml:"maxAgeDays"` // MaxAgeDays 轮转文件最长保留天数
75+
}
76+
6977
// ProviderAuth 云服务提供商认证字段集合
7078
ProviderAuth struct {
7179
// 阿里云认证字段
@@ -166,7 +174,41 @@ func validateConfig() error {
166174
if err := validateProviders(); err != nil {
167175
return err
168176
}
177+
applyLogDefaults()
178+
if err := validateLogConfig(); err != nil {
179+
return err
180+
}
181+
182+
return nil
183+
}
184+
185+
// applyLogDefaults 设置日志轮转默认值。
186+
func applyLogDefaults() {
187+
if Config.Log == nil {
188+
Config.Log = &LogConfig{}
189+
}
190+
if Config.Log.MaxSizeMB == 0 {
191+
Config.Log.MaxSizeMB = 20
192+
}
193+
if Config.Log.MaxBackups == 0 {
194+
Config.Log.MaxBackups = 5
195+
}
196+
if Config.Log.MaxAgeDays == 0 {
197+
Config.Log.MaxAgeDays = 30
198+
}
199+
}
169200

201+
// validateLogConfig 验证日志轮转配置。
202+
func validateLogConfig() error {
203+
if Config.Log.MaxSizeMB < 0 {
204+
return errors.New("log.maxSizeMB 不能小于 0")
205+
}
206+
if Config.Log.MaxBackups < 0 {
207+
return errors.New("log.maxBackups 不能小于 0")
208+
}
209+
if Config.Log.MaxAgeDays < 0 {
210+
return errors.New("log.maxAgeDays 不能小于 0")
211+
}
170212
return nil
171213
}
172214

0 commit comments

Comments
 (0)