-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathwriter.go
390 lines (331 loc) · 8.72 KB
/
writer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
package slog
import (
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
)
const (
backupTimeFormat = "2006-01-02T15-04-05"
compressSuffix = "gz"
defaultMaxSize = 100 // 默认单个文件最大 100MB
defaultMaxAge = 30 // 默认保留 30 天
defaultMaxBackups = 30 // 默认保留 30 个备份文件
)
var _ io.WriteCloser = (*writer)(nil)
type writer struct {
_Filename string // 文件名称
_MaxSize int // MB为单位
_MaxAge int // 天数
_MaxBackups int // 最大备份数
_LocalTime bool
_Compress bool
size int64
file *os.File
mu sync.Mutex
}
type logInfo struct {
timestamp time.Time
name string
}
// NewWriter 创建一个新的日志写入器,支持指定一个或多个文件路径,多个路径时使用第一个有效路径
// filename: 日志文件路径
// 默认配置:
// - 单个文件最大 100MB
// - 保留最近 30 天的日志
// - 最多保留 30 个备份文件
// - 使用本地时间
// - 不压缩旧文件
func NewWriter(filename ...string) *writer {
var logFile string
if len(filename) > 0 {
logFile = filename[0] // 取第一个文件名
}
return &writer{
_Filename: logFile,
_MaxSize: defaultMaxSize, // 100MB
_MaxBackups: defaultMaxBackups, // 保留30个备份
_MaxAge: defaultMaxAge, // 保留30天
_LocalTime: true, // 使用本地时间
_Compress: true, // 默认压缩
}
}
// SetMaxSize 设置日志文件的最大大小(MB)
// size: 文件大小上限,单位为MB
// 当日志文件达到此大小时会触发轮转
func (w *writer) SetMaxSize(size int) *writer {
w._MaxSize = size
return w
}
// SetMaxAge 设置日志文件的最大保留天数
// days: 文件保留天数
// 超过指定天数的日志文件将被删除,设置为0表示不删除
func (w *writer) SetMaxAge(days int) *writer {
w._MaxAge = days
return w
}
// SetMaxBackups 设置要保留的最大日志文件数
// count: 要保留的文件数量
// 超过数量限制的旧文件将被删除,设置为0表示不限制数量
func (w *writer) SetMaxBackups(count int) *writer {
w._MaxBackups = count
return w
}
// SetLocalTime 设置是否使用本地时间
// local: true表示使用本地时间,false表示使用UTC时间
// 影响日志文件的备份名称中的时间戳
func (w *writer) SetLocalTime(local bool) *writer {
w._LocalTime = local
return w
}
// SetCompress 设置是否压缩旧的日志文件
// compress: true表示启用压缩,false表示不压缩
// 启用后,旧的日志文件将被压缩为.gz格式
func (w *writer) SetCompress(compress bool) *writer {
w._Compress = compress
return w
}
func (w *writer) Write(p []byte) (n int, err error) {
if err := w.validate(); err != nil {
return 0, err
}
w.mu.Lock()
defer w.mu.Unlock()
// 清理颜色控制码
cleanBytes := stripAnsiCodes(p)
writeLen := int64(len(cleanBytes))
if writeLen > w.maxBytes() {
return 0, fmt.Errorf("write length %d exceeds maximum file size %d", writeLen, w.maxBytes())
}
if w.file == nil {
if err = w.openFile(); err != nil {
return 0, err
}
}
if w.size+writeLen > w.maxBytes() {
if err := w.rotate(); err != nil {
return 0, err
}
}
n, err = w.file.Write(cleanBytes)
w.size += int64(n)
return len(p), err
}
func (w *writer) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
return w.close()
}
func (w *writer) close() error {
if w.file == nil {
return nil
}
err := w.file.Close()
w.file = nil
return err
}
func (w *writer) rotate() error {
if err := w.close(); err != nil {
return fmt.Errorf("failed to close current log file: %v", err)
}
currentName := w.filename()
backupName := w.backupName()
if err := os.Rename(currentName, backupName); err != nil {
return fmt.Errorf("failed to backup log file: %v", err)
}
if err := w.openFile(); err != nil {
return fmt.Errorf("failed to create new log file: %v", err)
}
go func() {
if err := w.processOldFiles(); err != nil {
// 这里可以考虑添加错误日志记录
_ = err
}
}()
return nil
}
func (w *writer) openFile() error {
filename := w.filename()
if err := os.MkdirAll(filepath.Dir(filename), 0o755); err != nil {
return err
}
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return err
}
info, err := f.Stat()
if err != nil {
f.Close()
return err
}
w.file = f
w.size = info.Size()
return nil
}
func (w *writer) filename() string {
if w._Filename != "" {
if !filepath.IsAbs(w._Filename) {
dir, _ := os.Getwd()
fullPath := filepath.Join(dir, w._Filename)
if err := os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
return filepath.Join(os.TempDir(), filepath.Base(w._Filename))
}
return fullPath
}
if err := os.MkdirAll(filepath.Dir(w._Filename), 0o755); err != nil {
return filepath.Join(os.TempDir(), filepath.Base(w._Filename))
}
return w._Filename
}
name := filepath.Base(os.Args[0]) + "-slog.log"
return filepath.Join(os.TempDir(), name)
}
func (w *writer) backupName() string {
dir := filepath.Dir(w.filename())
filename := filepath.Base(w.filename())
ext := filepath.Ext(filename)
prefix := filename[:len(filename)-len(ext)]
t := time.Now()
if !w._LocalTime {
t = t.UTC()
}
// 保持原有扩展名,在文件名和扩展名之间插入时间戳
backupName := fmt.Sprintf("%s-%s%s",
prefix, // 原文件名(不含扩展名)
t.Format(backupTimeFormat), // 时间戳
ext, // 原扩展名
)
return filepath.Join(dir, backupName)
}
func (w *writer) processOldFiles() error {
files, err := w.oldLogFiles()
if err != nil {
return fmt.Errorf("failed to get old log files: %v", err)
}
sort.Slice(files, func(i, j int) bool {
return files[i].timestamp.After(files[j].timestamp)
})
if w._MaxBackups > 0 && len(files) > w._MaxBackups {
for _, f := range files[w._MaxBackups:] {
if err := os.Remove(filepath.Join(filepath.Dir(w.filename()), f.name)); err != nil {
return fmt.Errorf("failed to remove excess backup file: %v", err)
}
}
files = files[:w._MaxBackups]
}
if w._MaxAge > 0 {
cutoff := time.Now().Add(-time.Duration(w._MaxAge) * 24 * time.Hour)
for _, f := range files {
if f.timestamp.Before(cutoff) {
os.Remove(filepath.Join(filepath.Dir(w.filename()), f.name))
}
}
}
if w._Compress {
for _, f := range files {
if !strings.HasSuffix(f.name, compressSuffix) {
fname := filepath.Join(filepath.Dir(w.filename()), f.name)
if err := w.compressFile(fname); err != nil {
return err
}
}
}
}
return nil
}
func (w *writer) compressFile(src string) error {
const maxRetries = 3
var err error
for i := 0; i < maxRetries; i++ {
err = w.tryCompressfile(src)
if err == nil {
return nil
}
time.Sleep(time.Millisecond * 100 * time.Duration(i+1))
}
return fmt.Errorf("failed to compress file after %d retries: %v", maxRetries, err)
}
func (w *writer) tryCompressfile(src string) error {
dst := src + compressSuffix
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
gzf, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer gzf.Close()
gz := gzip.NewWriter(gzf)
defer gz.Close()
if _, err := io.Copy(gz, f); err != nil {
os.Remove(dst)
return err
}
return os.Remove(src)
}
func (w *writer) oldLogFiles() ([]logInfo, error) {
files, err := os.ReadDir(filepath.Dir(w.filename()))
if err != nil {
return nil, err
}
var logFiles []logInfo
prefix := filepath.Base(w.filename())
ext := filepath.Ext(prefix)
prefix = prefix[:len(prefix)-len(ext)] + "-"
for _, f := range files {
if f.IsDir() {
continue
}
name := f.Name()
if strings.HasPrefix(name, prefix) {
if t, err := time.Parse(backupTimeFormat, name[len(prefix):len(name)-len(ext)]); err == nil {
logFiles = append(logFiles, logInfo{t, name})
}
}
}
return logFiles, nil
}
func (w *writer) maxBytes() int64 {
if w._MaxSize == 0 {
return int64(defaultMaxSize * 1024 * 1024)
}
return int64(w._MaxSize) * 1024 * 1024
}
func (w *writer) validate() error {
if w._MaxSize < 0 {
return fmt.Errorf("MaxSize cannot be negative")
}
if w._MaxAge < 0 {
return fmt.Errorf("MaxAge cannot be negative")
}
if w._MaxBackups < 0 {
return fmt.Errorf("MaxBackups cannot be negative")
}
return nil
}
// stripAnsiCodes 移除ANSI颜色控制码
func stripAnsiCodes(input []byte) []byte {
if len(input) == 0 {
return input
}
output := make([]byte, 0, len(input))
for i := 0; i < len(input); i++ {
if input[i] == '\x1b' && i+1 < len(input) && input[i+1] == '[' {
// 跳过直到找到 m
i += 2
for i < len(input) && input[i] != 'm' {
i++
}
} else {
output = append(output, input[i])
}
}
return output
}