github.com/containerd/nerdctl@v1.7.7/pkg/logging/logging.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  	"bufio"
    21  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"os"
    27  	"path/filepath"
    28  	"sort"
    29  	"sync"
    30  
    31  	"github.com/containerd/containerd/runtime/v2/logging"
    32  	"github.com/containerd/errdefs"
    33  	"github.com/containerd/log"
    34  	"github.com/muesli/cancelreader"
    35  )
    36  
    37  const (
    38  	// MagicArgv1 is the magic argv1 for the containerd runtime v2 logging plugin mode.
    39  	MagicArgv1 = "_NERDCTL_INTERNAL_LOGGING"
    40  	LogPath    = "log-path"
    41  	MaxSize    = "max-size"
    42  	MaxFile    = "max-file"
    43  	Tag        = "tag"
    44  )
    45  
    46  type Driver interface {
    47  	Init(dataStore, ns, id string) error
    48  	PreProcess(dataStore string, config *logging.Config) error
    49  	Process(stdout <-chan string, stderr <-chan string) error
    50  	PostProcess() error
    51  }
    52  
    53  type DriverFactory func(map[string]string) (Driver, error)
    54  type LogOptsValidateFunc func(logOptMap map[string]string) error
    55  
    56  var drivers = make(map[string]DriverFactory)
    57  var driversLogOptsValidateFunctions = make(map[string]LogOptsValidateFunc)
    58  
    59  func ValidateLogOpts(logDriver string, logOpts map[string]string) error {
    60  	if value, ok := driversLogOptsValidateFunctions[logDriver]; ok && value != nil {
    61  		return value(logOpts)
    62  	}
    63  	return nil
    64  }
    65  
    66  func RegisterDriver(name string, f DriverFactory, validateFunc LogOptsValidateFunc) {
    67  	drivers[name] = f
    68  	driversLogOptsValidateFunctions[name] = validateFunc
    69  }
    70  
    71  func Drivers() []string {
    72  	var ss []string // nolint: prealloc
    73  	for f := range drivers {
    74  		ss = append(ss, f)
    75  	}
    76  	sort.Strings(ss)
    77  	return ss
    78  }
    79  
    80  func GetDriver(name string, opts map[string]string) (Driver, error) {
    81  	driverFactory, ok := drivers[name]
    82  	if !ok {
    83  		return nil, fmt.Errorf("unknown logging driver %q: %w", name, errdefs.ErrNotFound)
    84  	}
    85  	return driverFactory(opts)
    86  }
    87  
    88  func init() {
    89  	RegisterDriver("json-file", func(opts map[string]string) (Driver, error) {
    90  		return &JSONLogger{Opts: opts}, nil
    91  	}, JSONFileLogOptsValidate)
    92  	RegisterDriver("journald", func(opts map[string]string) (Driver, error) {
    93  		return &JournaldLogger{Opts: opts}, nil
    94  	}, JournalLogOptsValidate)
    95  	RegisterDriver("fluentd", func(opts map[string]string) (Driver, error) {
    96  		return &FluentdLogger{Opts: opts}, nil
    97  	}, FluentdLogOptsValidate)
    98  	RegisterDriver("syslog", func(opts map[string]string) (Driver, error) {
    99  		return &SyslogLogger{Opts: opts}, nil
   100  	}, SyslogOptsValidate)
   101  }
   102  
   103  // Main is the entrypoint for the containerd runtime v2 logging plugin mode.
   104  //
   105  // Should be called only if argv1 == MagicArgv1.
   106  func Main(argv2 string) error {
   107  	fn, err := loggerFunc(argv2)
   108  	if err != nil {
   109  		return err
   110  	}
   111  	logging.Run(fn)
   112  	return nil
   113  }
   114  
   115  // LogConfig is marshalled as "log-config.json"
   116  type LogConfig struct {
   117  	Driver string            `json:"driver"`
   118  	Opts   map[string]string `json:"opts,omitempty"`
   119  	LogURI string            `json:"-"`
   120  }
   121  
   122  // LogConfigFilePath returns the path of log-config.json
   123  func LogConfigFilePath(dataStore, ns, id string) string {
   124  	return filepath.Join(dataStore, "containers", ns, id, "log-config.json")
   125  }
   126  
   127  // LoadLogConfig loads the log-config.json for the afferrent container store
   128  func LoadLogConfig(dataStore, ns, id string) (LogConfig, error) {
   129  	logConfig := LogConfig{}
   130  
   131  	logConfigFilePath := LogConfigFilePath(dataStore, ns, id)
   132  	logConfigData, err := os.ReadFile(logConfigFilePath)
   133  	if err != nil {
   134  		return logConfig, fmt.Errorf("failed to read log config file %q: %s", logConfigFilePath, err)
   135  	}
   136  
   137  	err = json.Unmarshal(logConfigData, &logConfig)
   138  	if err != nil {
   139  		return logConfig, fmt.Errorf("failed to load JSON logging config file %q: %s", logConfigFilePath, err)
   140  	}
   141  	return logConfig, nil
   142  }
   143  
   144  func loggingProcessAdapter(ctx context.Context, driver Driver, dataStore string, config *logging.Config) error {
   145  	if err := driver.PreProcess(dataStore, config); err != nil {
   146  		return err
   147  	}
   148  
   149  	stdoutR, err := cancelreader.NewReader(config.Stdout)
   150  	if err != nil {
   151  		return err
   152  	}
   153  	stderrR, err := cancelreader.NewReader(config.Stderr)
   154  	if err != nil {
   155  		return err
   156  	}
   157  	go func() {
   158  		<-ctx.Done() // delivered on SIGTERM
   159  		stdoutR.Cancel()
   160  		stderrR.Cancel()
   161  	}()
   162  
   163  	var wg sync.WaitGroup
   164  	wg.Add(3)
   165  	stdout := make(chan string, 10000)
   166  	stderr := make(chan string, 10000)
   167  	processLogFunc := func(reader io.Reader, dataChan chan string) {
   168  		defer wg.Done()
   169  		defer close(dataChan)
   170  		scanner := bufio.NewScanner(reader)
   171  		for scanner.Scan() {
   172  			if scanner.Err() != nil {
   173  				log.L.Errorf("failed to read log: %v", scanner.Err())
   174  				return
   175  			}
   176  			dataChan <- scanner.Text()
   177  		}
   178  	}
   179  
   180  	go processLogFunc(stdoutR, stdout)
   181  	go processLogFunc(stderrR, stderr)
   182  	go func() {
   183  		defer wg.Done()
   184  		driver.Process(stdout, stderr)
   185  	}()
   186  	wg.Wait()
   187  	return driver.PostProcess()
   188  }
   189  
   190  func loggerFunc(dataStore string) (logging.LoggerFunc, error) {
   191  	if dataStore == "" {
   192  		return nil, errors.New("got empty data store")
   193  	}
   194  	return func(ctx context.Context, config *logging.Config, ready func() error) error {
   195  		if config.Namespace == "" || config.ID == "" {
   196  			return errors.New("got invalid config")
   197  		}
   198  		logConfigFilePath := LogConfigFilePath(dataStore, config.Namespace, config.ID)
   199  		if _, err := os.Stat(logConfigFilePath); err == nil {
   200  			logConfig, err := LoadLogConfig(dataStore, config.Namespace, config.ID)
   201  			if err != nil {
   202  				return err
   203  			}
   204  			driver, err := GetDriver(logConfig.Driver, logConfig.Opts)
   205  			if err != nil {
   206  				return err
   207  			}
   208  			if err := ready(); err != nil {
   209  				return err
   210  			}
   211  
   212  			return loggingProcessAdapter(ctx, driver, dataStore, config)
   213  		} else if !errors.Is(err, os.ErrNotExist) {
   214  			// the file does not exist if the container was created with nerdctl < 0.20
   215  			return err
   216  		}
   217  		return nil
   218  	}, nil
   219  }