|
| 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 | +} |
0 commit comments