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 15package logs 16 17import ( 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. 34type 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. 75func 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// } 103func (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. 121func (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 133func (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 139func (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. 147func (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 188func (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 206func (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 233func (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 247func (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 262func (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) 291func (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 346RESTART_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 360func (w *fileLogWriter) deleteOldLog() { 361 dir := filepath.Dir(w.Filename) 362 absolutePath, err := filepath.EvalSymlinks(w.Filename) 363 if err == nil { 364 dir = filepath.Dir(absolutePath) 365 } 366 filepath.Walk(dir, func(path string, info os.FileInfo, err error) (returnErr error) { 367 defer func() { 368 if r := recover(); r != nil { 369 fmt.Fprintf(os.Stderr, "Unable to delete old log '%s', error: %v\n", path, r) 370 } 371 }() 372 373 if info == nil { 374 return 375 } 376 if w.Hourly { 377 if !info.IsDir() && info.ModTime().Add(1*time.Hour*time.Duration(w.MaxHours)).Before(time.Now()) { 378 if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) && 379 strings.HasSuffix(filepath.Base(path), w.suffix) { 380 os.Remove(path) 381 } 382 } 383 } else if w.Daily { 384 if !info.IsDir() && info.ModTime().Add(24*time.Hour*time.Duration(w.MaxDays)).Before(time.Now()) { 385 if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) && 386 strings.HasSuffix(filepath.Base(path), w.suffix) { 387 os.Remove(path) 388 } 389 } 390 } 391 return 392 }) 393} 394 395// Destroy close the file description, close file writer. 396func (w *fileLogWriter) Destroy() { 397 w.fileWriter.Close() 398} 399 400// Flush flush file logger. 401// there are no buffering messages in file logger in memory. 402// flush file means sync file from disk. 403func (w *fileLogWriter) Flush() { 404 w.fileWriter.Sync() 405} 406 407func init() { 408 Register(AdapterFile, newFileWriter) 409} 410