github.com/containerd/nerdctl@v1.7.7/pkg/logging/json_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  package logging
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"os/exec"
    25  	"path/filepath"
    26  	"strconv"
    27  	"time"
    28  
    29  	"github.com/containerd/containerd/runtime/v2/logging"
    30  	"github.com/containerd/log"
    31  	"github.com/containerd/nerdctl/pkg/logging/jsonfile"
    32  	"github.com/containerd/nerdctl/pkg/strutil"
    33  	"github.com/docker/go-units"
    34  	"github.com/fahedouch/go-logrotate"
    35  )
    36  
    37  var JSONDriverLogOpts = []string{
    38  	LogPath,
    39  	MaxSize,
    40  	MaxFile,
    41  }
    42  
    43  type JSONLogger struct {
    44  	Opts   map[string]string
    45  	logger *logrotate.Logger
    46  }
    47  
    48  func JSONFileLogOptsValidate(logOptMap map[string]string) error {
    49  	for key := range logOptMap {
    50  		if !strutil.InStringSlice(JSONDriverLogOpts, key) {
    51  			log.L.Warnf("log-opt %s is ignored for json-file log driver", key)
    52  		}
    53  	}
    54  	return nil
    55  }
    56  
    57  func (jsonLogger *JSONLogger) Init(dataStore, ns, id string) error {
    58  	// Initialize the log file (https://github.com/containerd/nerdctl/issues/1071)
    59  	var jsonFilePath string
    60  	if logPath, ok := jsonLogger.Opts[LogPath]; ok {
    61  		jsonFilePath = logPath
    62  	} else {
    63  		jsonFilePath = jsonfile.Path(dataStore, ns, id)
    64  	}
    65  	if err := os.MkdirAll(filepath.Dir(jsonFilePath), 0700); err != nil {
    66  		return err
    67  	}
    68  	if _, err := os.Stat(jsonFilePath); errors.Is(err, os.ErrNotExist) {
    69  		if writeErr := os.WriteFile(jsonFilePath, []byte{}, 0600); writeErr != nil {
    70  			return writeErr
    71  		}
    72  	}
    73  	return nil
    74  }
    75  
    76  func (jsonLogger *JSONLogger) PreProcess(dataStore string, config *logging.Config) error {
    77  	var jsonFilePath string
    78  	if logPath, ok := jsonLogger.Opts[LogPath]; ok {
    79  		jsonFilePath = logPath
    80  	} else {
    81  		jsonFilePath = jsonfile.Path(dataStore, config.Namespace, config.ID)
    82  	}
    83  	l := &logrotate.Logger{
    84  		Filename: jsonFilePath,
    85  	}
    86  	//maxSize Defaults to unlimited.
    87  	var capVal int64
    88  	capVal = -1
    89  	if capacity, ok := jsonLogger.Opts[MaxSize]; ok {
    90  		var err error
    91  		capVal, err = units.FromHumanSize(capacity)
    92  		if err != nil {
    93  			return err
    94  		}
    95  		if capVal <= 0 {
    96  			return fmt.Errorf("max-size must be a positive number")
    97  		}
    98  	}
    99  	l.MaxBytes = capVal
   100  	maxFile := 1
   101  	if maxFileString, ok := jsonLogger.Opts[MaxFile]; ok {
   102  		var err error
   103  		maxFile, err = strconv.Atoi(maxFileString)
   104  		if err != nil {
   105  			return err
   106  		}
   107  		if maxFile < 1 {
   108  			return fmt.Errorf("max-file cannot be less than 1")
   109  		}
   110  	}
   111  	// MaxBackups does not include file to write logs to
   112  	l.MaxBackups = maxFile - 1
   113  	jsonLogger.logger = l
   114  	return nil
   115  }
   116  
   117  func (jsonLogger *JSONLogger) Process(stdout <-chan string, stderr <-chan string) error {
   118  	return jsonfile.Encode(stdout, stderr, jsonLogger.logger)
   119  }
   120  
   121  func (jsonLogger *JSONLogger) PostProcess() error {
   122  	return nil
   123  }
   124  
   125  // Loads log entries from logfiles produced by the json-logger driver and forwards
   126  // them to the provided io.Writers after applying the provided logging options.
   127  func viewLogsJSONFile(lvopts LogViewOptions, stdout, stderr io.Writer, stopChannel chan os.Signal) error {
   128  	logFilePath := jsonfile.Path(lvopts.DatastoreRootPath, lvopts.Namespace, lvopts.ContainerID)
   129  	if _, err := os.Stat(logFilePath); err != nil {
   130  		return fmt.Errorf("failed to stat JSON log file ")
   131  	}
   132  
   133  	if checkExecutableAvailableInPath("tail") {
   134  		return viewLogsJSONFileThroughTailExec(lvopts, logFilePath, stdout, stderr, stopChannel)
   135  	}
   136  	return viewLogsJSONFileDirect(lvopts, logFilePath, stdout, stderr, stopChannel)
   137  }
   138  
   139  // Loads JSON log entries directly from the provided JSON log file.
   140  // If `LogViewOptions.Follow` is provided, it will refresh and re-read the file until
   141  // it receives something through the stopChannel.
   142  func viewLogsJSONFileDirect(lvopts LogViewOptions, jsonLogFilePath string, stdout, stderr io.Writer, stopChannel chan os.Signal) error {
   143  	fin, err := os.OpenFile(jsonLogFilePath, os.O_RDONLY, 0400)
   144  	if err != nil {
   145  		return err
   146  	}
   147  	defer fin.Close()
   148  	err = jsonfile.Decode(stdout, stderr, fin, lvopts.Timestamps, lvopts.Since, lvopts.Until, lvopts.Tail)
   149  	if err != nil {
   150  		return fmt.Errorf("error occurred while doing initial read of JSON logfile %q: %s", jsonLogFilePath, err)
   151  	}
   152  
   153  	if lvopts.Follow {
   154  		// Get the current file handler's seek.
   155  		lastPos, err := fin.Seek(0, io.SeekCurrent)
   156  		if err != nil {
   157  			return fmt.Errorf("error occurred while trying to seek JSON logfile %q at position %d: %s", jsonLogFilePath, lastPos, err)
   158  		}
   159  		fin.Close()
   160  		for {
   161  			select {
   162  			case <-stopChannel:
   163  				log.L.Debugf("received stop signal while re-reading JSON logfile, returning")
   164  				return nil
   165  			default:
   166  				// Re-open the file and seek to the last-consumed offset.
   167  				fin, err = os.OpenFile(jsonLogFilePath, os.O_RDONLY, 0400)
   168  				if err != nil {
   169  					fin.Close()
   170  					return fmt.Errorf("error occurred while trying to re-open JSON logfile %q: %s", jsonLogFilePath, err)
   171  				}
   172  				_, err = fin.Seek(lastPos, 0)
   173  				if err != nil {
   174  					fin.Close()
   175  					return fmt.Errorf("error occurred while trying to seek JSON logfile %q at position %d: %s", jsonLogFilePath, lastPos, err)
   176  				}
   177  
   178  				err = jsonfile.Decode(stdout, stderr, fin, lvopts.Timestamps, lvopts.Since, lvopts.Until, 0)
   179  				if err != nil {
   180  					fin.Close()
   181  					return fmt.Errorf("error occurred while doing follow-up decoding of JSON logfile %q at starting position %d: %s", jsonLogFilePath, lastPos, err)
   182  				}
   183  
   184  				// Record current file seek position before looping again.
   185  				lastPos, err = fin.Seek(0, io.SeekCurrent)
   186  				if err != nil {
   187  					fin.Close()
   188  					return fmt.Errorf("error occurred while trying to seek JSON logfile %q at current position: %s", jsonLogFilePath, err)
   189  				}
   190  				fin.Close()
   191  			}
   192  			// Give the OS a second to breathe before re-opening the file:
   193  			time.Sleep(time.Second)
   194  		}
   195  	}
   196  	return nil
   197  }
   198  
   199  // Loads logs through the `tail` executable.
   200  func viewLogsJSONFileThroughTailExec(lvopts LogViewOptions, jsonLogFilePath string, stdout, stderr io.Writer, stopChannel chan os.Signal) error {
   201  	var args []string
   202  
   203  	args = append(args, "-n")
   204  	if lvopts.Tail == 0 {
   205  		args = append(args, "+0")
   206  	} else {
   207  		args = append(args, strconv.FormatUint(uint64(lvopts.Tail), 10))
   208  	}
   209  
   210  	if lvopts.Follow {
   211  		args = append(args, "-f")
   212  	}
   213  	args = append(args, jsonLogFilePath)
   214  	cmd := exec.Command("tail", args...)
   215  	cmd.Stderr = os.Stderr
   216  	r, err := cmd.StdoutPipe()
   217  	if err != nil {
   218  		return err
   219  	}
   220  	if err := cmd.Start(); err != nil {
   221  		return err
   222  	}
   223  
   224  	// Setup killing goroutine:
   225  	go func() {
   226  		<-stopChannel
   227  		log.L.Debugf("killing tail logs process with PID: %d", cmd.Process.Pid)
   228  		cmd.Process.Kill()
   229  	}()
   230  
   231  	return jsonfile.Decode(stdout, stderr, r, lvopts.Timestamps, lvopts.Since, lvopts.Until, 0)
   232  }