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  }