github.com/hanks177/podman/v4@v4.1.3-0.20220613032544-16d90015bc83/libpod/logs/log.go (about)

     1  package logs
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/hanks177/podman/v4/libpod/logs/reversereader"
    12  	"github.com/nxadm/tail"
    13  	"github.com/pkg/errors"
    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  		nlls       []*LogLine
    89  		nllCounter int
    90  		leftover   string
    91  		partial    string
    92  		tailLog    []*LogLine
    93  	)
    94  	f, err := os.Open(path)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  	rr, err := reversereader.NewReverseReader(f)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	inputs := make(chan []string)
   104  	go func() {
   105  		for {
   106  			s, err := rr.Read()
   107  			if err != nil {
   108  				if errors.Cause(err) == io.EOF {
   109  					inputs <- []string{leftover}
   110  				} else {
   111  					logrus.Error(err)
   112  				}
   113  				close(inputs)
   114  				if err := f.Close(); err != nil {
   115  					logrus.Error(err)
   116  				}
   117  				break
   118  			}
   119  			line := strings.Split(s+leftover, "\n")
   120  			if len(line) > 1 {
   121  				inputs <- line[1:]
   122  			}
   123  			leftover = line[0]
   124  		}
   125  	}()
   126  
   127  	for i := range inputs {
   128  		// the incoming array is FIFO; we want FIFO so
   129  		// reverse the slice read order
   130  		for j := len(i) - 1; j >= 0; j-- {
   131  			// lines that are "" are junk
   132  			if len(i[j]) < 1 {
   133  				continue
   134  			}
   135  			// read the content in reverse and add each nll until we have the same
   136  			// number of F type messages as the desired tail
   137  			nll, err := NewLogLine(i[j])
   138  			if err != nil {
   139  				return nil, err
   140  			}
   141  			nlls = append(nlls, nll)
   142  			if !nll.Partial() {
   143  				nllCounter++
   144  			}
   145  		}
   146  		// if we have enough log lines, we can hangup
   147  		if nllCounter >= tail {
   148  			break
   149  		}
   150  	}
   151  
   152  	// re-assemble the log lines and trim (if needed) to the
   153  	// tail length
   154  	for _, nll := range nlls {
   155  		if nll.Partial() {
   156  			partial += nll.Msg
   157  		} else {
   158  			nll.Msg += partial
   159  			// prepend because we need to reverse the order again to FIFO
   160  			tailLog = append([]*LogLine{nll}, tailLog...)
   161  			partial = ""
   162  		}
   163  		if len(tailLog) == tail {
   164  			break
   165  		}
   166  	}
   167  	return tailLog, nil
   168  }
   169  
   170  // getColor returns a ANSI escape code for color based on the colorID
   171  func getColor(colorID int64) string {
   172  	colors := map[int64]string{
   173  		0: "\033[37m", // Light Gray
   174  		1: "\033[31m", // Red
   175  		2: "\033[33m", // Yellow
   176  		3: "\033[34m", // Blue
   177  		4: "\033[35m", // Magenta
   178  		5: "\033[36m", // Cyan
   179  		6: "\033[32m", // Green
   180  	}
   181  	return colors[colorID%int64(len(colors))]
   182  }
   183  
   184  func (l *LogLine) colorize(prefix string) string {
   185  	return getColor(l.ColorID) + prefix + l.Msg + ANSIEscapeResetCode
   186  }
   187  
   188  // String converts a log line to a string for output given whether a detail
   189  // bool is specified.
   190  func (l *LogLine) String(options *LogOptions) string {
   191  	var out string
   192  	if options.Multi {
   193  		if options.UseName {
   194  			out = l.CName + " "
   195  		} else {
   196  			cid := l.CID
   197  			if len(cid) > 12 {
   198  				cid = cid[:12]
   199  			}
   200  			out = fmt.Sprintf("%s ", cid)
   201  		}
   202  	}
   203  
   204  	if options.Timestamps {
   205  		out += fmt.Sprintf("%s ", l.Time.Format(LogTimeFormat))
   206  	}
   207  
   208  	if options.Colors {
   209  		out = l.colorize(out)
   210  	} else {
   211  		out += l.Msg
   212  	}
   213  
   214  	return out
   215  }
   216  
   217  // Since returns a bool as to whether a log line occurred after a given time
   218  func (l *LogLine) Since(since time.Time) bool {
   219  	return l.Time.After(since) || since.IsZero()
   220  }
   221  
   222  // Until returns a bool as to whether a log line occurred before a given time
   223  func (l *LogLine) Until(until time.Time) bool {
   224  	return l.Time.Before(until) || until.IsZero()
   225  }
   226  
   227  // NewLogLine creates a logLine struct from a container log string
   228  func NewLogLine(line string) (*LogLine, error) {
   229  	splitLine := strings.Split(line, " ")
   230  	if len(splitLine) < 4 {
   231  		return nil, errors.Errorf("'%s' is not a valid container log line", line)
   232  	}
   233  	logTime, err := time.Parse(LogTimeFormat, splitLine[0])
   234  	if err != nil {
   235  		return nil, errors.Wrapf(err, "unable to convert time %s from container log", splitLine[0])
   236  	}
   237  	l := LogLine{
   238  		Time:         logTime,
   239  		Device:       splitLine[1],
   240  		ParseLogType: splitLine[2],
   241  		Msg:          strings.Join(splitLine[3:], " "),
   242  	}
   243  	return &l, nil
   244  }
   245  
   246  // NewJournaldLogLine creates a LogLine from the specified line from journald.
   247  // Note that if withID is set, the first item of the message is considerred to
   248  // be the container ID and set as such.
   249  func NewJournaldLogLine(line string, withID bool) (*LogLine, error) {
   250  	splitLine := strings.Split(line, " ")
   251  	if len(splitLine) < 4 {
   252  		return nil, errors.Errorf("'%s' is not a valid container log line", line)
   253  	}
   254  	logTime, err := time.Parse(LogTimeFormat, splitLine[0])
   255  	if err != nil {
   256  		return nil, errors.Wrapf(err, "unable to convert time %s from container log", splitLine[0])
   257  	}
   258  	var msg, id string
   259  	if withID {
   260  		id = splitLine[3]
   261  		msg = strings.Join(splitLine[4:], " ")
   262  	} else {
   263  		msg = strings.Join(splitLine[3:], " ")
   264  		// NO ID
   265  	}
   266  	l := LogLine{
   267  		Time:         logTime,
   268  		Device:       splitLine[1],
   269  		ParseLogType: splitLine[2],
   270  		Msg:          msg,
   271  		CID:          id,
   272  	}
   273  	return &l, nil
   274  }
   275  
   276  // Partial returns a bool if the log line is a partial log type
   277  func (l *LogLine) Partial() bool {
   278  	return l.ParseLogType == PartialLogType
   279  }
   280  
   281  func (l *LogLine) Write(stdout io.Writer, stderr io.Writer, logOpts *LogOptions) {
   282  	switch l.Device {
   283  	case "stdout":
   284  		if stdout != nil {
   285  			if l.Partial() {
   286  				fmt.Fprint(stdout, l.String(logOpts))
   287  			} else {
   288  				fmt.Fprintln(stdout, l.String(logOpts))
   289  			}
   290  		}
   291  	case "stderr":
   292  		if stderr != nil {
   293  			if l.Partial() {
   294  				fmt.Fprint(stderr, l.String(logOpts))
   295  			} else {
   296  				fmt.Fprintln(stderr, l.String(logOpts))
   297  			}
   298  		}
   299  	default:
   300  		// Warn the user if the device type does not match. Most likely the file is corrupted.
   301  		logrus.Warnf("Unknown Device type '%s' in log file from Container %s", l.Device, l.CID)
   302  	}
   303  }