github.com/mdaxf/iac@v0.0.0-20240519030858-58a061660378/framework/logs/log_file.go (about) 1 // the package is exported from github.com/beego/beego/v2/core/logs 2 3 // Copyright 2023. All Rights Reserved. 4 // 5 // Licensed under the Apache License, Version 2.0 (the "License"); 6 // you may not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, software 12 // distributed under the License is distributed on an "AS IS" BASIS, 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 // See the License for the specific language governing permissions and 15 // limitations under the License. 16 17 package logs 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "io" 25 "os" 26 "path" 27 "path/filepath" 28 "strconv" 29 "strings" 30 "sync" 31 "time" 32 ) 33 34 // fileLogWriter implements LoggerInterface. 35 // Writes messages by lines limit, file size limit, or time frequency. 36 type fileLogWriter struct { 37 sync.RWMutex // write log order by order and atomic incr maxLinesCurLines and maxSizeCurSize 38 39 Rotate bool `json:"rotate"` 40 Daily bool `json:"daily"` 41 Hourly bool `json:"hourly"` 42 43 // The opened file 44 Filename string `json:"filename"` 45 fileWriter *os.File 46 47 // Rotate at line 48 MaxLines int `json:"maxlines"` 49 maxLinesCurLines int 50 51 MaxFiles int `json:"maxfiles"` 52 MaxFilesCurFiles int 53 54 // Rotate at size 55 MaxSize int `json:"maxsize"` 56 maxSizeCurSize int 57 58 // Rotate daily 59 MaxDays int64 `json:"maxdays"` 60 dailyOpenDate int 61 dailyOpenTime time.Time 62 63 // Rotate hourly 64 MaxHours int64 `json:"maxhours"` 65 hourlyOpenDate int 66 hourlyOpenTime time.Time 67 68 Level int `json:"level"` 69 // Permissions for log file 70 Perm string `json:"perm"` 71 // Permissions for directory if it is specified in FileName 72 DirPerm string `json:"dirperm"` 73 74 RotatePerm string `json:"rotateperm"` 75 76 fileNameOnly, suffix string // like "project.log", project is fileNameOnly and .log is suffix 77 78 logFormatter LogFormatter 79 Formatter string `json:"formatter"` 80 } 81 82 // newFileWriter creates a FileLogWriter returning as LoggerInterface. 83 func newFileWriter() Logger { 84 w := &fileLogWriter{ 85 Daily: true, 86 MaxDays: 7, 87 Hourly: false, 88 MaxHours: 168, 89 Rotate: true, 90 RotatePerm: "0440", 91 Level: LevelTrace, 92 Perm: "0660", 93 DirPerm: "0770", 94 MaxLines: 10000000, 95 MaxFiles: 999, 96 MaxSize: 1 << 28, 97 } 98 w.logFormatter = w 99 return w 100 } 101 102 func (*fileLogWriter) Format(lm *LogMsg) string { 103 msg := lm.OldStyleFormat() 104 hd, _, _ := formatTimeHeader(lm.When) 105 msg = fmt.Sprintf("%s %s\n", string(hd), msg) 106 return msg 107 } 108 109 func (w *fileLogWriter) SetFormatter(f LogFormatter) { 110 w.logFormatter = f 111 } 112 113 // Init file logger with json config. 114 // jsonConfig like: 115 // 116 // { 117 // "filename":"logs/beego.log", 118 // "maxLines":10000, 119 // "maxsize":1024, 120 // "daily":true, 121 // "maxDays":15, 122 // "rotate":true, 123 // "perm":"0600" 124 // } 125 func (w *fileLogWriter) Init(config string) error { 126 127 // fmt.Println("fileLogWriter.Init, %s", config) 128 129 err := json.Unmarshal([]byte(config), w) 130 // fmt.Println("fileLogWriter.Init unmarshall, %s", w, err) 131 // fmt.Println("fileLogWriter.Init unmarshall, %s", w, err) 132 if err != nil { 133 fmt.Println("fileLogWriter.Init Error, %s", err) 134 fmt.Errorf("fileLogWriter.Init: %s", err) 135 return err 136 } 137 // fmt.Println("fileLogWriter.Init unmarshall, %s", w) 138 if w.Filename == "" { 139 fmt.Println("jsonconfig must have filename") 140 return errors.New("jsonconfig must have filename") 141 } 142 w.suffix = filepath.Ext(w.Filename) 143 w.fileNameOnly = strings.TrimSuffix(w.Filename, w.suffix) 144 if w.suffix == "" { 145 w.suffix = ".log" 146 } 147 // fmt.Println("fileLogWriter.Init get filename, %s", w) 148 if len(w.Formatter) > 0 { 149 fmtr, ok := GetFormatter(w.Formatter) 150 if !ok { 151 fmt.Println("the formatter with name: %s not found", w.Formatter) 152 return fmt.Errorf("the formatter with name: %s not found", w.Formatter) 153 } 154 w.logFormatter = fmtr 155 } 156 err = w.startLogger() 157 if err != nil { 158 fmt.Println("fileLogWriter.Init startLogger error, %s", err) 159 fmt.Errorf("fileLogWriter.Init: %s", err) 160 } 161 return err 162 } 163 164 // start file logger. create log file and set to locker-inside file writer. 165 func (w *fileLogWriter) startLogger() error { 166 // fmt.Println("fileLogWriter.startLogger, %s", w.Filename) 167 file, err := w.createLogFile() 168 if err != nil { 169 return err 170 } 171 if w.fileWriter != nil { 172 w.fileWriter.Close() 173 } 174 w.fileWriter = file 175 return w.initFd() 176 } 177 178 func (w *fileLogWriter) needRotateDaily(day int) bool { 179 return (w.MaxLines > 0 && w.maxLinesCurLines >= w.MaxLines) || 180 (w.MaxSize > 0 && w.maxSizeCurSize >= w.MaxSize) || 181 (w.Daily && day != w.dailyOpenDate) 182 } 183 184 func (w *fileLogWriter) needRotateHourly(hour int) bool { 185 return (w.MaxLines > 0 && w.maxLinesCurLines >= w.MaxLines) || 186 (w.MaxSize > 0 && w.maxSizeCurSize >= w.MaxSize) || 187 (w.Hourly && hour != w.hourlyOpenDate) 188 } 189 190 // WriteMsg writes logger message into file. 191 func (w *fileLogWriter) WriteMsg(lm *LogMsg) error { 192 // fmt.Println("fileLogWriter.WriteMsg, %s", lm) 193 194 if lm.Level > w.Level { 195 return nil 196 } 197 198 _, d, h := formatTimeHeader(lm.When) 199 200 msg := w.logFormatter.Format(lm) 201 if w.Rotate { 202 w.RLock() 203 if w.needRotateHourly(h) { 204 w.RUnlock() 205 w.Lock() 206 if w.needRotateHourly(h) { 207 if err := w.doRotate(lm.When); err != nil { 208 fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err) 209 } 210 } 211 w.Unlock() 212 } else if w.needRotateDaily(d) { 213 w.RUnlock() 214 w.Lock() 215 if w.needRotateDaily(d) { 216 if err := w.doRotate(lm.When); err != nil { 217 fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err) 218 } 219 } 220 w.Unlock() 221 } else { 222 w.RUnlock() 223 } 224 } 225 226 w.Lock() 227 _, err := w.fileWriter.Write([]byte(msg)) 228 if err == nil { 229 w.maxLinesCurLines++ 230 w.maxSizeCurSize += len(msg) 231 } 232 w.Unlock() 233 return err 234 } 235 236 func (w *fileLogWriter) createLogFile() (*os.File, error) { 237 // Open the log file 238 // fmt.Println("fileLogWriter.createLogFile, %s", w.Filename) 239 perm, err := strconv.ParseInt(w.Perm, 8, 64) 240 if err != nil { 241 return nil, err 242 } 243 244 dirperm, err := strconv.ParseInt(w.DirPerm, 8, 64) 245 if err != nil { 246 return nil, err 247 } 248 249 filepath := path.Dir(w.Filename) 250 os.MkdirAll(filepath, os.FileMode(dirperm)) 251 252 fd, err := os.OpenFile(w.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(perm)) 253 if err == nil { 254 // Make sure file perm is user set perm cause of `os.OpenFile` will obey umask 255 os.Chmod(w.Filename, os.FileMode(perm)) 256 } 257 return fd, err 258 } 259 260 func (w *fileLogWriter) initFd() error { 261 fd := w.fileWriter 262 fInfo, err := fd.Stat() 263 if err != nil { 264 return fmt.Errorf("get stat err: %s", err) 265 } 266 w.maxSizeCurSize = int(fInfo.Size()) 267 w.dailyOpenTime = time.Now() 268 w.dailyOpenDate = w.dailyOpenTime.Day() 269 w.hourlyOpenTime = time.Now() 270 w.hourlyOpenDate = w.hourlyOpenTime.Hour() 271 w.maxLinesCurLines = 0 272 if w.Hourly { 273 go w.hourlyRotate(w.hourlyOpenTime) 274 } else if w.Daily { 275 go w.dailyRotate(w.dailyOpenTime) 276 } 277 if fInfo.Size() > 0 && w.MaxLines > 0 { 278 count, err := w.lines() 279 if err != nil { 280 return err 281 } 282 w.maxLinesCurLines = count 283 } 284 return nil 285 } 286 287 func (w *fileLogWriter) dailyRotate(openTime time.Time) { 288 y, m, d := openTime.Add(24 * time.Hour).Date() 289 nextDay := time.Date(y, m, d, 0, 0, 0, 0, openTime.Location()) 290 tm := time.NewTimer(time.Duration(nextDay.UnixNano() - openTime.UnixNano() + 100)) 291 <-tm.C 292 w.Lock() 293 if w.needRotateDaily(time.Now().Day()) { 294 if err := w.doRotate(time.Now()); err != nil { 295 fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err) 296 } 297 } 298 w.Unlock() 299 } 300 301 func (w *fileLogWriter) hourlyRotate(openTime time.Time) { 302 y, m, d := openTime.Add(1 * time.Hour).Date() 303 h, _, _ := openTime.Add(1 * time.Hour).Clock() 304 nextHour := time.Date(y, m, d, h, 0, 0, 0, openTime.Location()) 305 tm := time.NewTimer(time.Duration(nextHour.UnixNano() - openTime.UnixNano() + 100)) 306 <-tm.C 307 w.Lock() 308 if w.needRotateHourly(time.Now().Hour()) { 309 if err := w.doRotate(time.Now()); err != nil { 310 fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err) 311 } 312 } 313 w.Unlock() 314 } 315 316 func (w *fileLogWriter) lines() (int, error) { 317 fd, err := os.Open(w.Filename) 318 if err != nil { 319 return 0, err 320 } 321 defer fd.Close() 322 323 buf := make([]byte, 32768) // 32k 324 count := 0 325 lineSep := []byte{'\n'} 326 327 for { 328 c, err := fd.Read(buf) 329 if err != nil && err != io.EOF { 330 return count, err 331 } 332 333 count += bytes.Count(buf[:c], lineSep) 334 335 if err == io.EOF { 336 break 337 } 338 } 339 340 return count, nil 341 } 342 343 // DoRotate means it needs to write logs into a new file. 344 // new file name like xx.2013-01-01.log (daily) or xx.001.log (by line or size) 345 func (w *fileLogWriter) doRotate(logTime time.Time) error { 346 // file exists 347 // Find the next available number 348 num := w.MaxFilesCurFiles + 1 349 fName := "" 350 format := "" 351 var openTime time.Time 352 rotatePerm, err := strconv.ParseInt(w.RotatePerm, 8, 64) 353 if err != nil { 354 return err 355 } 356 357 _, err = os.Lstat(w.Filename) 358 if err != nil { 359 // even if the file is not exist or other ,we should RESTART the logger 360 goto RESTART_LOGGER 361 } 362 363 if w.Hourly { 364 format = "2006010215" 365 openTime = w.hourlyOpenTime 366 } else if w.Daily { 367 format = "2006-01-02" 368 openTime = w.dailyOpenTime 369 } 370 371 // only when one of them be setted, then the file would be splited 372 if w.MaxLines > 0 || w.MaxSize > 0 { 373 for ; err == nil && num <= w.MaxFiles; num++ { 374 fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", logTime.Format(format), num, w.suffix) 375 _, err = os.Lstat(fName) 376 } 377 } else { 378 fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", openTime.Format(format), num, w.suffix) 379 _, err = os.Lstat(fName) 380 w.MaxFilesCurFiles = num 381 } 382 383 // return error if the last file checked still existed 384 if err == nil { 385 return fmt.Errorf("Rotate: Cannot find free log number to rename %s", w.Filename) 386 } 387 388 // close fileWriter before rename 389 w.fileWriter.Close() 390 391 // Rename the file to its new found name 392 // even if occurs error,we MUST guarantee to restart new logger 393 err = os.Rename(w.Filename, fName) 394 if err != nil { 395 goto RESTART_LOGGER 396 } 397 398 err = os.Chmod(fName, os.FileMode(rotatePerm)) 399 400 RESTART_LOGGER: 401 402 startLoggerErr := w.startLogger() 403 go w.deleteOldLog() 404 405 if startLoggerErr != nil { 406 return fmt.Errorf("Rotate StartLogger: %s", startLoggerErr) 407 } 408 if err != nil { 409 return fmt.Errorf("Rotate: %s", err) 410 } 411 return nil 412 } 413 414 func (w *fileLogWriter) deleteOldLog() { 415 dir := filepath.Dir(w.Filename) 416 absolutePath, err := filepath.EvalSymlinks(w.Filename) 417 if err == nil { 418 dir = filepath.Dir(absolutePath) 419 } 420 filepath.Walk(dir, func(path string, info os.FileInfo, err error) (returnErr error) { 421 defer func() { 422 if r := recover(); r != nil { 423 fmt.Fprintf(os.Stderr, "Unable to delete old log '%s', error: %v\n", path, r) 424 } 425 }() 426 427 if info == nil { 428 return 429 } 430 if w.Hourly { 431 if !info.IsDir() && info.ModTime().Add(1*time.Hour*time.Duration(w.MaxHours)).Before(time.Now()) { 432 if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) && 433 strings.HasSuffix(filepath.Base(path), w.suffix) { 434 os.Remove(path) 435 } 436 } 437 } else if w.Daily { 438 if !info.IsDir() && info.ModTime().Add(24*time.Hour*time.Duration(w.MaxDays)).Before(time.Now()) { 439 if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) && 440 strings.HasSuffix(filepath.Base(path), w.suffix) { 441 os.Remove(path) 442 } 443 } 444 } 445 return 446 }) 447 } 448 449 // Destroy close the file description, close file writer. 450 func (w *fileLogWriter) Destroy() { 451 w.fileWriter.Close() 452 } 453 454 // Flush flushes file logger. 455 // there are no buffering messages in file logger in memory. 456 // flush file means sync file from disk. 457 func (w *fileLogWriter) Flush() { 458 w.fileWriter.Sync() 459 } 460 461 func init() { 462 Register(AdapterFile, newFileWriter) 463 }