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  }