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  }