github.com/influxdata/telegraf@v1.30.3/internal/rotate/file_writer.go (about)

     1  package rotate
     2  
     3  // Rotating things
     4  import (
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"sort"
    10  	"strconv"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  )
    15  
    16  // FilePerm defines the permissions that Writer will use for all
    17  // the files it creates.
    18  const (
    19  	FilePerm   = os.FileMode(0644)
    20  	DateFormat = "2006-01-02"
    21  )
    22  
    23  // FileWriter implements the io.Writer interface and writes to the
    24  // filename specified.
    25  // Will rotate at the specified interval and/or when the current file size exceeds maxSizeInBytes
    26  // At rotation time, current file is renamed and a new file is created.
    27  // If the number of archives exceeds maxArchives, older files are deleted.
    28  type FileWriter struct {
    29  	filename                 string
    30  	filenameRotationTemplate string
    31  	current                  *os.File
    32  	interval                 time.Duration
    33  	maxSizeInBytes           int64
    34  	maxArchives              int
    35  	expireTime               time.Time
    36  	bytesWritten             int64
    37  	sync.Mutex
    38  }
    39  
    40  // NewFileWriter creates a new file writer.
    41  func NewFileWriter(filename string, interval time.Duration, maxSizeInBytes int64, maxArchives int) (io.WriteCloser, error) {
    42  	if interval == 0 && maxSizeInBytes <= 0 {
    43  		// No rotation needed so a basic io.Writer will do the trick
    44  		return openFile(filename)
    45  	}
    46  
    47  	w := &FileWriter{
    48  		filename:                 filename,
    49  		interval:                 interval,
    50  		maxSizeInBytes:           maxSizeInBytes,
    51  		maxArchives:              maxArchives,
    52  		filenameRotationTemplate: getFilenameRotationTemplate(filename),
    53  	}
    54  
    55  	if err := w.openCurrent(); err != nil {
    56  		return nil, err
    57  	}
    58  
    59  	return w, nil
    60  }
    61  
    62  func openFile(filename string) (*os.File, error) {
    63  	return os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, FilePerm)
    64  }
    65  
    66  func getFilenameRotationTemplate(filename string) string {
    67  	// Extract the file extension
    68  	fileExt := filepath.Ext(filename)
    69  	// Remove the file extension from the filename (if any)
    70  	stem := strings.TrimSuffix(filename, fileExt)
    71  	return stem + ".%s-%s" + fileExt
    72  }
    73  
    74  // Write writes p to the current file, then checks to see if
    75  // rotation is necessary.
    76  func (w *FileWriter) Write(p []byte) (n int, err error) {
    77  	w.Lock()
    78  	defer w.Unlock()
    79  	if n, err = w.current.Write(p); err != nil {
    80  		return 0, err
    81  	}
    82  	w.bytesWritten += int64(n)
    83  
    84  	if err := w.rotateIfNeeded(); err != nil {
    85  		return 0, err
    86  	}
    87  
    88  	return n, nil
    89  }
    90  
    91  // Close closes the current file.  Writer is unusable after this
    92  // is called.
    93  func (w *FileWriter) Close() (err error) {
    94  	w.Lock()
    95  	defer w.Unlock()
    96  
    97  	// Rotate before closing
    98  	if err := w.rotateIfNeeded(); err != nil {
    99  		return err
   100  	}
   101  
   102  	// Close the file if we did not rotate
   103  	if err := w.current.Close(); err != nil {
   104  		return err
   105  	}
   106  
   107  	w.current = nil
   108  	return nil
   109  }
   110  
   111  func (w *FileWriter) openCurrent() (err error) {
   112  	// In case ModTime() fails, we use time.Now()
   113  	w.expireTime = time.Now().Add(w.interval)
   114  	w.bytesWritten = 0
   115  	w.current, err = openFile(w.filename)
   116  
   117  	if err != nil {
   118  		return err
   119  	}
   120  
   121  	// Goal here is to rotate old pre-existing files.
   122  	// For that we use fileInfo.ModTime, instead of time.Now().
   123  	// Example: telegraf is restarted every 23 hours and
   124  	// the rotation interval is set to 24 hours.
   125  	// With time.now() as a reference we'd never rotate the file.
   126  	if fileInfo, err := w.current.Stat(); err == nil {
   127  		w.expireTime = fileInfo.ModTime().Add(w.interval)
   128  		w.bytesWritten = fileInfo.Size()
   129  	}
   130  
   131  	return w.rotateIfNeeded()
   132  }
   133  
   134  func (w *FileWriter) rotateIfNeeded() error {
   135  	if (w.interval > 0 && time.Now().After(w.expireTime)) ||
   136  		(w.maxSizeInBytes > 0 && w.bytesWritten >= w.maxSizeInBytes) {
   137  		if err := w.rotate(); err != nil {
   138  			//Ignore rotation errors and keep the log open
   139  			fmt.Printf("unable to rotate the file %q, %s", w.filename, err.Error())
   140  		}
   141  		return w.openCurrent()
   142  	}
   143  	return nil
   144  }
   145  
   146  func (w *FileWriter) rotate() (err error) {
   147  	if err := w.current.Close(); err != nil {
   148  		return err
   149  	}
   150  
   151  	// Use year-month-date for readability, unix time to make the file name unique with second precision
   152  	now := time.Now()
   153  	rotatedFilename := fmt.Sprintf(w.filenameRotationTemplate, now.Format(DateFormat), strconv.FormatInt(now.Unix(), 10))
   154  	if err := os.Rename(w.filename, rotatedFilename); err != nil {
   155  		return err
   156  	}
   157  
   158  	return w.purgeArchivesIfNeeded()
   159  }
   160  
   161  func (w *FileWriter) purgeArchivesIfNeeded() (err error) {
   162  	if w.maxArchives == -1 {
   163  		//Skip archiving
   164  		return nil
   165  	}
   166  
   167  	var matches []string
   168  	if matches, err = filepath.Glob(fmt.Sprintf(w.filenameRotationTemplate, "*", "*")); err != nil {
   169  		return err
   170  	}
   171  
   172  	//if there are more archives than the configured maximum, then purge older files
   173  	if len(matches) > w.maxArchives {
   174  		//sort files alphanumerically to delete older files first
   175  		sort.Strings(matches)
   176  		for _, filename := range matches[:len(matches)-w.maxArchives] {
   177  			if err := os.Remove(filename); err != nil {
   178  				return err
   179  			}
   180  		}
   181  	}
   182  	return nil
   183  }