github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/logger/file.go (about)

     1  // Copyright 2017 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  package logger
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"regexp"
    12  	"sort"
    13  	"sync"
    14  	"time"
    15  
    16  	logging "github.com/keybase/go-logging"
    17  )
    18  
    19  // LogFileConfig is the config structure for new style log files with rotation.
    20  type LogFileConfig struct {
    21  	// Path is the path of the log file to use
    22  	Path string
    23  	// MaxSize is the size of log file (in bytes) before rotation, 0 for infinite.
    24  	MaxSize int64
    25  	// MaxAge is the duration before log rotation, zero value for infinite.
    26  	MaxAge time.Duration
    27  	// MaxKeepFiles is maximum number of log files for this service, older
    28  	// files are deleted.
    29  	MaxKeepFiles int
    30  	// RedirectStdErr indicates if the current stderr redirected to the given
    31  	// Path.
    32  	SkipRedirectStdErr bool
    33  }
    34  
    35  // SetLogFileConfig sets the log file config to be used globally.
    36  func SetLogFileConfig(lfc *LogFileConfig, blc *BufferedLoggerConfig) error {
    37  	globalLock.Lock()
    38  	defer globalLock.Unlock()
    39  
    40  	first := true
    41  	var w = currentLogFileWriter
    42  	if w != nil {
    43  		first = false
    44  		w.lock.Lock()
    45  		defer w.lock.Unlock()
    46  		w.Close()
    47  		w.config = *lfc
    48  	} else {
    49  		w = NewLogFileWriter(*lfc)
    50  
    51  		// Clean up the default logger, if it is in use
    52  		select {
    53  		case stdErrLoggingShutdown <- struct{}{}:
    54  		default:
    55  		}
    56  	}
    57  
    58  	if err := w.Open(time.Now()); err != nil {
    59  		return err
    60  	}
    61  
    62  	if first {
    63  		buf, shutdown, _ := NewAutoFlushingBufferedWriter(w, blc)
    64  		w.stopFlushing = shutdown
    65  		fileBackend := logging.NewLogBackend(buf, "", 0)
    66  		logging.SetBackend(fileBackend)
    67  
    68  		stderrIsTerminal = false
    69  		currentLogFileWriter = w
    70  	}
    71  	return nil
    72  }
    73  
    74  type LogFileWriter struct {
    75  	lock         sync.Mutex
    76  	config       LogFileConfig
    77  	file         *os.File
    78  	currentSize  int64
    79  	currentStart time.Time
    80  	stopFlushing chan<- struct{}
    81  }
    82  
    83  func NewLogFileWriter(config LogFileConfig) *LogFileWriter {
    84  	return &LogFileWriter{
    85  		config: config,
    86  	}
    87  }
    88  
    89  func (lfw *LogFileWriter) Open(at time.Time) error {
    90  	var err error
    91  	_, lfw.file, err = OpenLogFile(lfw.config.Path)
    92  	if err != nil {
    93  		return err
    94  	}
    95  	lfw.currentStart = at
    96  	lfw.currentSize = 0
    97  	fi, err := lfw.file.Stat()
    98  	if err != nil {
    99  		return err
   100  	}
   101  	lfw.currentSize = fi.Size()
   102  	if !lfw.config.SkipRedirectStdErr {
   103  		_ = tryRedirectStderrTo(lfw.file)
   104  	}
   105  	return nil
   106  }
   107  
   108  func (lfw *LogFileWriter) Close() error {
   109  	if lfw == nil {
   110  		return nil
   111  	}
   112  	lfw.lock.Lock()
   113  	defer lfw.lock.Unlock()
   114  	if lfw.file == nil {
   115  		return nil
   116  	}
   117  	if lfw.stopFlushing != nil {
   118  		lfw.stopFlushing <- struct{}{}
   119  	}
   120  
   121  	return lfw.file.Close()
   122  }
   123  
   124  const zeroDuration time.Duration = 0
   125  const oldLogFileTimeRangeTimeLayout = "20060102T150405Z0700"
   126  const oldLogFileTimeRangeTimeLayoutLegacy = "20060102T150405"
   127  
   128  func (lfw *LogFileWriter) Write(bs []byte) (int, error) {
   129  	lfw.lock.Lock()
   130  	defer lfw.lock.Unlock()
   131  	n, err := lfw.file.Write(bs)
   132  	if err != nil {
   133  		return n, err
   134  	}
   135  	needRotation := false
   136  	if lfw.config.MaxSize > 0 {
   137  		lfw.currentSize += int64(n)
   138  		needRotation = needRotation || lfw.currentSize > lfw.config.MaxSize
   139  	}
   140  	if lfw.config.MaxAge != zeroDuration {
   141  		elapsed := time.Since(lfw.currentStart)
   142  		needRotation = needRotation || elapsed > lfw.config.MaxAge
   143  	}
   144  	if !needRotation {
   145  		return n, nil
   146  	}
   147  	// Close first because some systems don't like to rename otherwise.
   148  	lfw.file.Close()
   149  	lfw.file = nil
   150  	now := time.Now()
   151  	start := lfw.currentStart.Format(oldLogFileTimeRangeTimeLayout)
   152  	end := now.Format(oldLogFileTimeRangeTimeLayout)
   153  	tgt := fmt.Sprintf("%s-%s-%s", lfw.config.Path, start, end)
   154  	// Handle the error further down
   155  	err = os.Rename(lfw.config.Path, tgt)
   156  	if err != nil {
   157  		return n, err
   158  	}
   159  	// Spawn old log deletion worker if we have a max-amount of log-files.
   160  	if lfw.config.MaxKeepFiles > 0 {
   161  		go deleteOldLogFilesIfNeeded(lfw.config)
   162  	}
   163  	err = lfw.Open(now)
   164  	return n, err
   165  }
   166  
   167  func deleteOldLogFilesIfNeeded(config LogFileConfig) {
   168  	err := deleteOldLogFilesIfNeededWorker(config)
   169  	if err != nil {
   170  		log := New("logger")
   171  		log.Warning("Deletion of old log files failed: %v", err)
   172  	}
   173  }
   174  
   175  func deleteOldLogFilesIfNeededWorker(config LogFileConfig) error {
   176  	// Returns list of old log files (not the current one) sorted.
   177  	// The oldest one is first in the list.
   178  	entries, err := scanOldLogFiles(config.Path)
   179  	if err != nil {
   180  		return err
   181  	}
   182  	// entries has only the old renamed log files, not the current
   183  	// log file. E.g. if MaxKeepFiles is 2 then we keep the current
   184  	// file and one archived log file. If there are 3 archived files
   185  	// then removeN = 1 + 3 - 2 = 2.
   186  	removeN := 1 + len(entries) - config.MaxKeepFiles
   187  	if config.MaxKeepFiles <= 0 || removeN <= 0 {
   188  		return nil
   189  	}
   190  	// Try to remove all old log files that we want to remove, and
   191  	// don't stop on the first error.
   192  	for i := 0; i < removeN; i++ {
   193  		err2 := os.Remove(entries[i])
   194  		if err == nil {
   195  			err = err2
   196  		}
   197  	}
   198  	return err
   199  }
   200  
   201  type logFilename struct {
   202  	fName string
   203  	start time.Time
   204  }
   205  
   206  type logFilenamesByTime []logFilename
   207  
   208  func (a logFilenamesByTime) Len() int      { return len(a) }
   209  func (a logFilenamesByTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
   210  func (a logFilenamesByTime) Less(i, j int) bool {
   211  	return a[i].start.Before(a[j].start)
   212  }
   213  
   214  // getLogFilenamesOrderByTime filters fNames to return only old log files
   215  // starting with baseName, followed by a timestamp-range suffix. It also sorts
   216  // them by start time, in increasing order.
   217  //
   218  // Both baseName and fNames are base names not including dir names.
   219  //
   220  // This function supports both old (no timezone) and current (with timezone)
   221  // format of log file names. TODO: simplify this when we don't care about old
   222  // format any more.
   223  func getLogFilenamesOrderByTime(
   224  	baseName string, fNames []string) (names []string, err error) {
   225  	re, err := regexp.Compile(`^` + regexp.QuoteMeta(baseName) +
   226  		`-(\d{8}T\d{6}(?:(?:[Z\+-]\d{4})|(?:Z))?)-\d{8}T\d{6}(?:(?:[Z\+-]\d{4})|(?:Z))?$`)
   227  	if err != nil {
   228  		return nil, err
   229  	}
   230  
   231  	var logFilenames []logFilename
   232  	for _, fName := range fNames {
   233  		match := re.FindStringSubmatch(fName)
   234  		if len(match) != 2 {
   235  			continue
   236  		}
   237  		t, err1 := time.ParseInLocation(oldLogFileTimeRangeTimeLayout, match[1], time.Local)
   238  		if err1 != nil {
   239  			var err2 error
   240  			t, err2 = time.ParseInLocation(oldLogFileTimeRangeTimeLayoutLegacy, match[1], time.Local)
   241  			if err2 != nil {
   242  				return nil, errors.New(err1.Error() + " | " + err2.Error())
   243  			}
   244  		}
   245  		logFilenames = append(logFilenames, logFilename{fName: fName, start: t})
   246  	}
   247  
   248  	sort.Sort(logFilenamesByTime(logFilenames))
   249  
   250  	names = make([]string, 0, len(logFilenames))
   251  	for _, f := range logFilenames {
   252  		names = append(names, f.fName)
   253  	}
   254  
   255  	return names, nil
   256  }
   257  
   258  // scanOldLogFiles finds old archived log files corresponding to the log file path.
   259  // Returns the list of such log files sorted with the eldest one first.
   260  func scanOldLogFiles(path string) ([]string, error) {
   261  	dname, fname := filepath.Split(path)
   262  	if dname == "" {
   263  		dname = "."
   264  	}
   265  	dir, err := os.Open(dname)
   266  	if err != nil {
   267  		return nil, err
   268  	}
   269  	defer dir.Close()
   270  	ns, err := dir.Readdirnames(-1)
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  	names, err := getLogFilenamesOrderByTime(fname, ns)
   275  	if err != nil {
   276  		return nil, err
   277  	}
   278  	var res []string
   279  	for _, name := range names {
   280  		res = append(res, filepath.Join(dname, name))
   281  	}
   282  	return res, nil
   283  }