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