github.com/shuguocloud/go-zero@v1.3.0/core/logx/rotatelogger.go (about) 1 package logx 2 3 import ( 4 "compress/gzip" 5 "errors" 6 "fmt" 7 "io" 8 "log" 9 "os" 10 "path" 11 "path/filepath" 12 "strings" 13 "sync" 14 "time" 15 16 "github.com/shuguocloud/go-zero/core/fs" 17 "github.com/shuguocloud/go-zero/core/lang" 18 "github.com/shuguocloud/go-zero/core/timex" 19 ) 20 21 const ( 22 dateFormat = "2006-01-02" 23 hoursPerDay = 24 24 bufferSize = 100 25 defaultDirMode = 0o755 26 defaultFileMode = 0o600 27 ) 28 29 // ErrLogFileClosed is an error that indicates the log file is already closed. 30 var ErrLogFileClosed = errors.New("error: log file closed") 31 32 type ( 33 // A RotateRule interface is used to define the log rotating rules. 34 RotateRule interface { 35 BackupFileName() string 36 MarkRotated() 37 OutdatedFiles() []string 38 ShallRotate() bool 39 } 40 41 // A RotateLogger is a Logger that can rotate log files with given rules. 42 RotateLogger struct { 43 filename string 44 backup string 45 fp *os.File 46 channel chan []byte 47 done chan lang.PlaceholderType 48 rule RotateRule 49 compress bool 50 // can't use threading.RoutineGroup because of cycle import 51 waitGroup sync.WaitGroup 52 closeOnce sync.Once 53 } 54 55 // A DailyRotateRule is a rule to daily rotate the log files. 56 DailyRotateRule struct { 57 rotatedTime string 58 filename string 59 delimiter string 60 days int 61 gzip bool 62 } 63 ) 64 65 // DefaultRotateRule is a default log rotating rule, currently DailyRotateRule. 66 func DefaultRotateRule(filename, delimiter string, days int, gzip bool) RotateRule { 67 return &DailyRotateRule{ 68 rotatedTime: getNowDate(), 69 filename: filename, 70 delimiter: delimiter, 71 days: days, 72 gzip: gzip, 73 } 74 } 75 76 // BackupFileName returns the backup filename on rotating. 77 func (r *DailyRotateRule) BackupFileName() string { 78 return fmt.Sprintf("%s%s%s", r.filename, r.delimiter, getNowDate()) 79 } 80 81 // MarkRotated marks the rotated time of r to be the current time. 82 func (r *DailyRotateRule) MarkRotated() { 83 r.rotatedTime = getNowDate() 84 } 85 86 // OutdatedFiles returns the files that exceeded the keeping days. 87 func (r *DailyRotateRule) OutdatedFiles() []string { 88 if r.days <= 0 { 89 return nil 90 } 91 92 var pattern string 93 if r.gzip { 94 pattern = fmt.Sprintf("%s%s*.gz", r.filename, r.delimiter) 95 } else { 96 pattern = fmt.Sprintf("%s%s*", r.filename, r.delimiter) 97 } 98 99 files, err := filepath.Glob(pattern) 100 if err != nil { 101 Errorf("failed to delete outdated log files, error: %s", err) 102 return nil 103 } 104 105 var buf strings.Builder 106 boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay*r.days)).Format(dateFormat) 107 fmt.Fprintf(&buf, "%s%s%s", r.filename, r.delimiter, boundary) 108 if r.gzip { 109 buf.WriteString(".gz") 110 } 111 boundaryFile := buf.String() 112 113 var outdates []string 114 for _, file := range files { 115 if file < boundaryFile { 116 outdates = append(outdates, file) 117 } 118 } 119 120 return outdates 121 } 122 123 // ShallRotate checks if the file should be rotated. 124 func (r *DailyRotateRule) ShallRotate() bool { 125 return len(r.rotatedTime) > 0 && getNowDate() != r.rotatedTime 126 } 127 128 // NewLogger returns a RotateLogger with given filename and rule, etc. 129 func NewLogger(filename string, rule RotateRule, compress bool) (*RotateLogger, error) { 130 l := &RotateLogger{ 131 filename: filename, 132 channel: make(chan []byte, bufferSize), 133 done: make(chan lang.PlaceholderType), 134 rule: rule, 135 compress: compress, 136 } 137 if err := l.init(); err != nil { 138 return nil, err 139 } 140 141 l.startWorker() 142 return l, nil 143 } 144 145 // Close closes l. 146 func (l *RotateLogger) Close() error { 147 var err error 148 149 l.closeOnce.Do(func() { 150 close(l.done) 151 l.waitGroup.Wait() 152 153 if err = l.fp.Sync(); err != nil { 154 return 155 } 156 157 err = l.fp.Close() 158 }) 159 160 return err 161 } 162 163 func (l *RotateLogger) Write(data []byte) (int, error) { 164 select { 165 case l.channel <- data: 166 return len(data), nil 167 case <-l.done: 168 log.Println(string(data)) 169 return 0, ErrLogFileClosed 170 } 171 } 172 173 func (l *RotateLogger) getBackupFilename() string { 174 if len(l.backup) == 0 { 175 return l.rule.BackupFileName() 176 } 177 178 return l.backup 179 } 180 181 func (l *RotateLogger) init() error { 182 l.backup = l.rule.BackupFileName() 183 184 if _, err := os.Stat(l.filename); err != nil { 185 basePath := path.Dir(l.filename) 186 if _, err = os.Stat(basePath); err != nil { 187 if err = os.MkdirAll(basePath, defaultDirMode); err != nil { 188 return err 189 } 190 } 191 192 if l.fp, err = os.Create(l.filename); err != nil { 193 return err 194 } 195 } else if l.fp, err = os.OpenFile(l.filename, os.O_APPEND|os.O_WRONLY, defaultFileMode); err != nil { 196 return err 197 } 198 199 fs.CloseOnExec(l.fp) 200 201 return nil 202 } 203 204 func (l *RotateLogger) maybeCompressFile(file string) { 205 if !l.compress { 206 return 207 } 208 209 defer func() { 210 if r := recover(); r != nil { 211 ErrorStack(r) 212 } 213 }() 214 compressLogFile(file) 215 } 216 217 func (l *RotateLogger) maybeDeleteOutdatedFiles() { 218 files := l.rule.OutdatedFiles() 219 for _, file := range files { 220 if err := os.Remove(file); err != nil { 221 Errorf("failed to remove outdated file: %s", file) 222 } 223 } 224 } 225 226 func (l *RotateLogger) postRotate(file string) { 227 go func() { 228 // we cannot use threading.GoSafe here, because of import cycle. 229 l.maybeCompressFile(file) 230 l.maybeDeleteOutdatedFiles() 231 }() 232 } 233 234 func (l *RotateLogger) rotate() error { 235 if l.fp != nil { 236 err := l.fp.Close() 237 l.fp = nil 238 if err != nil { 239 return err 240 } 241 } 242 243 _, err := os.Stat(l.filename) 244 if err == nil && len(l.backup) > 0 { 245 backupFilename := l.getBackupFilename() 246 err = os.Rename(l.filename, backupFilename) 247 if err != nil { 248 return err 249 } 250 251 l.postRotate(backupFilename) 252 } 253 254 l.backup = l.rule.BackupFileName() 255 if l.fp, err = os.Create(l.filename); err == nil { 256 fs.CloseOnExec(l.fp) 257 } 258 259 return err 260 } 261 262 func (l *RotateLogger) startWorker() { 263 l.waitGroup.Add(1) 264 265 go func() { 266 defer l.waitGroup.Done() 267 268 for { 269 select { 270 case event := <-l.channel: 271 l.write(event) 272 case <-l.done: 273 return 274 } 275 } 276 }() 277 } 278 279 func (l *RotateLogger) write(v []byte) { 280 if l.rule.ShallRotate() { 281 if err := l.rotate(); err != nil { 282 log.Println(err) 283 } else { 284 l.rule.MarkRotated() 285 } 286 } 287 if l.fp != nil { 288 l.fp.Write(v) 289 } 290 } 291 292 func compressLogFile(file string) { 293 start := timex.Now() 294 Infof("compressing log file: %s", file) 295 if err := gzipFile(file); err != nil { 296 Errorf("compress error: %s", err) 297 } else { 298 Infof("compressed log file: %s, took %s", file, timex.Since(start)) 299 } 300 } 301 302 func getNowDate() string { 303 return time.Now().Format(dateFormat) 304 } 305 306 func gzipFile(file string) error { 307 in, err := os.Open(file) 308 if err != nil { 309 return err 310 } 311 defer in.Close() 312 313 out, err := os.Create(fmt.Sprintf("%s.gz", file)) 314 if err != nil { 315 return err 316 } 317 defer out.Close() 318 319 w := gzip.NewWriter(out) 320 if _, err = io.Copy(w, in); err != nil { 321 return err 322 } else if err = w.Close(); err != nil { 323 return err 324 } 325 326 return os.Remove(file) 327 }