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 }