code.gitea.io/gitea@v1.19.3/modules/log/file.go (about)

     1  // Copyright 2014 The Gogs Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package log
     5  
     6  import (
     7  	"bufio"
     8  	"compress/gzip"
     9  	"errors"
    10  	"fmt"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  	"sync"
    15  	"time"
    16  
    17  	"code.gitea.io/gitea/modules/json"
    18  	"code.gitea.io/gitea/modules/util"
    19  )
    20  
    21  // FileLogger implements LoggerProvider.
    22  // It writes messages by lines limit, file size limit, or time frequency.
    23  type FileLogger struct {
    24  	WriterLogger
    25  	mw *MuxWriter
    26  	// The opened file
    27  	Filename string `json:"filename"`
    28  
    29  	// Rotate at size
    30  	Maxsize        int `json:"maxsize"`
    31  	maxsizeCursize int
    32  
    33  	// Rotate daily
    34  	Daily         bool  `json:"daily"`
    35  	Maxdays       int64 `json:"maxdays"`
    36  	dailyOpenDate int
    37  
    38  	Rotate bool `json:"rotate"`
    39  
    40  	Compress         bool `json:"compress"`
    41  	CompressionLevel int  `json:"compressionLevel"`
    42  
    43  	startLock sync.Mutex // Only one log can write to the file
    44  }
    45  
    46  // MuxWriter an *os.File writer with locker.
    47  type MuxWriter struct {
    48  	mu    sync.Mutex
    49  	fd    *os.File
    50  	owner *FileLogger
    51  }
    52  
    53  // Write writes to os.File.
    54  func (mw *MuxWriter) Write(b []byte) (int, error) {
    55  	mw.mu.Lock()
    56  	defer mw.mu.Unlock()
    57  	mw.owner.docheck(len(b))
    58  	return mw.fd.Write(b)
    59  }
    60  
    61  // Close the internal writer
    62  func (mw *MuxWriter) Close() error {
    63  	return mw.fd.Close()
    64  }
    65  
    66  // SetFd sets os.File in writer.
    67  func (mw *MuxWriter) SetFd(fd *os.File) {
    68  	if mw.fd != nil {
    69  		mw.fd.Close()
    70  	}
    71  	mw.fd = fd
    72  }
    73  
    74  // NewFileLogger create a FileLogger returning as LoggerProvider.
    75  func NewFileLogger() LoggerProvider {
    76  	log := &FileLogger{
    77  		Filename:         "",
    78  		Maxsize:          1 << 28, // 256 MB
    79  		Daily:            true,
    80  		Maxdays:          7,
    81  		Rotate:           true,
    82  		Compress:         true,
    83  		CompressionLevel: gzip.DefaultCompression,
    84  	}
    85  	log.Level = TRACE
    86  	// use MuxWriter instead direct use os.File for lock write when rotate
    87  	log.mw = new(MuxWriter)
    88  	log.mw.owner = log
    89  
    90  	return log
    91  }
    92  
    93  // Init file logger with json config.
    94  // config like:
    95  //
    96  //	{
    97  //	"filename":"log/gogs.log",
    98  //	"maxsize":1<<30,
    99  //	"daily":true,
   100  //	"maxdays":15,
   101  //	"rotate":true
   102  //	}
   103  func (log *FileLogger) Init(config string) error {
   104  	if err := json.Unmarshal([]byte(config), log); err != nil {
   105  		return fmt.Errorf("Unable to parse JSON: %w", err)
   106  	}
   107  	if len(log.Filename) == 0 {
   108  		return errors.New("config must have filename")
   109  	}
   110  	// set MuxWriter as Logger's io.Writer
   111  	log.NewWriterLogger(log.mw)
   112  	return log.StartLogger()
   113  }
   114  
   115  // StartLogger start file logger. create log file and set to locker-inside file writer.
   116  func (log *FileLogger) StartLogger() error {
   117  	fd, err := log.createLogFile()
   118  	if err != nil {
   119  		return err
   120  	}
   121  	log.mw.SetFd(fd)
   122  	return log.initFd()
   123  }
   124  
   125  func (log *FileLogger) docheck(size int) {
   126  	log.startLock.Lock()
   127  	defer log.startLock.Unlock()
   128  	if log.Rotate && ((log.Maxsize > 0 && log.maxsizeCursize >= log.Maxsize) ||
   129  		(log.Daily && time.Now().Day() != log.dailyOpenDate)) {
   130  		if err := log.DoRotate(); err != nil {
   131  			fmt.Fprintf(os.Stderr, "FileLogger(%q): %s\n", log.Filename, err)
   132  			return
   133  		}
   134  	}
   135  	log.maxsizeCursize += size
   136  }
   137  
   138  func (log *FileLogger) createLogFile() (*os.File, error) {
   139  	// Open the log file
   140  	return os.OpenFile(log.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o660)
   141  }
   142  
   143  func (log *FileLogger) initFd() error {
   144  	fd := log.mw.fd
   145  	finfo, err := fd.Stat()
   146  	if err != nil {
   147  		return fmt.Errorf("get stat: %w", err)
   148  	}
   149  	log.maxsizeCursize = int(finfo.Size())
   150  	log.dailyOpenDate = time.Now().Day()
   151  	return nil
   152  }
   153  
   154  // DoRotate means it need to write file in new file.
   155  // new file name like xx.log.2013-01-01.2
   156  func (log *FileLogger) DoRotate() error {
   157  	_, err := os.Lstat(log.Filename)
   158  	if err == nil { // file exists
   159  		// Find the next available number
   160  		num := 1
   161  		fname := ""
   162  		for ; err == nil && num <= 999; num++ {
   163  			fname = log.Filename + fmt.Sprintf(".%s.%03d", time.Now().Format("2006-01-02"), num)
   164  			_, err = os.Lstat(fname)
   165  			if log.Compress && err != nil {
   166  				_, err = os.Lstat(fname + ".gz")
   167  			}
   168  		}
   169  		// return error if the last file checked still existed
   170  		if err == nil {
   171  			return fmt.Errorf("rotate: cannot find free log number to rename %s", log.Filename)
   172  		}
   173  
   174  		fd := log.mw.fd
   175  		fd.Close()
   176  
   177  		// close fd before rename
   178  		// Rename the file to its newfound home
   179  		if err = util.Rename(log.Filename, fname); err != nil {
   180  			return fmt.Errorf("Rotate: %w", err)
   181  		}
   182  
   183  		if log.Compress {
   184  			go compressOldLogFile(fname, log.CompressionLevel)
   185  		}
   186  
   187  		// re-start logger
   188  		if err = log.StartLogger(); err != nil {
   189  			return fmt.Errorf("Rotate StartLogger: %w", err)
   190  		}
   191  
   192  		go log.deleteOldLog()
   193  	}
   194  
   195  	return nil
   196  }
   197  
   198  func compressOldLogFile(fname string, compressionLevel int) error {
   199  	reader, err := os.Open(fname)
   200  	if err != nil {
   201  		return err
   202  	}
   203  	defer reader.Close()
   204  	buffer := bufio.NewReader(reader)
   205  	fw, err := os.OpenFile(fname+".gz", os.O_WRONLY|os.O_CREATE, 0o660)
   206  	if err != nil {
   207  		return err
   208  	}
   209  	defer fw.Close()
   210  	zw, err := gzip.NewWriterLevel(fw, compressionLevel)
   211  	if err != nil {
   212  		return err
   213  	}
   214  	defer zw.Close()
   215  	_, err = buffer.WriteTo(zw)
   216  	if err != nil {
   217  		zw.Close()
   218  		fw.Close()
   219  		util.Remove(fname + ".gz")
   220  		return err
   221  	}
   222  	reader.Close()
   223  	return util.Remove(fname)
   224  }
   225  
   226  func (log *FileLogger) deleteOldLog() {
   227  	dir := filepath.Dir(log.Filename)
   228  	_ = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) (returnErr error) {
   229  		defer func() {
   230  			if r := recover(); r != nil {
   231  				returnErr = fmt.Errorf("Unable to delete old log '%s', error: %+v", path, r)
   232  			}
   233  		}()
   234  
   235  		if err != nil {
   236  			return err
   237  		}
   238  		if d.IsDir() {
   239  			return nil
   240  		}
   241  		info, err := d.Info()
   242  		if err != nil {
   243  			return err
   244  		}
   245  		if info.ModTime().Unix() < (time.Now().Unix() - 60*60*24*log.Maxdays) {
   246  			if strings.HasPrefix(filepath.Base(path), filepath.Base(log.Filename)) {
   247  				if err := util.Remove(path); err != nil {
   248  					returnErr = fmt.Errorf("Failed to remove %s: %w", path, err)
   249  				}
   250  			}
   251  		}
   252  		return returnErr
   253  	})
   254  }
   255  
   256  // Content returns the content accumulated in the content provider
   257  func (log *FileLogger) Content() (string, error) {
   258  	b, err := os.ReadFile(log.Filename)
   259  	if err != nil {
   260  		return "", err
   261  	}
   262  	return string(b), nil
   263  }
   264  
   265  // Flush flush file logger.
   266  // there are no buffering messages in file logger in memory.
   267  // flush file means sync file from disk.
   268  func (log *FileLogger) Flush() {
   269  	_ = log.mw.fd.Sync()
   270  }
   271  
   272  // ReleaseReopen releases and reopens log files
   273  func (log *FileLogger) ReleaseReopen() error {
   274  	closingErr := log.mw.fd.Close()
   275  	startingErr := log.StartLogger()
   276  	if startingErr != nil {
   277  		if closingErr != nil {
   278  			return fmt.Errorf("Error during closing: %v Error during starting: %v", closingErr, startingErr)
   279  		}
   280  		return startingErr
   281  	}
   282  	return closingErr
   283  }
   284  
   285  // GetName returns the default name for this implementation
   286  func (log *FileLogger) GetName() string {
   287  	return "file"
   288  }
   289  
   290  func init() {
   291  	Register("file", NewFileLogger)
   292  }