github.com/anuvu/nomad@v0.8.7-atom1/client/driver/logging/rotator.go (about)

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