github.com/containers/podman/v4@v4.9.4/libpod/events/logfile.go (about)

     1  //go:build linux || freebsd
     2  // +build linux freebsd
     3  
     4  package events
     5  
     6  import (
     7  	"bufio"
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"path"
    14  	"path/filepath"
    15  	"time"
    16  
    17  	"github.com/containers/podman/v4/pkg/util"
    18  	"github.com/containers/storage/pkg/lockfile"
    19  	"github.com/nxadm/tail"
    20  	"github.com/sirupsen/logrus"
    21  	"golang.org/x/sys/unix"
    22  )
    23  
    24  // EventLogFile is the structure for event writing to a logfile. It contains the eventer
    25  // options and the event itself.  Methods for reading and writing are also defined from it.
    26  type EventLogFile struct {
    27  	options EventerOptions
    28  }
    29  
    30  // newLogFileEventer creates a new EventLogFile eventer
    31  func newLogFileEventer(options EventerOptions) (*EventLogFile, error) {
    32  	// Create events log dir
    33  	if err := os.MkdirAll(filepath.Dir(options.LogFilePath), 0700); err != nil {
    34  		return nil, fmt.Errorf("creating events dirs: %w", err)
    35  	}
    36  	// We have to make sure the file is created otherwise reading events will hang.
    37  	// https://github.com/containers/podman/issues/15688
    38  	fd, err := os.OpenFile(options.LogFilePath, os.O_RDONLY|os.O_CREATE, 0700)
    39  	if err != nil {
    40  		return nil, fmt.Errorf("failed to create event log file: %w", err)
    41  	}
    42  	return &EventLogFile{options: options}, fd.Close()
    43  }
    44  
    45  // Writes to the log file
    46  func (e EventLogFile) Write(ee Event) error {
    47  	// We need to lock events file
    48  	lock, err := lockfile.GetLockFile(e.options.LogFilePath + ".lock")
    49  	if err != nil {
    50  		return err
    51  	}
    52  	lock.Lock()
    53  	defer lock.Unlock()
    54  
    55  	eventJSONString, err := ee.ToJSONString()
    56  	if err != nil {
    57  		return err
    58  	}
    59  
    60  	if _, err := rotateLog(e.options.LogFilePath, eventJSONString, e.options.LogFileMaxSize); err != nil {
    61  		return err
    62  	}
    63  
    64  	return e.writeString(eventJSONString)
    65  }
    66  
    67  func (e EventLogFile) writeString(s string) error {
    68  	f, err := os.OpenFile(e.options.LogFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0700)
    69  	if err != nil {
    70  		return err
    71  	}
    72  	return writeToFile(s, f)
    73  }
    74  
    75  func writeToFile(s string, f *os.File) error {
    76  	_, err := f.WriteString(s + "\n")
    77  	return err
    78  }
    79  
    80  func (e EventLogFile) getTail(options ReadOptions) (*tail.Tail, error) {
    81  	seek := tail.SeekInfo{Offset: 0, Whence: io.SeekEnd}
    82  	if options.FromStart || !options.Stream {
    83  		seek.Whence = 0
    84  	}
    85  	stream := options.Stream
    86  	return tail.TailFile(e.options.LogFilePath, tail.Config{ReOpen: stream, Follow: stream, Location: &seek, Logger: tail.DiscardingLogger, Poll: true})
    87  }
    88  
    89  func (e EventLogFile) readRotateEvent(event *Event) (begin bool, end bool, err error) {
    90  	if event.Status != Rotate {
    91  		return
    92  	}
    93  	if event.Details.Attributes == nil {
    94  		// may be an old event before storing attributes in the rotate event
    95  		return
    96  	}
    97  	switch event.Details.Attributes[rotateEventAttribute] {
    98  	case rotateEventBegin:
    99  		begin = true
   100  		return
   101  	case rotateEventEnd:
   102  		end = true
   103  		return
   104  	default:
   105  		err = fmt.Errorf("unknown rotate-event attribute %q", event.Details.Attributes[rotateEventAttribute])
   106  		return
   107  	}
   108  }
   109  
   110  // Reads from the log file
   111  func (e EventLogFile) Read(ctx context.Context, options ReadOptions) error {
   112  	defer close(options.EventChannel)
   113  	filterMap, err := generateEventFilters(options.Filters, options.Since, options.Until)
   114  	if err != nil {
   115  		return fmt.Errorf("failed to parse event filters: %w", err)
   116  	}
   117  	t, err := e.getTail(options)
   118  	if err != nil {
   119  		return err
   120  	}
   121  	if len(options.Until) > 0 {
   122  		untilTime, err := util.ParseInputTime(options.Until, false)
   123  		if err != nil {
   124  			return err
   125  		}
   126  		go func() {
   127  			time.Sleep(time.Until(untilTime))
   128  			if err := t.Stop(); err != nil {
   129  				logrus.Errorf("Stopping logger: %v", err)
   130  			}
   131  		}()
   132  	}
   133  	logrus.Debugf("Reading events from file %q", e.options.LogFilePath)
   134  
   135  	// Get the time *before* starting to read.  Comparing the timestamps
   136  	// with events avoids returning events more than once after a log-file
   137  	// rotation.
   138  	readTime, err := func() (time.Time, error) {
   139  		// We need to lock events file
   140  		lock, err := lockfile.GetLockFile(e.options.LogFilePath + ".lock")
   141  		if err != nil {
   142  			return time.Time{}, err
   143  		}
   144  		lock.Lock()
   145  		defer lock.Unlock()
   146  		return time.Now(), nil
   147  	}()
   148  	if err != nil {
   149  		return err
   150  	}
   151  
   152  	var line *tail.Line
   153  	var ok bool
   154  	var skipRotate bool
   155  	for {
   156  		select {
   157  		case <-ctx.Done():
   158  			// the consumer has cancelled
   159  			t.Kill(errors.New("hangup by client"))
   160  			return nil
   161  		case line, ok = <-t.Lines:
   162  			if !ok {
   163  				// channel was closed
   164  				return nil
   165  			}
   166  			// fallthrough
   167  		}
   168  
   169  		event, err := newEventFromJSONString(line.Text)
   170  		if err != nil {
   171  			return err
   172  		}
   173  		switch event.Type {
   174  		case Image, Volume, Pod, Container, Network:
   175  			//	no-op
   176  		case System:
   177  			begin, end, err := e.readRotateEvent(event)
   178  			if err != nil {
   179  				return err
   180  			}
   181  			if begin && event.Time.After(readTime) {
   182  				// If the rotation event happened _after_ we
   183  				// started reading, we need to ignore/skip
   184  				// subsequent event until the end of the
   185  				// rotation.
   186  				skipRotate = true
   187  				logrus.Debugf("Skipping already read events after log-file rotation: %v", event)
   188  			} else if end {
   189  				// This rotate event
   190  				skipRotate = false
   191  			}
   192  		default:
   193  			return fmt.Errorf("event type %s is not valid in %s", event.Type.String(), e.options.LogFilePath)
   194  		}
   195  		if skipRotate {
   196  			continue
   197  		}
   198  		if applyFilters(event, filterMap) {
   199  			options.EventChannel <- event
   200  		}
   201  	}
   202  }
   203  
   204  // String returns a string representation of the logger
   205  func (e EventLogFile) String() string {
   206  	return LogFile.String()
   207  }
   208  
   209  const (
   210  	rotateEventAttribute = "io.podman.event.rotate"
   211  	rotateEventBegin     = "begin"
   212  	rotateEventEnd       = "end"
   213  )
   214  
   215  func writeRotateEvent(f *os.File, logFilePath string, begin bool) error {
   216  	rEvent := NewEvent(Rotate)
   217  	rEvent.Type = System
   218  	rEvent.Name = logFilePath
   219  	rEvent.Attributes = make(map[string]string)
   220  	if begin {
   221  		rEvent.Attributes[rotateEventAttribute] = rotateEventBegin
   222  	} else {
   223  		rEvent.Attributes[rotateEventAttribute] = rotateEventEnd
   224  	}
   225  	rotateJSONString, err := rEvent.ToJSONString()
   226  	if err != nil {
   227  		return err
   228  	}
   229  	return writeToFile(rotateJSONString, f)
   230  }
   231  
   232  // Rotates the log file if the log file size and content exceeds limit
   233  func rotateLog(logfile string, content string, limit uint64) (bool, error) {
   234  	needsRotation, err := logNeedsRotation(logfile, content, limit)
   235  	if err != nil || !needsRotation {
   236  		return false, err
   237  	}
   238  	if err := truncate(logfile); err != nil {
   239  		return false, err
   240  	}
   241  	return true, nil
   242  }
   243  
   244  // logNeedsRotation return true if the log file needs to be rotated.
   245  func logNeedsRotation(logfile string, content string, limit uint64) (bool, error) {
   246  	if limit == 0 {
   247  		return false, nil
   248  	}
   249  	file, err := os.Stat(logfile)
   250  	if err != nil {
   251  		if errors.Is(err, os.ErrNotExist) {
   252  			// The logfile does not exist yet.
   253  			return false, nil
   254  		}
   255  		return false, err
   256  	}
   257  	var filesize = uint64(file.Size())
   258  	var contentsize = uint64(len([]rune(content)))
   259  	if filesize+contentsize < limit {
   260  		return false, nil
   261  	}
   262  
   263  	return true, nil
   264  }
   265  
   266  // Truncates the log file and saves 50% of content to new log file
   267  func truncate(filePath string) error {
   268  	orig, err := os.Open(filePath)
   269  	if err != nil {
   270  		return err
   271  	}
   272  	defer orig.Close()
   273  
   274  	origFinfo, err := orig.Stat()
   275  	if err != nil {
   276  		return err
   277  	}
   278  
   279  	size := origFinfo.Size()
   280  	threshold := size / 2
   281  
   282  	tmp, err := os.CreateTemp(path.Dir(filePath), "")
   283  	if err != nil {
   284  		// Retry in /tmp in case creating a tmp file in the same
   285  		// directory has failed.
   286  		tmp, err = os.CreateTemp("", "")
   287  		if err != nil {
   288  			return err
   289  		}
   290  	}
   291  	defer tmp.Close()
   292  
   293  	// Jump directly to the threshold, drop the first line and copy the remainder
   294  	if _, err := orig.Seek(threshold, 0); err != nil {
   295  		return err
   296  	}
   297  	reader := bufio.NewReader(orig)
   298  	if _, err := reader.ReadString('\n'); err != nil {
   299  		if !errors.Is(err, io.EOF) {
   300  			return err
   301  		}
   302  	}
   303  
   304  	if err := writeRotateEvent(tmp, filePath, true); err != nil {
   305  		return fmt.Errorf("writing rotation event begin marker: %w", err)
   306  	}
   307  	if _, err := reader.WriteTo(tmp); err != nil {
   308  		return fmt.Errorf("writing truncated contents: %w", err)
   309  	}
   310  	if err := writeRotateEvent(tmp, filePath, false); err != nil {
   311  		return fmt.Errorf("writing rotation event end marker: %w", err)
   312  	}
   313  
   314  	if err := renameLog(tmp.Name(), filePath); err != nil {
   315  		return fmt.Errorf("writing back %s to %s: %w", tmp.Name(), filePath, err)
   316  	}
   317  
   318  	return nil
   319  }
   320  
   321  // Renames from, to
   322  func renameLog(from, to string) error {
   323  	err := os.Rename(from, to)
   324  	if err == nil {
   325  		return nil
   326  	}
   327  
   328  	if !errors.Is(err, unix.EXDEV) {
   329  		return err
   330  	}
   331  
   332  	// Files are not on the same partition, so we need to copy them the
   333  	// hard way.
   334  	fFrom, err := os.Open(from)
   335  	if err != nil {
   336  		return err
   337  	}
   338  	defer fFrom.Close()
   339  
   340  	// Remove the old file to make sure we're not truncating current
   341  	// readers.
   342  	if err := os.Remove(to); err != nil {
   343  		return fmt.Errorf("recreating file %s: %w", to, err)
   344  	}
   345  
   346  	fTo, err := os.Create(to)
   347  	if err != nil {
   348  		return err
   349  	}
   350  	defer fTo.Close()
   351  
   352  	if _, err := io.Copy(fTo, fFrom); err != nil {
   353  		return fmt.Errorf("writing back from temporary file: %w", err)
   354  	}
   355  
   356  	if err := os.Remove(from); err != nil {
   357  		return fmt.Errorf("removing temporary file: %w", err)
   358  	}
   359  
   360  	return nil
   361  }