github.com/containers/libpod@v1.9.4-0.20220419124438-4284fd425507/pkg/logs/logs.go (about) 1 /* 2 This package picks up CRI parsing and writer for the logs from the kubernetes 3 logs package. These two bits have been modified to fit the requirements of libpod. 4 5 Copyright 2017 The Kubernetes Authors. 6 7 Licensed under the Apache License, Version 2.0 (the "License"); 8 you may not use this file except in compliance with the License. 9 You may obtain a copy of the License at 10 11 http://www.apache.org/licenses/LICENSE-2.0 12 13 Unless required by applicable law or agreed to in writing, software 14 distributed under the License is distributed on an "AS IS" BASIS, 15 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 See the License for the specific language governing permissions and 17 limitations under the License. 18 */ 19 20 package logs 21 22 import ( 23 "bufio" 24 "bytes" 25 "fmt" 26 "io" 27 "math" 28 "os" 29 "time" 30 31 "github.com/containers/libpod/libpod" 32 "github.com/containers/libpod/libpod/define" 33 "github.com/containers/libpod/pkg/errorhandling" 34 "github.com/pkg/errors" 35 "github.com/sirupsen/logrus" 36 ) 37 38 const ( 39 // timeFormat is the time format used in the log. 40 // It is a modified version of RFC3339Nano that guarantees trailing 41 // zeroes are not trimmed, taken from 42 // https://github.com/golang/go/issues/19635 43 timeFormat = "2006-01-02T15:04:05.000000000Z07:00" 44 ) 45 46 // LogStreamType is the type of the stream in CRI container log. 47 type LogStreamType string 48 49 const ( 50 // Stdout is the stream type for stdout. 51 Stdout LogStreamType = "stdout" 52 // Stderr is the stream type for stderr. 53 Stderr LogStreamType = "stderr" 54 ) 55 56 // LogTag is the tag of a log line in CRI container log. 57 // Currently defined log tags: 58 // * First tag: Partial/Full - P/F. 59 // The field in the container log format can be extended to include multiple 60 // tags by using a delimiter, but changes should be rare. 61 type LogTag string 62 63 const ( 64 // LogTagPartial means the line is part of multiple lines. 65 LogTagPartial LogTag = "P" 66 // LogTagFull means the line is a single full line or the end of multiple lines. 67 LogTagFull LogTag = "F" 68 // LogTagDelimiter is the delimiter for different log tags. 69 LogTagDelimiter = ":" 70 ) 71 72 var ( 73 // eol is the end-of-line sign in the log. 74 eol = []byte{'\n'} 75 // delimiter is the delimiter for timestamp and stream type in log line. 76 delimiter = []byte{' '} 77 // tagDelimiter is the delimiter for log tags. 78 tagDelimiter = []byte(LogTagDelimiter) 79 ) 80 81 // logMessage is the CRI internal log type. 82 type logMessage struct { 83 timestamp time.Time 84 stream LogStreamType 85 log []byte 86 } 87 88 // LogOptions is the options you can use for logs 89 type LogOptions struct { 90 Details bool 91 Follow bool 92 Since time.Time 93 Tail uint64 94 Timestamps bool 95 bytes int64 96 } 97 98 // reset resets the log to nil. 99 func (l *logMessage) reset() { 100 l.timestamp = time.Time{} 101 l.stream = "" 102 l.log = nil 103 } 104 105 // parseCRILog parses logs in CRI log format. CRI Log format example: 106 // 2016-10-06T00:17:09.669794202Z stdout P log content 1 107 // 2016-10-06T00:17:09.669794203Z stderr F log content 2 108 func parseCRILog(log []byte, msg *logMessage) error { 109 var err error 110 // Parse timestamp 111 idx := bytes.Index(log, delimiter) 112 if idx < 0 { 113 return fmt.Errorf("timestamp is not found") 114 } 115 msg.timestamp, err = time.Parse(timeFormat, string(log[:idx])) 116 if err != nil { 117 return fmt.Errorf("unexpected timestamp format %q: %v", timeFormat, err) 118 } 119 120 // Parse stream type 121 log = log[idx+1:] 122 idx = bytes.Index(log, delimiter) 123 if idx < 0 { 124 return fmt.Errorf("stream type is not found") 125 } 126 msg.stream = LogStreamType(log[:idx]) 127 if msg.stream != Stdout && msg.stream != Stderr { 128 return fmt.Errorf("unexpected stream type %q", msg.stream) 129 } 130 131 // Parse log tag 132 log = log[idx+1:] 133 idx = bytes.Index(log, delimiter) 134 if idx < 0 { 135 return fmt.Errorf("log tag is not found") 136 } 137 // Keep this forward compatible. 138 tags := bytes.Split(log[:idx], tagDelimiter) 139 partial := LogTag(tags[0]) == LogTagPartial 140 // Trim the tailing new line if this is a partial line. 141 if partial && len(log) > 0 && log[len(log)-1] == '\n' { 142 log = log[:len(log)-1] 143 } 144 145 // Get log content 146 msg.log = log[idx+1:] 147 148 return nil 149 } 150 151 // ReadLogs reads in the logs from the logPath 152 func ReadLogs(logPath string, ctr *libpod.Container, opts *LogOptions) error { 153 file, err := os.Open(logPath) 154 if err != nil { 155 return errors.Wrapf(err, "failed to open log file %q", logPath) 156 } 157 defer errorhandling.CloseQuiet(file) 158 159 msg := &logMessage{} 160 opts.bytes = -1 161 writer := newLogWriter(opts) 162 reader := bufio.NewReader(file) 163 164 if opts.Follow { 165 err = followLog(reader, writer, opts, ctr, msg, logPath) 166 } else { 167 err = dumpLog(reader, writer, opts, msg, logPath) 168 } 169 return err 170 } 171 172 func followLog(reader *bufio.Reader, writer *logWriter, opts *LogOptions, ctr *libpod.Container, msg *logMessage, logPath string) error { 173 var cacheOutput []string 174 firstPass := false 175 if opts.Tail > 0 { 176 firstPass = true 177 } 178 // We need to read the entire file in here until we reach EOF 179 // and then dump it out in the case that the user also wants 180 // tail output 181 for { 182 line, err := reader.ReadString(eol[0]) 183 if err == io.EOF && opts.Follow { 184 if firstPass { 185 firstPass = false 186 cacheLen := int64(len(cacheOutput)) 187 start := int64(0) 188 if cacheLen > int64(opts.Tail) { 189 start = cacheLen - int64(opts.Tail) 190 } 191 for i := start; i < cacheLen; i++ { 192 msg.reset() 193 if err := parseCRILog([]byte(cacheOutput[i]), msg); err != nil { 194 return errors.Wrapf(err, "error parsing log line") 195 } 196 // Write the log line into the stream. 197 if err := writer.write(msg); err != nil { 198 if err == errMaximumWrite { 199 logrus.Infof("Finish parsing log file %q, hit bytes limit %d(bytes)", logPath, opts.bytes) 200 return nil 201 } 202 logrus.Errorf("Failed with err %v when writing log for log file %q: %+v", err, logPath, msg) 203 return err 204 } 205 } 206 continue 207 } 208 time.Sleep(1 * time.Second) 209 // Check if container is still running or paused 210 state, err := ctr.State() 211 if err != nil { 212 return err 213 } 214 if state != define.ContainerStateRunning && state != define.ContainerStatePaused { 215 break 216 } 217 continue 218 } 219 // exits 220 if err != nil { 221 break 222 } 223 if firstPass { 224 cacheOutput = append(cacheOutput, line) 225 continue 226 } 227 msg.reset() 228 if err := parseCRILog([]byte(line), msg); err != nil { 229 return errors.Wrapf(err, "error parsing log line") 230 } 231 // Write the log line into the stream. 232 if err := writer.write(msg); err != nil { 233 if err == errMaximumWrite { 234 logrus.Infof("Finish parsing log file %q, hit bytes limit %d(bytes)", logPath, opts.bytes) 235 return nil 236 } 237 logrus.Errorf("Failed with err %v when writing log for log file %q: %+v", err, logPath, msg) 238 return err 239 } 240 } 241 return nil 242 } 243 244 func dumpLog(reader *bufio.Reader, writer *logWriter, opts *LogOptions, msg *logMessage, logPath string) error { 245 output := readLog(reader, opts) 246 for _, line := range output { 247 msg.reset() 248 if err := parseCRILog([]byte(line), msg); err != nil { 249 return errors.Wrapf(err, "error parsing log line") 250 } 251 // Write the log line into the stream. 252 if err := writer.write(msg); err != nil { 253 if err == errMaximumWrite { 254 logrus.Infof("Finish parsing log file %q, hit bytes limit %d(bytes)", logPath, opts.bytes) 255 return nil 256 } 257 logrus.Errorf("Failed with err %v when writing log for log file %q: %+v", err, logPath, msg) 258 return err 259 } 260 } 261 262 return nil 263 } 264 265 func readLog(reader *bufio.Reader, opts *LogOptions) []string { 266 var output []string 267 for { 268 line, err := reader.ReadString(eol[0]) 269 if err != nil { 270 break 271 } 272 output = append(output, line) 273 } 274 start := 0 275 if opts.Tail > 0 { 276 if len(output) > int(opts.Tail) { 277 start = len(output) - int(opts.Tail) 278 } 279 } 280 return output[start:] 281 } 282 283 // logWriter controls the writing into the stream based on the log options. 284 type logWriter struct { 285 stdout io.Writer 286 stderr io.Writer 287 opts *LogOptions 288 remain int64 289 doAppend bool 290 } 291 292 // errMaximumWrite is returned when all bytes have been written. 293 var errMaximumWrite = errors.New("maximum write") 294 295 // errShortWrite is returned when the message is not fully written. 296 var errShortWrite = errors.New("short write") 297 298 func newLogWriter(opts *LogOptions) *logWriter { 299 w := &logWriter{ 300 stdout: os.Stdout, 301 stderr: os.Stderr, 302 opts: opts, 303 remain: math.MaxInt64, // initialize it as infinity 304 } 305 if opts.bytes >= 0 { 306 w.remain = opts.bytes 307 } 308 return w 309 } 310 311 // writeLogs writes logs into stdout, stderr. 312 func (w *logWriter) write(msg *logMessage) error { 313 if msg.timestamp.Before(w.opts.Since) { 314 // Skip the line because it's older than since 315 return nil 316 } 317 line := msg.log 318 if w.opts.Timestamps && !w.doAppend { 319 prefix := append([]byte(msg.timestamp.Format(timeFormat)), delimiter[0]) 320 line = append(prefix, line...) 321 if len(line) > 0 && line[len(line)-1] != '\n' { 322 w.doAppend = true 323 } 324 } 325 if w.doAppend && len(line) > 0 && line[len(line)-1] == '\n' { 326 w.doAppend = false 327 } 328 // If the line is longer than the remaining bytes, cut it. 329 if int64(len(line)) > w.remain { 330 line = line[:w.remain] 331 } 332 // Get the proper stream to write to. 333 var stream io.Writer 334 switch msg.stream { 335 case Stdout: 336 stream = w.stdout 337 case Stderr: 338 stream = w.stderr 339 default: 340 return fmt.Errorf("unexpected stream type %q", msg.stream) 341 } 342 n, err := stream.Write(line) 343 w.remain -= int64(n) 344 if err != nil { 345 return err 346 } 347 // If the line has not been fully written, return errShortWrite 348 if n < len(line) { 349 return errShortWrite 350 } 351 // If there are no more bytes left, return errMaximumWrite 352 if w.remain <= 0 { 353 return errMaximumWrite 354 } 355 return nil 356 }