github.com/zhouyu0/docker-note@v0.0.0-20190722021225-b8d3825084db/daemon/logger/loggerutils/logfile.go (about)

     1  package loggerutils // import "github.com/docker/docker/daemon/logger/loggerutils"
     2  
     3  import (
     4  	"compress/gzip"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"runtime"
    11  	"strconv"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/docker/docker/daemon/logger"
    17  	"github.com/docker/docker/pkg/filenotify"
    18  	"github.com/docker/docker/pkg/pools"
    19  	"github.com/docker/docker/pkg/pubsub"
    20  	"github.com/fsnotify/fsnotify"
    21  	"github.com/pkg/errors"
    22  	"github.com/sirupsen/logrus"
    23  )
    24  
    25  const tmpLogfileSuffix = ".tmp"
    26  
    27  // rotateFileMetadata is a metadata of the gzip header of the compressed log file
    28  type rotateFileMetadata struct {
    29  	LastTime time.Time `json:"lastTime,omitempty"`
    30  }
    31  
    32  // refCounter is a counter of logfile being referenced
    33  type refCounter struct {
    34  	mu      sync.Mutex
    35  	counter map[string]int
    36  }
    37  
    38  // Reference increase the reference counter for specified logfile
    39  func (rc *refCounter) GetReference(fileName string, openRefFile func(fileName string, exists bool) (*os.File, error)) (*os.File, error) {
    40  	rc.mu.Lock()
    41  	defer rc.mu.Unlock()
    42  
    43  	var (
    44  		file *os.File
    45  		err  error
    46  	)
    47  	_, ok := rc.counter[fileName]
    48  	file, err = openRefFile(fileName, ok)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  
    53  	if ok {
    54  		rc.counter[fileName]++
    55  	} else if file != nil {
    56  		rc.counter[file.Name()] = 1
    57  	}
    58  
    59  	return file, nil
    60  }
    61  
    62  // Dereference reduce the reference counter for specified logfile
    63  func (rc *refCounter) Dereference(fileName string) error {
    64  	rc.mu.Lock()
    65  	defer rc.mu.Unlock()
    66  
    67  	rc.counter[fileName]--
    68  	if rc.counter[fileName] <= 0 {
    69  		delete(rc.counter, fileName)
    70  		err := os.Remove(fileName)
    71  		if err != nil {
    72  			return err
    73  		}
    74  	}
    75  	return nil
    76  }
    77  
    78  // LogFile is Logger implementation for default Docker logging.
    79  type LogFile struct {
    80  	mu              sync.RWMutex // protects the logfile access
    81  	f               *os.File     // store for closing
    82  	closed          bool
    83  	rotateMu        sync.Mutex // blocks the next rotation until the current rotation is completed
    84  	capacity        int64      // maximum size of each file
    85  	currentSize     int64      // current size of the latest file
    86  	maxFiles        int        // maximum number of files
    87  	compress        bool       // whether old versions of log files are compressed
    88  	lastTimestamp   time.Time  // timestamp of the last log
    89  	filesRefCounter refCounter // keep reference-counted of decompressed files
    90  	notifyRotate    *pubsub.Publisher
    91  	marshal         logger.MarshalFunc
    92  	createDecoder   makeDecoderFunc
    93  	getTailReader   GetTailReaderFunc
    94  	perms           os.FileMode
    95  }
    96  
    97  type makeDecoderFunc func(rdr io.Reader) func() (*logger.Message, error)
    98  
    99  // SizeReaderAt defines a ReaderAt that also reports its size.
   100  // This is used for tailing log files.
   101  type SizeReaderAt interface {
   102  	io.ReaderAt
   103  	Size() int64
   104  }
   105  
   106  // GetTailReaderFunc is used to truncate a reader to only read as much as is required
   107  // in order to get the passed in number of log lines.
   108  // It returns the sectioned reader, the number of lines that the section reader
   109  // contains, and any error that occurs.
   110  type GetTailReaderFunc func(ctx context.Context, f SizeReaderAt, nLogLines int) (rdr io.Reader, nLines int, err error)
   111  
   112  // NewLogFile creates new LogFile
   113  func NewLogFile(logPath string, capacity int64, maxFiles int, compress bool, marshaller logger.MarshalFunc, decodeFunc makeDecoderFunc, perms os.FileMode, getTailReader GetTailReaderFunc) (*LogFile, error) {
   114  	log, err := os.OpenFile(logPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, perms)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	size, err := log.Seek(0, os.SEEK_END)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	return &LogFile{
   125  		f:               log,
   126  		capacity:        capacity,
   127  		currentSize:     size,
   128  		maxFiles:        maxFiles,
   129  		compress:        compress,
   130  		filesRefCounter: refCounter{counter: make(map[string]int)},
   131  		notifyRotate:    pubsub.NewPublisher(0, 1),
   132  		marshal:         marshaller,
   133  		createDecoder:   decodeFunc,
   134  		perms:           perms,
   135  		getTailReader:   getTailReader,
   136  	}, nil
   137  }
   138  
   139  // WriteLogEntry writes the provided log message to the current log file.
   140  // This may trigger a rotation event if the max file/capacity limits are hit.
   141  func (w *LogFile) WriteLogEntry(msg *logger.Message) error {
   142  	b, err := w.marshal(msg)
   143  	if err != nil {
   144  		return errors.Wrap(err, "error marshalling log message")
   145  	}
   146  
   147  	logger.PutMessage(msg)
   148  
   149  	w.mu.Lock()
   150  	if w.closed {
   151  		w.mu.Unlock()
   152  		return errors.New("cannot write because the output file was closed")
   153  	}
   154  
   155  	if err := w.checkCapacityAndRotate(); err != nil {
   156  		w.mu.Unlock()
   157  		return err
   158  	}
   159  
   160  	n, err := w.f.Write(b)
   161  	if err == nil {
   162  		w.currentSize += int64(n)
   163  		w.lastTimestamp = msg.Timestamp
   164  	}
   165  	w.mu.Unlock()
   166  	return err
   167  }
   168  
   169  func (w *LogFile) checkCapacityAndRotate() error {
   170  	if w.capacity == -1 {
   171  		return nil
   172  	}
   173  
   174  	if w.currentSize >= w.capacity {
   175  		w.rotateMu.Lock()
   176  		fname := w.f.Name()
   177  		if err := w.f.Close(); err != nil {
   178  			w.rotateMu.Unlock()
   179  			return errors.Wrap(err, "error closing file")
   180  		}
   181  		if err := rotate(fname, w.maxFiles, w.compress); err != nil {
   182  			w.rotateMu.Unlock()
   183  			return err
   184  		}
   185  		file, err := os.OpenFile(fname, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, w.perms)
   186  		if err != nil {
   187  			w.rotateMu.Unlock()
   188  			return err
   189  		}
   190  		w.f = file
   191  		w.currentSize = 0
   192  		w.notifyRotate.Publish(struct{}{})
   193  
   194  		if w.maxFiles <= 1 || !w.compress {
   195  			w.rotateMu.Unlock()
   196  			return nil
   197  		}
   198  
   199  		go func() {
   200  			compressFile(fname+".1", w.lastTimestamp)
   201  			w.rotateMu.Unlock()
   202  		}()
   203  	}
   204  
   205  	return nil
   206  }
   207  
   208  func rotate(name string, maxFiles int, compress bool) error {
   209  	if maxFiles < 2 {
   210  		return nil
   211  	}
   212  
   213  	var extension string
   214  	if compress {
   215  		extension = ".gz"
   216  	}
   217  
   218  	lastFile := fmt.Sprintf("%s.%d%s", name, maxFiles-1, extension)
   219  	err := os.Remove(lastFile)
   220  	if err != nil && !os.IsNotExist(err) {
   221  		return errors.Wrap(err, "error removing oldest log file")
   222  	}
   223  
   224  	for i := maxFiles - 1; i > 1; i-- {
   225  		toPath := name + "." + strconv.Itoa(i) + extension
   226  		fromPath := name + "." + strconv.Itoa(i-1) + extension
   227  		if err := os.Rename(fromPath, toPath); err != nil && !os.IsNotExist(err) {
   228  			return err
   229  		}
   230  	}
   231  
   232  	if err := os.Rename(name, name+".1"); err != nil && !os.IsNotExist(err) {
   233  		return err
   234  	}
   235  
   236  	return nil
   237  }
   238  
   239  func compressFile(fileName string, lastTimestamp time.Time) {
   240  	file, err := os.Open(fileName)
   241  	if err != nil {
   242  		logrus.Errorf("Failed to open log file: %v", err)
   243  		return
   244  	}
   245  	defer func() {
   246  		file.Close()
   247  		err := os.Remove(fileName)
   248  		if err != nil {
   249  			logrus.Errorf("Failed to remove source log file: %v", err)
   250  		}
   251  	}()
   252  
   253  	outFile, err := os.OpenFile(fileName+".gz", os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0640)
   254  	if err != nil {
   255  		logrus.Errorf("Failed to open or create gzip log file: %v", err)
   256  		return
   257  	}
   258  	defer func() {
   259  		outFile.Close()
   260  		if err != nil {
   261  			os.Remove(fileName + ".gz")
   262  		}
   263  	}()
   264  
   265  	compressWriter := gzip.NewWriter(outFile)
   266  	defer compressWriter.Close()
   267  
   268  	// Add the last log entry timestramp to the gzip header
   269  	extra := rotateFileMetadata{}
   270  	extra.LastTime = lastTimestamp
   271  	compressWriter.Header.Extra, err = json.Marshal(&extra)
   272  	if err != nil {
   273  		// Here log the error only and don't return since this is just an optimization.
   274  		logrus.Warningf("Failed to marshal gzip header as JSON: %v", err)
   275  	}
   276  
   277  	_, err = pools.Copy(compressWriter, file)
   278  	if err != nil {
   279  		logrus.WithError(err).WithField("module", "container.logs").WithField("file", fileName).Error("Error compressing log file")
   280  		return
   281  	}
   282  }
   283  
   284  // MaxFiles return maximum number of files
   285  func (w *LogFile) MaxFiles() int {
   286  	return w.maxFiles
   287  }
   288  
   289  // Close closes underlying file and signals all readers to stop.
   290  func (w *LogFile) Close() error {
   291  	w.mu.Lock()
   292  	defer w.mu.Unlock()
   293  	if w.closed {
   294  		return nil
   295  	}
   296  	if err := w.f.Close(); err != nil {
   297  		return err
   298  	}
   299  	w.closed = true
   300  	return nil
   301  }
   302  
   303  // ReadLogs decodes entries from log files and sends them the passed in watcher
   304  //
   305  // Note: Using the follow option can become inconsistent in cases with very frequent rotations and max log files is 1.
   306  // TODO: Consider a different implementation which can effectively follow logs under frequent rotations.
   307  func (w *LogFile) ReadLogs(config logger.ReadConfig, watcher *logger.LogWatcher) {
   308  	w.mu.RLock()
   309  	currentFile, err := os.Open(w.f.Name())
   310  	if err != nil {
   311  		w.mu.RUnlock()
   312  		watcher.Err <- err
   313  		return
   314  	}
   315  	defer currentFile.Close()
   316  
   317  	currentChunk, err := newSectionReader(currentFile)
   318  	if err != nil {
   319  		w.mu.RUnlock()
   320  		watcher.Err <- err
   321  		return
   322  	}
   323  
   324  	if config.Tail != 0 {
   325  		// TODO(@cpuguy83): Instead of opening every file, only get the files which
   326  		// are needed to tail.
   327  		// This is especially costly when compression is enabled.
   328  		files, err := w.openRotatedFiles(config)
   329  		w.mu.RUnlock()
   330  		if err != nil {
   331  			watcher.Err <- err
   332  			return
   333  		}
   334  
   335  		closeFiles := func() {
   336  			for _, f := range files {
   337  				f.Close()
   338  				fileName := f.Name()
   339  				if strings.HasSuffix(fileName, tmpLogfileSuffix) {
   340  					err := w.filesRefCounter.Dereference(fileName)
   341  					if err != nil {
   342  						logrus.Errorf("Failed to dereference the log file %q: %v", fileName, err)
   343  					}
   344  				}
   345  			}
   346  		}
   347  
   348  		readers := make([]SizeReaderAt, 0, len(files)+1)
   349  		for _, f := range files {
   350  			stat, err := f.Stat()
   351  			if err != nil {
   352  				watcher.Err <- errors.Wrap(err, "error reading size of rotated file")
   353  				closeFiles()
   354  				return
   355  			}
   356  			readers = append(readers, io.NewSectionReader(f, 0, stat.Size()))
   357  		}
   358  		if currentChunk.Size() > 0 {
   359  			readers = append(readers, currentChunk)
   360  		}
   361  
   362  		tailFiles(readers, watcher, w.createDecoder, w.getTailReader, config)
   363  		closeFiles()
   364  
   365  		w.mu.RLock()
   366  	}
   367  
   368  	if !config.Follow || w.closed {
   369  		w.mu.RUnlock()
   370  		return
   371  	}
   372  	w.mu.RUnlock()
   373  
   374  	notifyRotate := w.notifyRotate.Subscribe()
   375  	defer w.notifyRotate.Evict(notifyRotate)
   376  	followLogs(currentFile, watcher, notifyRotate, w.createDecoder, config.Since, config.Until)
   377  }
   378  
   379  func (w *LogFile) openRotatedFiles(config logger.ReadConfig) (files []*os.File, err error) {
   380  	w.rotateMu.Lock()
   381  	defer w.rotateMu.Unlock()
   382  
   383  	defer func() {
   384  		if err == nil {
   385  			return
   386  		}
   387  		for _, f := range files {
   388  			f.Close()
   389  			if strings.HasSuffix(f.Name(), tmpLogfileSuffix) {
   390  				err := os.Remove(f.Name())
   391  				if err != nil && !os.IsNotExist(err) {
   392  					logrus.Warnf("Failed to remove logfile: %v", err)
   393  				}
   394  			}
   395  		}
   396  	}()
   397  
   398  	for i := w.maxFiles; i > 1; i-- {
   399  		f, err := os.Open(fmt.Sprintf("%s.%d", w.f.Name(), i-1))
   400  		if err != nil {
   401  			if !os.IsNotExist(err) {
   402  				return nil, errors.Wrap(err, "error opening rotated log file")
   403  			}
   404  
   405  			fileName := fmt.Sprintf("%s.%d.gz", w.f.Name(), i-1)
   406  			decompressedFileName := fileName + tmpLogfileSuffix
   407  			tmpFile, err := w.filesRefCounter.GetReference(decompressedFileName, func(refFileName string, exists bool) (*os.File, error) {
   408  				if exists {
   409  					return os.Open(refFileName)
   410  				}
   411  				return decompressfile(fileName, refFileName, config.Since)
   412  			})
   413  
   414  			if err != nil {
   415  				if !os.IsNotExist(errors.Cause(err)) {
   416  					return nil, errors.Wrap(err, "error getting reference to decompressed log file")
   417  				}
   418  				continue
   419  			}
   420  			if tmpFile == nil {
   421  				// The log before `config.Since` does not need to read
   422  				break
   423  			}
   424  
   425  			files = append(files, tmpFile)
   426  			continue
   427  		}
   428  		files = append(files, f)
   429  	}
   430  
   431  	return files, nil
   432  }
   433  
   434  func decompressfile(fileName, destFileName string, since time.Time) (*os.File, error) {
   435  	cf, err := os.Open(fileName)
   436  	if err != nil {
   437  		return nil, errors.Wrap(err, "error opening file for decompression")
   438  	}
   439  	defer cf.Close()
   440  
   441  	rc, err := gzip.NewReader(cf)
   442  	if err != nil {
   443  		return nil, errors.Wrap(err, "error making gzip reader for compressed log file")
   444  	}
   445  	defer rc.Close()
   446  
   447  	// Extract the last log entry timestramp from the gzip header
   448  	extra := &rotateFileMetadata{}
   449  	err = json.Unmarshal(rc.Header.Extra, extra)
   450  	if err == nil && extra.LastTime.Before(since) {
   451  		return nil, nil
   452  	}
   453  
   454  	rs, err := os.OpenFile(destFileName, os.O_CREATE|os.O_RDWR, 0640)
   455  	if err != nil {
   456  		return nil, errors.Wrap(err, "error creating file for copying decompressed log stream")
   457  	}
   458  
   459  	_, err = pools.Copy(rs, rc)
   460  	if err != nil {
   461  		rs.Close()
   462  		rErr := os.Remove(rs.Name())
   463  		if rErr != nil && !os.IsNotExist(rErr) {
   464  			logrus.Errorf("Failed to remove logfile: %v", rErr)
   465  		}
   466  		return nil, errors.Wrap(err, "error while copying decompressed log stream to file")
   467  	}
   468  
   469  	return rs, nil
   470  }
   471  
   472  func newSectionReader(f *os.File) (*io.SectionReader, error) {
   473  	// seek to the end to get the size
   474  	// we'll leave this at the end of the file since section reader does not advance the reader
   475  	size, err := f.Seek(0, os.SEEK_END)
   476  	if err != nil {
   477  		return nil, errors.Wrap(err, "error getting current file size")
   478  	}
   479  	return io.NewSectionReader(f, 0, size), nil
   480  }
   481  
   482  func tailFiles(files []SizeReaderAt, watcher *logger.LogWatcher, createDecoder makeDecoderFunc, getTailReader GetTailReaderFunc, config logger.ReadConfig) {
   483  	nLines := config.Tail
   484  
   485  	ctx, cancel := context.WithCancel(context.Background())
   486  	defer cancel()
   487  	// TODO(@cpuguy83): we should plumb a context through instead of dealing with `WatchClose()` here.
   488  	go func() {
   489  		select {
   490  		case <-ctx.Done():
   491  		case <-watcher.WatchConsumerGone():
   492  			cancel()
   493  		}
   494  	}()
   495  
   496  	readers := make([]io.Reader, 0, len(files))
   497  
   498  	if config.Tail > 0 {
   499  		for i := len(files) - 1; i >= 0 && nLines > 0; i-- {
   500  			tail, n, err := getTailReader(ctx, files[i], nLines)
   501  			if err != nil {
   502  				watcher.Err <- errors.Wrap(err, "error finding file position to start log tailing")
   503  				return
   504  			}
   505  			nLines -= n
   506  			readers = append([]io.Reader{tail}, readers...)
   507  		}
   508  	} else {
   509  		for _, r := range files {
   510  			readers = append(readers, &wrappedReaderAt{ReaderAt: r})
   511  		}
   512  	}
   513  
   514  	rdr := io.MultiReader(readers...)
   515  	decodeLogLine := createDecoder(rdr)
   516  	for {
   517  		msg, err := decodeLogLine()
   518  		if err != nil {
   519  			if errors.Cause(err) != io.EOF {
   520  				watcher.Err <- err
   521  			}
   522  			return
   523  		}
   524  		if !config.Since.IsZero() && msg.Timestamp.Before(config.Since) {
   525  			continue
   526  		}
   527  		if !config.Until.IsZero() && msg.Timestamp.After(config.Until) {
   528  			return
   529  		}
   530  		select {
   531  		case <-ctx.Done():
   532  			return
   533  		case watcher.Msg <- msg:
   534  		}
   535  	}
   536  }
   537  
   538  func followLogs(f *os.File, logWatcher *logger.LogWatcher, notifyRotate chan interface{}, createDecoder makeDecoderFunc, since, until time.Time) {
   539  	decodeLogLine := createDecoder(f)
   540  
   541  	name := f.Name()
   542  	fileWatcher, err := watchFile(name)
   543  	if err != nil {
   544  		logWatcher.Err <- err
   545  		return
   546  	}
   547  	defer func() {
   548  		f.Close()
   549  		fileWatcher.Close()
   550  	}()
   551  
   552  	var retries int
   553  	handleRotate := func() error {
   554  		f.Close()
   555  		fileWatcher.Remove(name)
   556  
   557  		// retry when the file doesn't exist
   558  		for retries := 0; retries <= 5; retries++ {
   559  			f, err = os.Open(name)
   560  			if err == nil || !os.IsNotExist(err) {
   561  				break
   562  			}
   563  		}
   564  		if err != nil {
   565  			return err
   566  		}
   567  		if err := fileWatcher.Add(name); err != nil {
   568  			return err
   569  		}
   570  		decodeLogLine = createDecoder(f)
   571  		return nil
   572  	}
   573  
   574  	errRetry := errors.New("retry")
   575  	errDone := errors.New("done")
   576  	waitRead := func() error {
   577  		select {
   578  		case e := <-fileWatcher.Events():
   579  			switch e.Op {
   580  			case fsnotify.Write:
   581  				decodeLogLine = createDecoder(f)
   582  				return nil
   583  			case fsnotify.Rename, fsnotify.Remove:
   584  				select {
   585  				case <-notifyRotate:
   586  				case <-logWatcher.WatchProducerGone():
   587  					return errDone
   588  				case <-logWatcher.WatchConsumerGone():
   589  					return errDone
   590  				}
   591  				if err := handleRotate(); err != nil {
   592  					return err
   593  				}
   594  				return nil
   595  			}
   596  			return errRetry
   597  		case err := <-fileWatcher.Errors():
   598  			logrus.Debugf("logger got error watching file: %v", err)
   599  			// Something happened, let's try and stay alive and create a new watcher
   600  			if retries <= 5 {
   601  				fileWatcher.Close()
   602  				fileWatcher, err = watchFile(name)
   603  				if err != nil {
   604  					return err
   605  				}
   606  				retries++
   607  				return errRetry
   608  			}
   609  			return err
   610  		case <-logWatcher.WatchProducerGone():
   611  			return errDone
   612  		case <-logWatcher.WatchConsumerGone():
   613  			return errDone
   614  		}
   615  	}
   616  
   617  	handleDecodeErr := func(err error) error {
   618  		if errors.Cause(err) != io.EOF {
   619  			return err
   620  		}
   621  
   622  		for {
   623  			err := waitRead()
   624  			if err == nil {
   625  				break
   626  			}
   627  			if err == errRetry {
   628  				continue
   629  			}
   630  			return err
   631  		}
   632  		return nil
   633  	}
   634  
   635  	// main loop
   636  	for {
   637  		msg, err := decodeLogLine()
   638  		if err != nil {
   639  			if err := handleDecodeErr(err); err != nil {
   640  				if err == errDone {
   641  					return
   642  				}
   643  				// we got an unrecoverable error, so return
   644  				logWatcher.Err <- err
   645  				return
   646  			}
   647  			// ready to try again
   648  			continue
   649  		}
   650  
   651  		retries = 0 // reset retries since we've succeeded
   652  		if !since.IsZero() && msg.Timestamp.Before(since) {
   653  			continue
   654  		}
   655  		if !until.IsZero() && msg.Timestamp.After(until) {
   656  			return
   657  		}
   658  		// send the message, unless the consumer is gone
   659  		select {
   660  		case logWatcher.Msg <- msg:
   661  		case <-logWatcher.WatchConsumerGone():
   662  			return
   663  		}
   664  	}
   665  }
   666  
   667  func watchFile(name string) (filenotify.FileWatcher, error) {
   668  	var fileWatcher filenotify.FileWatcher
   669  
   670  	if runtime.GOOS == "windows" {
   671  		// FileWatcher on Windows files is based on the syscall notifications which has an issue becuase of file caching.
   672  		// It is based on ReadDirectoryChangesW() which doesn't detect writes to the cache. It detects writes to disk only.
   673  		// Becuase of the OS lazy writing, we don't get notifications for file writes and thereby the watcher
   674  		// doesn't work. Hence for Windows we will use poll based notifier.
   675  		fileWatcher = filenotify.NewPollingWatcher()
   676  	} else {
   677  		var err error
   678  		fileWatcher, err = filenotify.New()
   679  		if err != nil {
   680  			return nil, err
   681  		}
   682  	}
   683  
   684  	logger := logrus.WithFields(logrus.Fields{
   685  		"module": "logger",
   686  		"file":   name,
   687  	})
   688  
   689  	if err := fileWatcher.Add(name); err != nil {
   690  		// we will retry using file poller.
   691  		logger.WithError(err).Warnf("falling back to file poller")
   692  		fileWatcher.Close()
   693  		fileWatcher = filenotify.NewPollingWatcher()
   694  
   695  		if err := fileWatcher.Add(name); err != nil {
   696  			fileWatcher.Close()
   697  			logger.WithError(err).Debugf("error watching log file for modifications")
   698  			return nil, err
   699  		}
   700  	}
   701  
   702  	return fileWatcher, nil
   703  }
   704  
   705  type wrappedReaderAt struct {
   706  	io.ReaderAt
   707  	pos int64
   708  }
   709  
   710  func (r *wrappedReaderAt) Read(p []byte) (int, error) {
   711  	n, err := r.ReaderAt.ReadAt(p, r.pos)
   712  	r.pos += int64(n)
   713  	return n, err
   714  }