// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package logging import ( "fmt" "os" "path/filepath" "sort" "strconv" "strings" "sync" "time" "github.com/hashicorp/go-multierror" ) var now = time.Now type LogFile struct { // Name of the log file fileName string // Path to the log file logPath string // duration between each file rotation operation duration time.Duration // lastCreated represents the creation time of the latest log lastCreated time.Time // fileInfo is the pointer to the current file being written to fileInfo *os.File // maxBytes is the maximum number of desired bytes for a log file maxBytes int // bytesWritten is the number of bytes written in the current log file bytesWritten int64 // Max rotated files to keep before removing them. maxArchivedFiles int // acquire is the mutex utilized to ensure we have no concurrency issues acquire sync.Mutex } // Write is used to implement io.Writer func (l *LogFile) Write(b []byte) (n int, err error) { l.acquire.Lock() defer l.acquire.Unlock() // Create a new file if we have no file to write to if l.fileInfo == nil { if err := l.openNew(); err != nil { return 0, err } } else if err := l.rotate(); err != nil { // Check for the last contact and rotate if necessary return 0, err } bytesWritten, err := l.fileInfo.Write(b) if bytesWritten > 0 { l.bytesWritten += int64(bytesWritten) } return bytesWritten, err } func (l *LogFile) fileNamePattern() string { // Extract the file extension fileExt := filepath.Ext(l.fileName) // If we have no file extension we append .log if fileExt == "" { fileExt = ".log" } // Remove the file extension from the filename return strings.TrimSuffix(l.fileName, fileExt) + "-%s" + fileExt } func (l *LogFile) openNew() error { fileNamePattern := l.fileNamePattern() createTime := now() newFileName := fmt.Sprintf(fileNamePattern, strconv.FormatInt(createTime.UnixNano(), 10)) newFilePath := filepath.Join(l.logPath, newFileName) // Try creating a file. We truncate the file because we are the only authority to write the logs filePointer, err := os.OpenFile(newFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o640) if err != nil { return err } // New file, new 'bytes' tracker, new creation time :) :) l.fileInfo = filePointer l.lastCreated = createTime l.bytesWritten = 0 return nil } func (l *LogFile) rotate() error { // Get the time from the last point of contact timeElapsed := time.Since(l.lastCreated) // Rotate if we hit the byte file limit or the time limit if (l.bytesWritten >= int64(l.maxBytes) && (l.maxBytes > 0)) || timeElapsed >= l.duration { if err := l.fileInfo.Close(); err != nil { return err } if err := l.pruneFiles(); err != nil { return err } return l.openNew() } return nil } func (l *LogFile) pruneFiles() error { if l.maxArchivedFiles == 0 { return nil } pattern := filepath.Join(l.logPath, fmt.Sprintf(l.fileNamePattern(), "*")) matches, err := filepath.Glob(pattern) if err != nil { return err } switch { case l.maxArchivedFiles < 0: return removeFiles(matches) case len(matches) < l.maxArchivedFiles: return nil } sort.Strings(matches) last := len(matches) - l.maxArchivedFiles return removeFiles(matches[:last]) } func removeFiles(files []string) (err error) { for _, file := range files { if fileError := os.Remove(file); fileError != nil { err = multierror.Append(err, fmt.Errorf("error removing file %s: %v", file, fileError)) } } return err }