github.com/Heebron/moby@v0.0.0-20221111184709-6eab4f55faf7/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  	"io/fs"
    10  	"math"
    11  	"os"
    12  	"strconv"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/docker/docker/daemon/logger"
    17  	"github.com/docker/docker/pkg/pools"
    18  	"github.com/pkg/errors"
    19  	"github.com/sirupsen/logrus"
    20  )
    21  
    22  // rotateFileMetadata is a metadata of the gzip header of the compressed log file
    23  type rotateFileMetadata struct {
    24  	LastTime time.Time `json:"lastTime,omitempty"`
    25  }
    26  
    27  // LogFile is Logger implementation for default Docker logging.
    28  type LogFile struct {
    29  	mu       sync.Mutex // protects the logfile access
    30  	closed   chan struct{}
    31  	rotateMu sync.Mutex // blocks the next rotation until the current rotation is completed
    32  	// Lock out readers while performing a non-atomic sequence of filesystem
    33  	// operations (RLock: open, Lock: rename, delete).
    34  	//
    35  	// fsopMu should be locked for writing only while holding rotateMu.
    36  	fsopMu sync.RWMutex
    37  
    38  	// Logger configuration
    39  
    40  	capacity int64 // maximum size of each file
    41  	maxFiles int   // maximum number of files
    42  	compress bool  // whether old versions of log files are compressed
    43  	perms    os.FileMode
    44  
    45  	// Log file codec
    46  
    47  	createDecoder MakeDecoderFn
    48  	getTailReader GetTailReaderFunc
    49  
    50  	// Log reader state in a 1-buffered channel.
    51  	//
    52  	// Share memory by communicating: receive to acquire, send to release.
    53  	// The state struct is passed around by value so that use-after-send
    54  	// bugs cannot escalate to data races.
    55  	//
    56  	// A method which receives the state value takes ownership of it. The
    57  	// owner is responsible for either passing ownership along or sending
    58  	// the state back to the channel. By convention, the semantics of
    59  	// passing along ownership is expressed with function argument types.
    60  	// Methods which take a pointer *logReadState argument borrow the state,
    61  	// analogous to functions which require a lock to be held when calling.
    62  	// The caller retains ownership. Calling a method which which takes a
    63  	// value logFileState argument gives ownership to the callee.
    64  	read chan logReadState
    65  
    66  	decompress *sharedTempFileConverter
    67  
    68  	pos           logPos    // Current log file write position.
    69  	f             *os.File  // Current log file for writing.
    70  	lastTimestamp time.Time // timestamp of the last log
    71  }
    72  
    73  type logPos struct {
    74  	// Size of the current file.
    75  	size int64
    76  	// File rotation sequence number (modulo 2**16).
    77  	rotation uint16
    78  }
    79  
    80  type logReadState struct {
    81  	// Current log file position.
    82  	pos logPos
    83  	// Wait list to be notified of the value of pos next time it changes.
    84  	wait []chan<- logPos
    85  }
    86  
    87  // MakeDecoderFn creates a decoder
    88  type MakeDecoderFn func(rdr io.Reader) Decoder
    89  
    90  // Decoder is for reading logs
    91  // It is created by the log reader by calling the `MakeDecoderFunc`
    92  type Decoder interface {
    93  	// Reset resets the decoder
    94  	// Reset is called for certain events, such as log rotations
    95  	Reset(io.Reader)
    96  	// Decode decodes the next log messeage from the stream
    97  	Decode() (*logger.Message, error)
    98  	// Close signals to the decoder that it can release whatever resources it was using.
    99  	Close()
   100  }
   101  
   102  // SizeReaderAt defines a ReaderAt that also reports its size.
   103  // This is used for tailing log files.
   104  type SizeReaderAt interface {
   105  	io.Reader
   106  	io.ReaderAt
   107  	Size() int64
   108  }
   109  
   110  type readAtCloser interface {
   111  	io.ReaderAt
   112  	io.Closer
   113  }
   114  
   115  // GetTailReaderFunc is used to truncate a reader to only read as much as is required
   116  // in order to get the passed in number of log lines.
   117  // It returns the sectioned reader, the number of lines that the section reader
   118  // contains, and any error that occurs.
   119  type GetTailReaderFunc func(ctx context.Context, f SizeReaderAt, nLogLines int) (rdr io.Reader, nLines int, err error)
   120  
   121  // NewLogFile creates new LogFile
   122  func NewLogFile(logPath string, capacity int64, maxFiles int, compress bool, decodeFunc MakeDecoderFn, perms os.FileMode, getTailReader GetTailReaderFunc) (*LogFile, error) {
   123  	log, err := openFile(logPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, perms)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	size, err := log.Seek(0, io.SeekEnd)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  
   133  	pos := logPos{
   134  		size: size,
   135  		// Force a wraparound on first rotation to shake out any
   136  		// modular-arithmetic bugs.
   137  		rotation: math.MaxUint16,
   138  	}
   139  	st := make(chan logReadState, 1)
   140  	st <- logReadState{pos: pos}
   141  
   142  	return &LogFile{
   143  		f:             log,
   144  		read:          st,
   145  		pos:           pos,
   146  		closed:        make(chan struct{}),
   147  		capacity:      capacity,
   148  		maxFiles:      maxFiles,
   149  		compress:      compress,
   150  		decompress:    newSharedTempFileConverter(decompress),
   151  		createDecoder: decodeFunc,
   152  		perms:         perms,
   153  		getTailReader: getTailReader,
   154  	}, nil
   155  }
   156  
   157  // WriteLogEntry writes the provided log message to the current log file.
   158  // This may trigger a rotation event if the max file/capacity limits are hit.
   159  func (w *LogFile) WriteLogEntry(timestamp time.Time, marshalled []byte) error {
   160  	select {
   161  	case <-w.closed:
   162  		return errors.New("cannot write because the output file was closed")
   163  	default:
   164  	}
   165  	w.mu.Lock()
   166  	defer w.mu.Unlock()
   167  
   168  	// Are we due for a rotation?
   169  	if w.capacity != -1 && w.pos.size >= w.capacity {
   170  		if err := w.rotate(); err != nil {
   171  			return errors.Wrap(err, "error rotating log file")
   172  		}
   173  	}
   174  
   175  	n, err := w.f.Write(marshalled)
   176  	if err != nil {
   177  		return errors.Wrap(err, "error writing log entry")
   178  	}
   179  	w.pos.size += int64(n)
   180  	w.lastTimestamp = timestamp
   181  
   182  	// Notify any waiting readers that there is a new log entry to read.
   183  	st := <-w.read
   184  	defer func() { w.read <- st }()
   185  	st.pos = w.pos
   186  
   187  	for _, c := range st.wait {
   188  		c <- st.pos
   189  	}
   190  	// Optimization: retain the backing array to save a heap allocation next
   191  	// time a reader appends to the list.
   192  	if st.wait != nil {
   193  		st.wait = st.wait[:0]
   194  	}
   195  	return nil
   196  }
   197  
   198  func (w *LogFile) rotate() (retErr error) {
   199  	w.rotateMu.Lock()
   200  	noCompress := w.maxFiles <= 1 || !w.compress
   201  	defer func() {
   202  		// If we aren't going to run the goroutine to compress the log file, then we need to unlock in this function.
   203  		// Otherwise the lock will be released in the goroutine that handles compression.
   204  		if retErr != nil || noCompress {
   205  			w.rotateMu.Unlock()
   206  		}
   207  	}()
   208  
   209  	fname := w.f.Name()
   210  	if err := w.f.Close(); err != nil {
   211  		// if there was an error during a prior rotate, the file could already be closed
   212  		if !errors.Is(err, fs.ErrClosed) {
   213  			return errors.Wrap(err, "error closing file")
   214  		}
   215  	}
   216  
   217  	file, err := func() (*os.File, error) {
   218  		w.fsopMu.Lock()
   219  		defer w.fsopMu.Unlock()
   220  
   221  		if err := rotate(fname, w.maxFiles, w.compress); err != nil {
   222  			logrus.WithError(err).Warn("Error rotating log file, log data may have been lost")
   223  		} else {
   224  			// We may have readers working their way through the
   225  			// current log file so we can't truncate it. We need to
   226  			// start writing new logs to an empty file with the same
   227  			// name as the current one so we need to rotate the
   228  			// current file out of the way.
   229  			if w.maxFiles < 2 {
   230  				if err := unlink(fname); err != nil && !errors.Is(err, fs.ErrNotExist) {
   231  					logrus.WithError(err).Error("Error unlinking current log file")
   232  				}
   233  			} else {
   234  				if err := os.Rename(fname, fname+".1"); err != nil && !errors.Is(err, fs.ErrNotExist) {
   235  					logrus.WithError(err).Error("Error renaming current log file")
   236  				}
   237  			}
   238  		}
   239  
   240  		// Notwithstanding the above, open with the truncate flag anyway
   241  		// in case rotation didn't work out as planned.
   242  		return openFile(fname, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, w.perms)
   243  	}()
   244  	if err != nil {
   245  		return err
   246  	}
   247  	w.f = file
   248  	w.pos = logPos{rotation: w.pos.rotation + 1}
   249  
   250  	if noCompress {
   251  		return nil
   252  	}
   253  
   254  	ts := w.lastTimestamp
   255  	go func() {
   256  		defer w.rotateMu.Unlock()
   257  		// No need to hold fsopMu as at no point will the filesystem be
   258  		// in a state which would cause problems for readers. Opening
   259  		// the uncompressed file is tried first, falling back to the
   260  		// compressed one. compressFile only deletes the uncompressed
   261  		// file once the compressed one is fully written out, so at no
   262  		// point during the compression process will a reader fail to
   263  		// open a complete copy of the file.
   264  		if err := compressFile(fname+".1", ts); err != nil {
   265  			logrus.WithError(err).Error("Error compressing log file after rotation")
   266  		}
   267  	}()
   268  
   269  	return nil
   270  }
   271  
   272  func rotate(name string, maxFiles int, compress bool) error {
   273  	if maxFiles < 2 {
   274  		return nil
   275  	}
   276  
   277  	var extension string
   278  	if compress {
   279  		extension = ".gz"
   280  	}
   281  
   282  	lastFile := fmt.Sprintf("%s.%d%s", name, maxFiles-1, extension)
   283  	err := unlink(lastFile)
   284  	if err != nil && !errors.Is(err, fs.ErrNotExist) {
   285  		return errors.Wrap(err, "error removing oldest log file")
   286  	}
   287  
   288  	for i := maxFiles - 1; i > 1; i-- {
   289  		toPath := name + "." + strconv.Itoa(i) + extension
   290  		fromPath := name + "." + strconv.Itoa(i-1) + extension
   291  		err := os.Rename(fromPath, toPath)
   292  		logrus.WithError(err).WithField("source", fromPath).WithField("target", toPath).Trace("Rotating log file")
   293  		if err != nil && !errors.Is(err, fs.ErrNotExist) {
   294  			return err
   295  		}
   296  	}
   297  
   298  	return nil
   299  }
   300  
   301  func compressFile(fileName string, lastTimestamp time.Time) (retErr error) {
   302  	file, err := open(fileName)
   303  	if err != nil {
   304  		if errors.Is(err, fs.ErrNotExist) {
   305  			logrus.WithField("file", fileName).WithError(err).Debug("Could not open log file to compress")
   306  			return nil
   307  		}
   308  		return errors.Wrap(err, "failed to open log file")
   309  	}
   310  	defer func() {
   311  		file.Close()
   312  		if retErr == nil {
   313  			err := unlink(fileName)
   314  			if err != nil && !errors.Is(err, fs.ErrNotExist) {
   315  				retErr = errors.Wrap(err, "failed to remove source log file")
   316  			}
   317  		}
   318  	}()
   319  
   320  	outFile, err := openFile(fileName+".gz", os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0640)
   321  	if err != nil {
   322  		return errors.Wrap(err, "failed to open or create gzip log file")
   323  	}
   324  	defer func() {
   325  		outFile.Close()
   326  		if retErr != nil {
   327  			if err := unlink(fileName + ".gz"); err != nil && !errors.Is(err, fs.ErrNotExist) {
   328  				logrus.WithError(err).Error("Error cleaning up after failed log compression")
   329  			}
   330  		}
   331  	}()
   332  
   333  	compressWriter := gzip.NewWriter(outFile)
   334  	defer compressWriter.Close()
   335  
   336  	// Add the last log entry timestamp to the gzip header
   337  	extra := rotateFileMetadata{}
   338  	extra.LastTime = lastTimestamp
   339  	compressWriter.Header.Extra, err = json.Marshal(&extra)
   340  	if err != nil {
   341  		// Here log the error only and don't return since this is just an optimization.
   342  		logrus.Warningf("Failed to marshal gzip header as JSON: %v", err)
   343  	}
   344  
   345  	_, err = pools.Copy(compressWriter, file)
   346  	if err != nil {
   347  		return errors.Wrapf(err, "error compressing log file %s", fileName)
   348  	}
   349  
   350  	return nil
   351  }
   352  
   353  // MaxFiles return maximum number of files
   354  func (w *LogFile) MaxFiles() int {
   355  	return w.maxFiles
   356  }
   357  
   358  // Close closes underlying file and signals all readers to stop.
   359  func (w *LogFile) Close() error {
   360  	w.mu.Lock()
   361  	defer w.mu.Unlock()
   362  	select {
   363  	case <-w.closed:
   364  		return nil
   365  	default:
   366  	}
   367  	if err := w.f.Close(); err != nil && !errors.Is(err, fs.ErrClosed) {
   368  		return err
   369  	}
   370  	close(w.closed)
   371  	// Wait until any in-progress rotation is complete.
   372  	w.rotateMu.Lock()
   373  	w.rotateMu.Unlock() //nolint:staticcheck
   374  	return nil
   375  }
   376  
   377  // ReadLogs decodes entries from log files.
   378  //
   379  // It is the caller's responsibility to call ConsumerGone on the LogWatcher.
   380  func (w *LogFile) ReadLogs(config logger.ReadConfig) *logger.LogWatcher {
   381  	watcher := logger.NewLogWatcher()
   382  	// Lock out filesystem operations so that we can capture the read
   383  	// position and atomically open the corresponding log file, without the
   384  	// file getting rotated out from under us.
   385  	w.fsopMu.RLock()
   386  	// Capture the read position synchronously to ensure that we start
   387  	// following from the last entry logged before ReadLogs was called,
   388  	// which is required for flake-free unit testing.
   389  	st := <-w.read
   390  	pos := st.pos
   391  	w.read <- st
   392  	go w.readLogsLocked(pos, config, watcher)
   393  	return watcher
   394  }
   395  
   396  // readLogsLocked is the bulk of the implementation of ReadLogs.
   397  //
   398  // w.fsopMu must be locked for reading when calling this method.
   399  // w.fsopMu.RUnlock() is called before returning.
   400  func (w *LogFile) readLogsLocked(currentPos logPos, config logger.ReadConfig, watcher *logger.LogWatcher) {
   401  	defer close(watcher.Msg)
   402  
   403  	currentFile, err := open(w.f.Name())
   404  	if err != nil {
   405  		watcher.Err <- err
   406  		return
   407  	}
   408  	defer currentFile.Close()
   409  
   410  	dec := w.createDecoder(nil)
   411  	defer dec.Close()
   412  
   413  	currentChunk := io.NewSectionReader(currentFile, 0, currentPos.size)
   414  	fwd := newForwarder(config)
   415  
   416  	if config.Tail != 0 {
   417  		// TODO(@cpuguy83): Instead of opening every file, only get the files which
   418  		// are needed to tail.
   419  		// This is especially costly when compression is enabled.
   420  		files, err := w.openRotatedFiles(config)
   421  		if err != nil {
   422  			watcher.Err <- err
   423  			return
   424  		}
   425  
   426  		closeFiles := func() {
   427  			for _, f := range files {
   428  				f.Close()
   429  			}
   430  		}
   431  
   432  		readers := make([]SizeReaderAt, 0, len(files)+1)
   433  		for _, f := range files {
   434  			switch ff := f.(type) {
   435  			case SizeReaderAt:
   436  				readers = append(readers, ff)
   437  			case interface{ Stat() (fs.FileInfo, error) }:
   438  				stat, err := ff.Stat()
   439  				if err != nil {
   440  					watcher.Err <- errors.Wrap(err, "error reading size of rotated file")
   441  					closeFiles()
   442  					return
   443  				}
   444  				readers = append(readers, io.NewSectionReader(f, 0, stat.Size()))
   445  			default:
   446  				panic(fmt.Errorf("rotated file value %#v (%[1]T) has neither Size() nor Stat() methods", f))
   447  			}
   448  		}
   449  		if currentChunk.Size() > 0 {
   450  			readers = append(readers, currentChunk)
   451  		}
   452  
   453  		ok := tailFiles(readers, watcher, dec, w.getTailReader, config.Tail, fwd)
   454  		closeFiles()
   455  		if !ok {
   456  			return
   457  		}
   458  	} else {
   459  		w.fsopMu.RUnlock()
   460  	}
   461  
   462  	if !config.Follow {
   463  		return
   464  	}
   465  
   466  	(&follow{
   467  		LogFile:   w,
   468  		Watcher:   watcher,
   469  		Decoder:   dec,
   470  		Forwarder: fwd,
   471  	}).Do(currentFile, currentPos)
   472  }
   473  
   474  // openRotatedFiles returns a slice of files open for reading, in order from
   475  // oldest to newest, and calls w.fsopMu.RUnlock() before returning.
   476  //
   477  // This method must only be called with w.fsopMu locked for reading.
   478  func (w *LogFile) openRotatedFiles(config logger.ReadConfig) (files []readAtCloser, err error) {
   479  	type rotatedFile struct {
   480  		f          *os.File
   481  		compressed bool
   482  	}
   483  
   484  	var q []rotatedFile
   485  	defer func() {
   486  		if err != nil {
   487  			for _, qq := range q {
   488  				qq.f.Close()
   489  			}
   490  			for _, f := range files {
   491  				f.Close()
   492  			}
   493  		}
   494  	}()
   495  
   496  	q, err = func() (q []rotatedFile, err error) {
   497  		defer w.fsopMu.RUnlock()
   498  
   499  		q = make([]rotatedFile, 0, w.maxFiles)
   500  		for i := w.maxFiles; i > 1; i-- {
   501  			var f rotatedFile
   502  			f.f, err = open(fmt.Sprintf("%s.%d", w.f.Name(), i-1))
   503  			if err != nil {
   504  				if !errors.Is(err, fs.ErrNotExist) {
   505  					return nil, errors.Wrap(err, "error opening rotated log file")
   506  				}
   507  				f.compressed = true
   508  				f.f, err = open(fmt.Sprintf("%s.%d.gz", w.f.Name(), i-1))
   509  				if err != nil {
   510  					if !errors.Is(err, fs.ErrNotExist) {
   511  						return nil, errors.Wrap(err, "error opening file for decompression")
   512  					}
   513  					continue
   514  				}
   515  			}
   516  			q = append(q, f)
   517  		}
   518  		return q, nil
   519  	}()
   520  	if err != nil {
   521  		return nil, err
   522  	}
   523  
   524  	for len(q) > 0 {
   525  		qq := q[0]
   526  		q = q[1:]
   527  		if qq.compressed {
   528  			defer qq.f.Close()
   529  			f, err := w.maybeDecompressFile(qq.f, config)
   530  			if err != nil {
   531  				return nil, err
   532  			}
   533  			if f != nil {
   534  				// The log before `config.Since` does not need to read
   535  				files = append(files, f)
   536  			}
   537  		} else {
   538  			files = append(files, qq.f)
   539  		}
   540  	}
   541  	return files, nil
   542  }
   543  
   544  func (w *LogFile) maybeDecompressFile(cf *os.File, config logger.ReadConfig) (readAtCloser, error) {
   545  	rc, err := gzip.NewReader(cf)
   546  	if err != nil {
   547  		return nil, errors.Wrap(err, "error making gzip reader for compressed log file")
   548  	}
   549  	defer rc.Close()
   550  
   551  	// Extract the last log entry timestramp from the gzip header
   552  	extra := &rotateFileMetadata{}
   553  	err = json.Unmarshal(rc.Header.Extra, extra)
   554  	if err == nil && !extra.LastTime.IsZero() && extra.LastTime.Before(config.Since) {
   555  		return nil, nil
   556  	}
   557  	tmpf, err := w.decompress.Do(cf)
   558  	return tmpf, errors.Wrap(err, "error decompressing log file")
   559  }
   560  
   561  func decompress(dst io.WriteSeeker, src io.ReadSeeker) error {
   562  	if _, err := src.Seek(0, io.SeekStart); err != nil {
   563  		return err
   564  	}
   565  	rc, err := gzip.NewReader(src)
   566  	if err != nil {
   567  		return err
   568  	}
   569  	_, err = pools.Copy(dst, rc)
   570  	if err != nil {
   571  		return err
   572  	}
   573  	return rc.Close()
   574  }
   575  
   576  func tailFiles(files []SizeReaderAt, watcher *logger.LogWatcher, dec Decoder, getTailReader GetTailReaderFunc, nLines int, fwd *forwarder) (cont bool) {
   577  	ctx, cancel := context.WithCancel(context.Background())
   578  	defer cancel()
   579  
   580  	cont = true
   581  	// TODO(@cpuguy83): we should plumb a context through instead of dealing with `WatchClose()` here.
   582  	go func() {
   583  		select {
   584  		case <-ctx.Done():
   585  		case <-watcher.WatchConsumerGone():
   586  			cancel()
   587  		}
   588  	}()
   589  
   590  	readers := make([]io.Reader, 0, len(files))
   591  
   592  	if nLines > 0 {
   593  		for i := len(files) - 1; i >= 0 && nLines > 0; i-- {
   594  			tail, n, err := getTailReader(ctx, files[i], nLines)
   595  			if err != nil {
   596  				watcher.Err <- errors.Wrap(err, "error finding file position to start log tailing")
   597  				return false
   598  			}
   599  			nLines -= n
   600  			readers = append([]io.Reader{tail}, readers...)
   601  		}
   602  	} else {
   603  		for _, r := range files {
   604  			readers = append(readers, r)
   605  		}
   606  	}
   607  
   608  	rdr := io.MultiReader(readers...)
   609  	dec.Reset(rdr)
   610  	return fwd.Do(watcher, dec)
   611  }
   612  
   613  type forwarder struct {
   614  	since, until time.Time
   615  }
   616  
   617  func newForwarder(config logger.ReadConfig) *forwarder {
   618  	return &forwarder{since: config.Since, until: config.Until}
   619  }
   620  
   621  // Do reads log messages from dec and sends the messages matching the filter
   622  // conditions to watcher. Do returns cont=true iff it has read all messages from
   623  // dec without encountering a message with a timestamp which is after the
   624  // configured until time.
   625  func (fwd *forwarder) Do(watcher *logger.LogWatcher, dec Decoder) (cont bool) {
   626  	for {
   627  		msg, err := dec.Decode()
   628  		if err != nil {
   629  			if errors.Is(err, io.EOF) {
   630  				return true
   631  			}
   632  			watcher.Err <- err
   633  			return false
   634  		}
   635  		if !fwd.since.IsZero() {
   636  			if msg.Timestamp.Before(fwd.since) {
   637  				continue
   638  			}
   639  			// We've found our first message with a timestamp >= since. As message
   640  			// timestamps might not be monotonic, we need to skip the since check for all
   641  			// subsequent messages so we do not filter out later messages which happen to
   642  			// have timestamps before since.
   643  			fwd.since = time.Time{}
   644  		}
   645  		if !fwd.until.IsZero() && msg.Timestamp.After(fwd.until) {
   646  			return false
   647  		}
   648  		select {
   649  		case <-watcher.WatchConsumerGone():
   650  			return false
   651  		case watcher.Msg <- msg:
   652  		}
   653  	}
   654  }