github.com/manicqin/nomad@v0.9.5/client/logmon/logging/rotator.go (about)

     1  package logging
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"sort"
    11  	"sync"
    12  	"time"
    13  
    14  	hclog "github.com/hashicorp/go-hclog"
    15  )
    16  
    17  const (
    18  	// logBufferSize is the size of the buffer.
    19  	logBufferSize = 64 * 1024
    20  
    21  	// bufferFlushDuration is the duration at which we flush the buffer.
    22  	bufferFlushDuration = 100 * time.Millisecond
    23  
    24  	// lineScanLimit is the number of bytes we will attempt to scan for new
    25  	// lines when approaching the end of the file to avoid a log line being
    26  	// split between two files. Any single line that is greater than this limit
    27  	// may be split.
    28  	lineScanLimit = 32 * 1024
    29  
    30  	// newLineDelimiter is the delimiter used for new lines.
    31  	newLineDelimiter = '\n'
    32  )
    33  
    34  // FileRotator writes bytes to a rotated set of files
    35  type FileRotator struct {
    36  	MaxFiles      int    // MaxFiles is the maximum number of rotated files allowed in a path
    37  	FileSize      int64  // FileSize is the size a rotated file is allowed to grow
    38  	FileExtension string // FileExtension is appened to the end of the rotated files
    39  
    40  	path             string // path is the path on the file system where the rotated set of files are opened
    41  	baseFileName     string // baseFileName is the base file name of the rotated files
    42  	fileNameTemplate string // file name template
    43  	logFileIdx       int    // logFileIdx is the current index of the rotated files
    44  	oldestLogFileIdx int    // oldestLogFileIdx is the index of the oldest log file in a path
    45  
    46  	currentFile *os.File // currentFile is the file that is currently getting written
    47  	currentWr   int64    // currentWr is the number of bytes written to the current file
    48  	bufw        *bufio.Writer
    49  	bufLock     sync.Mutex
    50  
    51  	flushTicker *time.Ticker
    52  	logger      hclog.Logger
    53  	purgeCh     chan struct{}
    54  	doneCh      chan struct{}
    55  
    56  	closed     bool
    57  	closedLock sync.Mutex
    58  }
    59  
    60  // NewFileRotator returns a new file rotator
    61  func NewFileRotator(path string, baseFile string, maxFiles int,
    62  	fileSize int64, fileExtension string, logger hclog.Logger) (*FileRotator, error) {
    63  	logger = logger.Named("rotator")
    64  	template := baseFile + ".%d"
    65  	if fileExtension != "" {
    66  		template += "." + fileExtension
    67  	}
    68  	rotator := &FileRotator{
    69  		MaxFiles:      maxFiles,
    70  		FileSize:      fileSize,
    71  		FileExtension: fileExtension,
    72  
    73  		path:             path,
    74  		baseFileName:     baseFile,
    75  		fileNameTemplate: template,
    76  
    77  		flushTicker: time.NewTicker(bufferFlushDuration),
    78  		logger:      logger,
    79  		purgeCh:     make(chan struct{}, 1),
    80  		doneCh:      make(chan struct{}, 1),
    81  	}
    82  
    83  	if err := rotator.lastFile(); err != nil {
    84  		return nil, err
    85  	}
    86  	go rotator.purgeOldFiles()
    87  	go rotator.flushPeriodically()
    88  	return rotator, nil
    89  }
    90  
    91  // Write writes a byte array to a file and rotates the file if it's size becomes
    92  // equal to the maximum size the user has defined.
    93  func (f *FileRotator) Write(p []byte) (n int, err error) {
    94  	n = 0
    95  	var forceRotate bool
    96  
    97  	for n < len(p) {
    98  		// Check if we still have space in the current file, otherwise close and
    99  		// open the next file
   100  		if forceRotate || f.currentWr >= f.FileSize {
   101  			forceRotate = false
   102  			f.flushBuffer()
   103  			f.currentFile.Close()
   104  			if err := f.nextFile(); err != nil {
   105  				f.logger.Error("error creating next file", "err", err)
   106  				return 0, err
   107  			}
   108  		}
   109  		// Calculate the remaining size on this file and how much we have left
   110  		// to write
   111  		remainingSpace := f.FileSize - f.currentWr
   112  		remainingToWrite := int64(len(p[n:]))
   113  
   114  		// Check if we are near the end of the file. If we are we attempt to
   115  		// avoid a log line being split between two files.
   116  		var nw int
   117  		if (remainingSpace - lineScanLimit) < remainingToWrite {
   118  			// Scan for new line and if the data up to new line fits in current
   119  			// file, write to buffer
   120  			idx := bytes.IndexByte(p[n:], newLineDelimiter)
   121  			if idx >= 0 && (remainingSpace-int64(idx)-1) >= 0 {
   122  				// We have space so write it to buffer
   123  				nw, err = f.writeToBuffer(p[n : n+idx+1])
   124  			} else if idx >= 0 {
   125  				// We found a new line but don't have space so just force rotate
   126  				forceRotate = true
   127  			} else if remainingToWrite > f.FileSize || f.FileSize-lineScanLimit < 0 {
   128  				// There is no new line remaining but there is no point in
   129  				// rotating since the remaining data will not even fit in the
   130  				// next file either so just fill this one up.
   131  				li := int64(n) + remainingSpace
   132  				if remainingSpace > remainingToWrite {
   133  					li = int64(n) + remainingToWrite
   134  				}
   135  				nw, err = f.writeToBuffer(p[n:li])
   136  			} else {
   137  				// There is no new line in the data remaining for us to write
   138  				// and it will fit in the next file so rotate.
   139  				forceRotate = true
   140  			}
   141  		} else {
   142  			// Write all the bytes in the current file
   143  			nw, err = f.writeToBuffer(p[n:])
   144  		}
   145  
   146  		// Increment the number of bytes written so far in this method
   147  		// invocation
   148  		n += nw
   149  
   150  		// Increment the total number of bytes in the file
   151  		f.currentWr += int64(n)
   152  		if err != nil {
   153  			f.logger.Error("error writing to file", "err", err)
   154  
   155  			// As bufio writer does not automatically recover in case of any
   156  			// io error, we need to recover from it manually resetting the
   157  			// writter.
   158  			f.createOrResetBuffer()
   159  
   160  			return
   161  		}
   162  	}
   163  	return
   164  }
   165  
   166  // nextFile opens the next file and purges older files if the number of rotated
   167  // files is larger than the maximum files configured by the user
   168  func (f *FileRotator) nextFile() error {
   169  	nextFileIdx := f.logFileIdx
   170  	for {
   171  		nextFileIdx += 1
   172  		logFileName := filepath.Join(f.path, fmt.Sprintf(f.fileNameTemplate, nextFileIdx))
   173  		if fi, err := os.Stat(logFileName); err == nil {
   174  			if fi.IsDir() || fi.Size() >= f.FileSize {
   175  				continue
   176  			}
   177  		}
   178  		f.logFileIdx = nextFileIdx
   179  		if err := f.createFile(); err != nil {
   180  			return err
   181  		}
   182  		break
   183  	}
   184  	// Purge old files if we have more files than MaxFiles
   185  	f.closedLock.Lock()
   186  	defer f.closedLock.Unlock()
   187  	if f.logFileIdx-f.oldestLogFileIdx >= f.MaxFiles && !f.closed {
   188  		select {
   189  		case f.purgeCh <- struct{}{}:
   190  		default:
   191  		}
   192  	}
   193  	return nil
   194  }
   195  
   196  // lastFile finds out the rotated file with the largest index in a path.
   197  func (f *FileRotator) lastFile() error {
   198  	finfos, err := ioutil.ReadDir(f.path)
   199  	if err != nil {
   200  		return err
   201  	}
   202  
   203  	for _, fi := range finfos {
   204  		if fi.IsDir() {
   205  			continue
   206  		}
   207  		var idx int
   208  		if n, err := fmt.Sscanf(fi.Name(), f.fileNameTemplate, &idx); err != nil || n == 0 {
   209  			continue
   210  		}
   211  		if idx > f.logFileIdx {
   212  			f.logFileIdx = idx
   213  		}
   214  	}
   215  	if err := f.createFile(); err != nil {
   216  		return err
   217  	}
   218  	return nil
   219  }
   220  
   221  // createFile opens a new or existing file for writing
   222  func (f *FileRotator) createFile() error {
   223  	logFileName := filepath.Join(f.path, fmt.Sprintf(f.fileNameTemplate, f.logFileIdx))
   224  	cFile, err := os.OpenFile(logFileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
   225  	if err != nil {
   226  		return err
   227  	}
   228  
   229  	f.currentFile = cFile
   230  	fi, err := f.currentFile.Stat()
   231  	if err != nil {
   232  		return err
   233  	}
   234  	f.currentWr = fi.Size()
   235  	f.createOrResetBuffer()
   236  	return nil
   237  }
   238  
   239  // flushPeriodically flushes the buffered writer every 100ms to the underlying
   240  // file
   241  func (f *FileRotator) flushPeriodically() {
   242  	for range f.flushTicker.C {
   243  		f.flushBuffer()
   244  	}
   245  }
   246  
   247  // Close flushes and closes the rotator. It never returns an error.
   248  func (f *FileRotator) Close() error {
   249  	f.closedLock.Lock()
   250  	defer f.closedLock.Unlock()
   251  
   252  	// Stop the ticker and flush for one last time
   253  	f.flushTicker.Stop()
   254  	f.flushBuffer()
   255  
   256  	// Stop the purge go routine
   257  	if !f.closed {
   258  		f.doneCh <- struct{}{}
   259  		close(f.purgeCh)
   260  		f.closed = true
   261  		f.currentFile.Close()
   262  	}
   263  
   264  	return nil
   265  }
   266  
   267  // purgeOldFiles removes older files and keeps only the last N files rotated for
   268  // a file
   269  func (f *FileRotator) purgeOldFiles() {
   270  	for {
   271  		select {
   272  		case <-f.purgeCh:
   273  			var fIndexes []int
   274  			files, err := ioutil.ReadDir(f.path)
   275  			if err != nil {
   276  				f.logger.Error("error getting directory listing", "err", err)
   277  				return
   278  			}
   279  			// Inserting all the rotated files in a slice
   280  			for _, fi := range files {
   281  				var idx int
   282  				if n, err := fmt.Sscanf(fi.Name(), f.fileNameTemplate, &idx); err != nil {
   283  					f.logger.Error("error extracting file index", "err", err)
   284  					continue
   285  				} else if n == 0 {
   286  					continue
   287  				}
   288  				fIndexes = append(fIndexes, idx)
   289  			}
   290  
   291  			// Not continuing to delete files if the number of files is not more
   292  			// than MaxFiles
   293  			if len(fIndexes) <= f.MaxFiles {
   294  				continue
   295  			}
   296  
   297  			// Sorting the file indexes so that we can purge the older files and keep
   298  			// only the number of files as configured by the user
   299  			sort.Sort(sort.IntSlice(fIndexes))
   300  			toDelete := fIndexes[0 : len(fIndexes)-f.MaxFiles]
   301  			for _, fIndex := range toDelete {
   302  				fname := filepath.Join(f.path, fmt.Sprintf(f.fileNameTemplate, fIndex))
   303  				err := os.RemoveAll(fname)
   304  				if err != nil {
   305  					f.logger.Error("error removing file", "filename", fname, "err", err)
   306  				}
   307  			}
   308  			f.oldestLogFileIdx = fIndexes[0]
   309  		case <-f.doneCh:
   310  			return
   311  		}
   312  	}
   313  }
   314  
   315  // flushBuffer flushes the buffer
   316  func (f *FileRotator) flushBuffer() error {
   317  	f.bufLock.Lock()
   318  	defer f.bufLock.Unlock()
   319  	if f.bufw != nil {
   320  		return f.bufw.Flush()
   321  	}
   322  	return nil
   323  }
   324  
   325  // writeToBuffer writes the byte array to buffer
   326  func (f *FileRotator) writeToBuffer(p []byte) (int, error) {
   327  	f.bufLock.Lock()
   328  	defer f.bufLock.Unlock()
   329  	return f.bufw.Write(p)
   330  }
   331  
   332  // createOrResetBuffer creates a new buffer if we don't have one otherwise
   333  // resets the buffer
   334  func (f *FileRotator) createOrResetBuffer() {
   335  	f.bufLock.Lock()
   336  	defer f.bufLock.Unlock()
   337  	if f.bufw == nil {
   338  		f.bufw = bufio.NewWriterSize(f.currentFile, logBufferSize)
   339  	} else {
   340  		f.bufw.Reset(f.currentFile)
   341  	}
   342  }