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 }