github.com/containers/podman/v4@v4.9.4/libpod/logs/log.go (about)

     1  package logs
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/containers/podman/v4/libpod/logs/reversereader"
    13  	"github.com/nxadm/tail"
    14  	"github.com/sirupsen/logrus"
    15  )
    16  
    17  const (
    18  	// LogTimeFormat is the time format used in the log.
    19  	// It is a modified version of RFC3339Nano that guarantees trailing
    20  	// zeroes are not trimmed, taken from
    21  	// https://github.com/golang/go/issues/19635
    22  	LogTimeFormat = "2006-01-02T15:04:05.000000000Z07:00"
    23  
    24  	// PartialLogType signifies a log line that exceeded the buffer
    25  	// length and needed to spill into a new line
    26  	PartialLogType = "P"
    27  
    28  	// FullLogType signifies a log line is full
    29  	FullLogType = "F"
    30  
    31  	// ANSIEscapeResetCode is a code that resets all colors and text effects
    32  	ANSIEscapeResetCode = "\033[0m"
    33  )
    34  
    35  // LogOptions is the options you can use for logs
    36  type LogOptions struct {
    37  	Details    bool
    38  	Follow     bool
    39  	Since      time.Time
    40  	Until      time.Time
    41  	Tail       int64
    42  	Timestamps bool
    43  	Colors     bool
    44  	Multi      bool
    45  	WaitGroup  *sync.WaitGroup
    46  	UseName    bool
    47  }
    48  
    49  // LogLine describes the information for each line of a log
    50  type LogLine struct {
    51  	Device       string
    52  	ParseLogType string
    53  	Time         time.Time
    54  	Msg          string
    55  	CID          string
    56  	CName        string
    57  	ColorID      int64
    58  }
    59  
    60  // GetLogFile returns an hp tail for a container given options
    61  func GetLogFile(path string, options *LogOptions) (*tail.Tail, []*LogLine, error) {
    62  	var (
    63  		whence  int
    64  		err     error
    65  		logTail []*LogLine
    66  	)
    67  	// whence 0=origin, 2=end
    68  	if options.Tail >= 0 {
    69  		whence = 2
    70  	}
    71  	if options.Tail > 0 {
    72  		logTail, err = getTailLog(path, int(options.Tail))
    73  		if err != nil {
    74  			return nil, nil, err
    75  		}
    76  	}
    77  	seek := tail.SeekInfo{
    78  		Offset: 0,
    79  		Whence: whence,
    80  	}
    81  
    82  	t, err := tail.TailFile(path, tail.Config{MustExist: true, Poll: true, Follow: options.Follow, Location: &seek, Logger: tail.DiscardingLogger, ReOpen: options.Follow})
    83  	return t, logTail, err
    84  }
    85  
    86  func getTailLog(path string, tail int) ([]*LogLine, error) {
    87  	var (
    88  		nllCounter int
    89  		leftover   string
    90  		tailLog    []*LogLine
    91  		eof        bool
    92  	)
    93  	f, err := os.Open(path)
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  	defer f.Close()
    98  	rr, err := reversereader.NewReverseReader(f)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	first := true
   104  
   105  	for {
   106  		s, err := rr.Read()
   107  		if err != nil {
   108  			if !errors.Is(err, io.EOF) {
   109  				return nil, fmt.Errorf("reverse log read: %w", err)
   110  			}
   111  			eof = true
   112  		}
   113  
   114  		lines := strings.Split(s+leftover, "\n")
   115  		// we read a chunk of data, so make sure to read the line in inverse order
   116  		for i := len(lines) - 1; i > 0; i-- {
   117  			// ignore empty lines
   118  			if lines[i] == "" {
   119  				continue
   120  			}
   121  			nll, err := NewLogLine(lines[i])
   122  			if err != nil {
   123  				return nil, err
   124  			}
   125  			if !nll.Partial() || first {
   126  				nllCounter++
   127  				// Even if the last line is partial we need to count it as it will be printed as line.
   128  				// Because we read backwards the first line we read is the last line in the log.
   129  				first = false
   130  			}
   131  			// We explicitly need to check for more lines than tail because we have
   132  			// to read to next full line and must keep all partial lines
   133  			// https://github.com/containers/podman/issues/19545
   134  			if nllCounter > tail {
   135  				// because we add lines in the inverse order we must invert the slice in the end
   136  				return reverseLog(tailLog), nil
   137  			}
   138  			// only append after the return here because we do not want to include the next full line
   139  			tailLog = append(tailLog, nll)
   140  		}
   141  		leftover = lines[0]
   142  
   143  		// eof was reached
   144  		if eof {
   145  			// when we have still a line and do not have enough tail lines already
   146  			if leftover != "" && nllCounter < tail {
   147  				nll, err := NewLogLine(leftover)
   148  				if err != nil {
   149  					return nil, err
   150  				}
   151  				tailLog = append(tailLog, nll)
   152  			}
   153  			// because we add lines in the inverse order we must invert the slice in the end
   154  			return reverseLog(tailLog), nil
   155  		}
   156  	}
   157  }
   158  
   159  // reverseLog reverse the log line slice, needed for tail as we read lines backwards but still
   160  // need to print them in the correct order at the end  so use that helper for it.
   161  func reverseLog(s []*LogLine) []*LogLine {
   162  	for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
   163  		s[i], s[j] = s[j], s[i]
   164  	}
   165  	return s
   166  }
   167  
   168  // getColor returns an ANSI escape code for color based on the colorID
   169  func getColor(colorID int64) string {
   170  	colors := map[int64]string{
   171  		0: "\033[37m", // Light Gray
   172  		1: "\033[31m", // Red
   173  		2: "\033[33m", // Yellow
   174  		3: "\033[34m", // Blue
   175  		4: "\033[35m", // Magenta
   176  		5: "\033[36m", // Cyan
   177  		6: "\033[32m", // Green
   178  	}
   179  	return colors[colorID%int64(len(colors))]
   180  }
   181  
   182  func (l *LogLine) colorize(prefix string) string {
   183  	return getColor(l.ColorID) + prefix + l.Msg + ANSIEscapeResetCode
   184  }
   185  
   186  // String converts a log line to a string for output given whether a detail
   187  // bool is specified.
   188  func (l *LogLine) String(options *LogOptions) string {
   189  	var out string
   190  	if options.Multi {
   191  		if options.UseName {
   192  			out = l.CName + " "
   193  		} else {
   194  			cid := l.CID
   195  			if len(cid) > 12 {
   196  				cid = cid[:12]
   197  			}
   198  			out = fmt.Sprintf("%s ", cid)
   199  		}
   200  	}
   201  
   202  	if options.Timestamps {
   203  		out += fmt.Sprintf("%s ", l.Time.Format(LogTimeFormat))
   204  	}
   205  
   206  	if options.Colors {
   207  		out = l.colorize(out)
   208  	} else {
   209  		out += l.Msg
   210  	}
   211  
   212  	return out
   213  }
   214  
   215  // Since returns a bool as to whether a log line occurred after a given time
   216  func (l *LogLine) Since(since time.Time) bool {
   217  	return l.Time.After(since) || since.IsZero()
   218  }
   219  
   220  // Until returns a bool as to whether a log line occurred before a given time
   221  func (l *LogLine) Until(until time.Time) bool {
   222  	return l.Time.Before(until) || until.IsZero()
   223  }
   224  
   225  // NewLogLine creates a logLine struct from a container log string
   226  func NewLogLine(line string) (*LogLine, error) {
   227  	splitLine := strings.Split(line, " ")
   228  	if len(splitLine) < 4 {
   229  		return nil, fmt.Errorf("'%s' is not a valid container log line", line)
   230  	}
   231  	logTime, err := time.Parse(LogTimeFormat, splitLine[0])
   232  	if err != nil {
   233  		return nil, fmt.Errorf("unable to convert time %s from container log: %w", splitLine[0], err)
   234  	}
   235  	l := LogLine{
   236  		Time:         logTime,
   237  		Device:       splitLine[1],
   238  		ParseLogType: splitLine[2],
   239  		Msg:          strings.Join(splitLine[3:], " "),
   240  	}
   241  	return &l, nil
   242  }
   243  
   244  // Partial returns a bool if the log line is a partial log type
   245  func (l *LogLine) Partial() bool {
   246  	return l.ParseLogType == PartialLogType
   247  }
   248  
   249  func (l *LogLine) Write(stdout io.Writer, stderr io.Writer, logOpts *LogOptions) {
   250  	switch l.Device {
   251  	case "stdout":
   252  		if stdout != nil {
   253  			if l.Partial() {
   254  				fmt.Fprint(stdout, l.String(logOpts))
   255  			} else {
   256  				fmt.Fprintln(stdout, l.String(logOpts))
   257  			}
   258  		}
   259  	case "stderr":
   260  		if stderr != nil {
   261  			if l.Partial() {
   262  				fmt.Fprint(stderr, l.String(logOpts))
   263  			} else {
   264  				fmt.Fprintln(stderr, l.String(logOpts))
   265  			}
   266  		}
   267  	default:
   268  		// Warn the user if the device type does not match. Most likely the file is corrupted.
   269  		logrus.Warnf("Unknown Device type '%s' in log file from Container %s", l.Device, l.CID)
   270  	}
   271  }