github.com/containerd/nerdctl@v1.7.7/pkg/logging/cri_logger.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 /* 18 Forked from https://github.com/kubernetes/kubernetes/blob/a66aad2d80dacc70025f95a8f97d2549ebd3208c/pkg/kubelet/kuberuntime/logs/logs.go 19 Copyright The Kubernetes Authors. 20 Licensed under the Apache License, Version 2.0 21 */ 22 23 package logging 24 25 import ( 26 "bufio" 27 "bytes" 28 "errors" 29 "fmt" 30 "io" 31 "math" 32 "os" 33 "path/filepath" 34 "time" 35 36 "github.com/containerd/log" 37 "github.com/containerd/nerdctl/pkg/logging/tail" 38 ) 39 40 // LogStreamType is the type of the stream in CRI container log. 41 type LogStreamType string 42 43 const ( 44 // Stdout is the stream type for stdout. 45 Stdout LogStreamType = "stdout" 46 // Stderr is the stream type for stderr. 47 Stderr LogStreamType = "stderr" 48 ) 49 50 // LogTag is the tag of a log line in CRI container log. 51 // Currently defined log tags: 52 // * First tag: Partial/Full - P/F. 53 // The field in the container log format can be extended to include multiple 54 // tags by using a delimiter, but changes should be rare. If it becomes clear 55 // that better extensibility is desired, a more extensible format (e.g., json) 56 // should be adopted as a replacement and/or addition. 57 type LogTag string 58 59 const ( 60 // LogTagPartial means the line is part of multiple lines. 61 LogTagPartial LogTag = "P" 62 // LogTagFull means the line is a single full line or the end of multiple lines. 63 LogTagFull LogTag = "F" 64 // LogTagDelimiter is the delimiter for different log tags. 65 LogTagDelimiter = ":" 66 ) 67 68 // Loads log entries from logfiles produced by the Text-logger driver and forwards 69 // them to the provided io.Writers after applying the provided logging options. 70 func viewLogsCRI(lvopts LogViewOptions, stdout, stderr io.Writer, stopChannel chan os.Signal) error { 71 if lvopts.LogPath == "" { 72 return fmt.Errorf("logpath is nil ") 73 } 74 75 return ReadLogs(&lvopts, stdout, stderr, stopChannel) 76 } 77 78 // ReadLogs read the container log and redirect into stdout and stderr. 79 // Note that containerID is only needed when following the log, or else 80 // just pass in empty string "". 81 func ReadLogs(opts *LogViewOptions, stdout, stderr io.Writer, stopChannel chan os.Signal) error { 82 var logPath = opts.LogPath 83 evaluated, err := filepath.EvalSymlinks(logPath) 84 if err != nil { 85 return fmt.Errorf("failed to try resolving symlinks in path %q: %v", logPath, err) 86 } 87 logPath = evaluated 88 f, err := os.Open(logPath) 89 if err != nil { 90 return fmt.Errorf("failed to open log file %q: %v", logPath, err) 91 } 92 defer f.Close() 93 94 // Search start point based on tail line. 95 start, err := tail.FindTailLineStartIndex(f, opts.Tail) 96 if err != nil { 97 return fmt.Errorf("failed to tail %d lines of log file %q: %v", opts.Tail, logPath, err) 98 } 99 100 if _, err := f.Seek(start, io.SeekStart); err != nil { 101 return fmt.Errorf("failed to seek in log file %q: %v", logPath, err) 102 } 103 104 limitedMode := (opts.Tail > 0) && (!opts.Follow) 105 limitedNum := opts.Tail 106 // Start parsing the logs. 107 r := bufio.NewReader(f) 108 109 var stop bool 110 isNewLine := true 111 writer := newLogWriter(stdout, stderr, opts) 112 msg := &logMessage{} 113 for { 114 select { 115 case <-stopChannel: 116 log.L.Debugf("received stop signal while reading cri logfile, returning") 117 return nil 118 default: 119 if stop || (limitedMode && limitedNum == 0) { 120 log.L.Debugf("finished parsing log file, path: %s", logPath) 121 return nil 122 } 123 l, err := r.ReadBytes(eol[0]) 124 if err != nil { 125 if err != io.EOF { // This is an real error 126 return fmt.Errorf("failed to read log file %q: %v", logPath, err) 127 } 128 if opts.Follow { 129 130 // Reset seek so that if this is an incomplete line, 131 // it will be read again. 132 if _, err := f.Seek(-int64(len(l)), io.SeekCurrent); err != nil { 133 return fmt.Errorf("failed to reset seek in log file %q: %v", logPath, err) 134 } 135 136 // If the container exited consume data until the next EOF 137 continue 138 } 139 // Should stop after writing the remaining content. 140 stop = true 141 if len(l) == 0 { 142 continue 143 } 144 log.L.Debugf("incomplete line in log file, path: %s, line: %s", logPath, l) 145 } 146 147 // Parse the log line. 148 msg.reset() 149 if err := ParseCRILog(l, msg); err != nil { 150 log.L.WithError(err).Errorf("failed when parsing line in log file, path: %s, line: %s", logPath, l) 151 continue 152 } 153 // Write the log line into the stream. 154 if err := writer.write(msg, isNewLine); err != nil { 155 if err == errMaximumWrite { 156 log.L.Debugf("finished parsing log file, hit bytes limit path: %s", logPath) 157 return nil 158 } 159 log.L.WithError(err).Errorf("failed when writing line to log file, path: %s, line: %s", logPath, l) 160 return err 161 } 162 if limitedMode { 163 limitedNum-- 164 } 165 if len(msg.log) > 0 { 166 isNewLine = msg.log[len(msg.log)-1] == eol[0] 167 } else { 168 isNewLine = true 169 } 170 } 171 } 172 } 173 174 var ( 175 // eol is the end-of-line sign in the log. 176 eol = []byte{'\n'} 177 // delimiter is the delimiter for timestamp and stream type in log line. 178 delimiter = []byte{' '} 179 // tagDelimiter is the delimiter for log tags. 180 tagDelimiter = []byte(":") 181 ) 182 183 // logWriter controls the writing into the stream based on the log options. 184 type logWriter struct { 185 stdout io.Writer 186 stderr io.Writer 187 opts *LogViewOptions 188 remain int64 189 } 190 191 // errMaximumWrite is returned when all bytes have been written. 192 var errMaximumWrite = errors.New("maximum write") 193 194 // errShortWrite is returned when the message is not fully written. 195 var errShortWrite = errors.New("short write") 196 197 func newLogWriter(stdout io.Writer, stderr io.Writer, opts *LogViewOptions) *logWriter { 198 w := &logWriter{ 199 stdout: stdout, 200 stderr: stderr, 201 opts: opts, 202 remain: math.MaxInt64, // initialize it as infinity 203 } 204 //if opts.bytes >= 0 { 205 // w.remain = opts.bytes 206 //} 207 return w 208 } 209 210 // writeLogs writes logs into stdout, stderr. 211 func (w *logWriter) write(msg *logMessage, addPrefix bool) error { 212 213 //if msg.timestamp.Before(ts) { 214 // // Skip the line because it's older than since 215 // return nil 216 //} 217 line := msg.log 218 if w.opts.Timestamps && addPrefix { 219 prefix := append([]byte(msg.timestamp.Format(log.RFC3339NanoFixed)), delimiter[0]) 220 line = append(prefix, line...) 221 } 222 // If the line is longer than the remaining bytes, cut it. 223 if int64(len(line)) > w.remain { 224 line = line[:w.remain] 225 } 226 // Get the proper stream to write to. 227 var stream io.Writer 228 switch msg.stream { 229 case Stdout: 230 stream = w.stdout 231 case Stderr: 232 stream = w.stderr 233 default: 234 return fmt.Errorf("unexpected stream type %q", msg.stream) 235 } 236 n, err := stream.Write(line) 237 w.remain -= int64(n) 238 if err != nil { 239 return err 240 } 241 // If the line has not been fully written, return errShortWrite 242 if n < len(line) { 243 return errShortWrite 244 } 245 // If there are no more bytes left, return errMaximumWrite 246 if w.remain <= 0 { 247 return errMaximumWrite 248 } 249 return nil 250 } 251 252 // logMessage is the CRI internal log type. 253 type logMessage struct { 254 timestamp time.Time 255 stream LogStreamType 256 log []byte 257 } 258 259 // reset the log to nil. 260 func (l *logMessage) reset() { 261 l.timestamp = time.Time{} 262 l.stream = "" 263 l.log = nil 264 } 265 266 // ParseCRILog parses logs in CRI log format. CRI Log format example: 267 // 268 // 2016-10-06T00:17:09.669794202Z stdout P log content 1 269 // 2016-10-06T00:17:09.669794203Z stderr F log content 2 270 func ParseCRILog(log []byte, msg *logMessage) error { 271 var err error 272 // Parse timestamp 273 idx := bytes.Index(log, delimiter) 274 if idx < 0 { 275 return fmt.Errorf("timestamp is not found") 276 } 277 msg.timestamp, err = time.Parse(time.RFC3339Nano, string(log[:idx])) 278 if err != nil { 279 return fmt.Errorf("unexpected timestamp format %q: %v", time.RFC3339Nano, err) 280 } 281 282 // Parse stream type 283 log = log[idx+1:] 284 idx = bytes.Index(log, delimiter) 285 if idx < 0 { 286 return fmt.Errorf("stream type is not found") 287 } 288 msg.stream = LogStreamType(log[:idx]) 289 if msg.stream != Stdout && msg.stream != Stderr { 290 return fmt.Errorf("unexpected stream type %q", msg.stream) 291 } 292 293 // Parse log tag 294 log = log[idx+1:] 295 idx = bytes.Index(log, delimiter) 296 if idx < 0 { 297 return fmt.Errorf("log tag is not found") 298 } 299 // Keep this forward compatible. 300 tags := bytes.Split(log[:idx], tagDelimiter) 301 partial := LogTag(tags[0]) == LogTagPartial 302 // Trim the tailing new line if this is a partial line. 303 if partial && len(log) > 0 && log[len(log)-1] == '\n' { 304 log = log[:len(log)-1] 305 } 306 307 // Get log content 308 msg.log = log[idx+1:] 309 310 return nil 311 }