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