github.com/containerd/nerdctl@v1.7.7/pkg/logging/fluentd_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  	"fmt"
    21  	"math"
    22  	"net/url"
    23  	"runtime"
    24  	"strconv"
    25  	"strings"
    26  	"sync"
    27  	"time"
    28  
    29  	"github.com/containerd/containerd/runtime/v2/logging"
    30  	"github.com/containerd/log"
    31  	"github.com/containerd/nerdctl/pkg/strutil"
    32  	"github.com/fluent/fluent-logger-golang/fluent"
    33  )
    34  
    35  type FluentdLogger struct {
    36  	Opts         map[string]string
    37  	fluentClient *fluent.Fluent
    38  	config       *logging.Config
    39  }
    40  
    41  const (
    42  	fluentAddress                 = "fluentd-address"
    43  	fluentdAsync                  = "fluentd-async"
    44  	fluentdBufferLimit            = "fluentd-buffer-limit"
    45  	fluentdRetryWait              = "fluentd-retry-wait"
    46  	fluentdMaxRetries             = "fluentd-max-retries"
    47  	fluentdSubSecondPrecision     = "fluentd-sub-second-precision"
    48  	fluentdAsyncReconnectInterval = "fluentd-async-reconnect-interval"
    49  	fluentRequestAck              = "fluentd-request-ack"
    50  )
    51  
    52  var FluentdLogOpts = []string{
    53  	fluentAddress,
    54  	fluentdAsync,
    55  	fluentdBufferLimit,
    56  	fluentdRetryWait,
    57  	fluentdMaxRetries,
    58  	fluentdSubSecondPrecision,
    59  	fluentdAsyncReconnectInterval,
    60  	fluentRequestAck,
    61  	Tag,
    62  }
    63  
    64  const (
    65  	defaultBufferLimit = 1024 * 1024
    66  	defaultHost        = "127.0.0.1"
    67  	defaultPort        = 24224
    68  	defaultProtocol    = "tcp"
    69  
    70  	defaultMaxRetries = math.MaxInt32
    71  	defaultRetryWait  = 1000 * time.Millisecond
    72  
    73  	minReconnectInterval = 100 * time.Millisecond
    74  	maxReconnectInterval = 10 * time.Second
    75  )
    76  
    77  func FluentdLogOptsValidate(logOptMap map[string]string) error {
    78  	for key := range logOptMap {
    79  		if !strutil.InStringSlice(FluentdLogOpts, key) {
    80  			log.L.Warnf("log-opt %s is ignored for fluentd log driver", key)
    81  		}
    82  	}
    83  	if _, ok := logOptMap[fluentAddress]; !ok {
    84  		log.L.Warnf("%s is missing for fluentd log driver, the default value %s:%d will be used", fluentAddress, defaultHost, defaultPort)
    85  	}
    86  	return nil
    87  }
    88  
    89  type fluentdLocation struct {
    90  	protocol string
    91  	host     string
    92  	port     int
    93  	path     string
    94  }
    95  
    96  func (f *FluentdLogger) Init(dataStore, ns, id string) error {
    97  	return nil
    98  }
    99  
   100  func (f *FluentdLogger) PreProcess(_ string, config *logging.Config) error {
   101  	if runtime.GOOS == "windows" {
   102  		// TODO: support fluentd on windows
   103  		return fmt.Errorf("logging to fluentd is not supported on windows")
   104  	}
   105  	fluentConfig, err := parseFluentdConfig(f.Opts)
   106  	if err != nil {
   107  		return err
   108  	}
   109  	fluentClient, err := fluent.New(fluentConfig)
   110  	if err != nil {
   111  		return fmt.Errorf("failed to create fluent client: %w", err)
   112  	}
   113  	f.fluentClient = fluentClient
   114  	f.config = config
   115  	return nil
   116  }
   117  func (f *FluentdLogger) Process(stdout <-chan string, stderr <-chan string) error {
   118  	var wg sync.WaitGroup
   119  	wg.Add(2)
   120  	fun := func(wg *sync.WaitGroup, dataChan <-chan string, id, namespace, source string) {
   121  		defer wg.Done()
   122  		metaData := map[string]string{
   123  			"container_id": id,
   124  			"namespace":    namespace,
   125  			"source":       source,
   126  		}
   127  		for log := range dataChan {
   128  			metaData["log"] = log
   129  			f.fluentClient.PostWithTime(f.Opts[Tag], time.Now(), metaData)
   130  		}
   131  	}
   132  	go fun(&wg, stdout, f.config.ID, f.config.Namespace, "stdout")
   133  	go fun(&wg, stderr, f.config.ID, f.config.Namespace, "stderr")
   134  
   135  	wg.Wait()
   136  	return nil
   137  }
   138  
   139  func (f *FluentdLogger) PostProcess() error {
   140  	defer f.fluentClient.Close()
   141  	return nil
   142  }
   143  
   144  func parseAddress(address string) (*fluentdLocation, error) {
   145  	if address == "" {
   146  		return &fluentdLocation{
   147  			protocol: defaultProtocol,
   148  			host:     defaultHost,
   149  			port:     defaultPort,
   150  		}, nil
   151  	}
   152  	if !strings.Contains(address, "://") {
   153  		address = defaultProtocol + "://" + address
   154  	}
   155  	tempURL, err := url.Parse(address)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  	switch tempURL.Scheme {
   160  	case "unix":
   161  		if strings.TrimLeft(tempURL.Path, "/") == "" {
   162  			return nil, fmt.Errorf("unix socket path must not be empty")
   163  		}
   164  		return &fluentdLocation{
   165  			protocol: tempURL.Scheme,
   166  			path:     tempURL.Path,
   167  		}, nil
   168  	case "tcp", "tls":
   169  	// continue to process below
   170  	default:
   171  		return nil, fmt.Errorf("unsupported protocol: %s", tempURL.Scheme)
   172  	}
   173  	if tempURL.Path != "" {
   174  		return nil, fmt.Errorf("path is not supported: %s", tempURL.Path)
   175  	}
   176  	host := defaultHost
   177  	port := defaultPort
   178  	if h := tempURL.Hostname(); h != "" {
   179  		host = h
   180  	}
   181  	if p := tempURL.Port(); p != "" {
   182  		portNum, err := strconv.ParseUint(p, 10, 16)
   183  		if err != nil {
   184  			return nil, fmt.Errorf("error occurs %v,invalid port", err)
   185  		}
   186  		port = int(portNum)
   187  	}
   188  	return &fluentdLocation{
   189  		protocol: tempURL.Scheme,
   190  		host:     host,
   191  		port:     port,
   192  	}, nil
   193  }
   194  
   195  func ValidateFluentdLoggerOpts(config map[string]string) error {
   196  	for key := range config {
   197  		switch key {
   198  		case Tag:
   199  		case fluentdBufferLimit:
   200  		case fluentdMaxRetries:
   201  		case fluentdRetryWait:
   202  		case fluentdSubSecondPrecision:
   203  		case fluentdAsync:
   204  		case fluentAddress:
   205  		case fluentdAsyncReconnectInterval:
   206  		case fluentRequestAck:
   207  		// Accepted logger opts
   208  		default:
   209  			return fmt.Errorf("unknown log opt '%s' for fluentd log driver", key)
   210  		}
   211  	}
   212  	return nil
   213  }
   214  
   215  func parseFluentdConfig(config map[string]string) (fluent.Config, error) {
   216  	result := fluent.Config{}
   217  	location, err := parseAddress(config[fluentAddress])
   218  	if err != nil {
   219  		return result, fmt.Errorf("error occurs %v,invalid fluentd address (%s)", err, config[fluentAddress])
   220  	}
   221  	bufferLimit := defaultBufferLimit
   222  	if config[fluentdBufferLimit] != "" {
   223  		bufferLimit, err = strconv.Atoi(config[fluentdBufferLimit])
   224  		if err != nil {
   225  			return result, fmt.Errorf("error occurs %v,invalid buffer limit (%s)", err, config[fluentdBufferLimit])
   226  		}
   227  	}
   228  	retryWait := int(defaultRetryWait)
   229  	if config[fluentdRetryWait] != "" {
   230  		temp, err := time.ParseDuration(config[fluentdRetryWait])
   231  		if err != nil {
   232  			return result, fmt.Errorf("error occurs %v,invalid retry wait (%s)", err, config[fluentdRetryWait])
   233  		}
   234  		retryWait = int(temp.Milliseconds())
   235  	}
   236  	maxRetries := defaultMaxRetries
   237  	if config[fluentdMaxRetries] != "" {
   238  		maxRetries, err = strconv.Atoi(config[fluentdMaxRetries])
   239  		if err != nil {
   240  			return result, fmt.Errorf("error occurs %v,invalid max retries (%s)", err, config[fluentdMaxRetries])
   241  		}
   242  	}
   243  	async := false
   244  	if config[fluentdAsync] != "" {
   245  		async, err = strconv.ParseBool(config[fluentdAsync])
   246  		if err != nil {
   247  			return result, fmt.Errorf("error occurs %v,invalid async (%s)", err, config[fluentdAsync])
   248  		}
   249  	}
   250  	asyncReconnectInterval := 0
   251  	if config[fluentdAsyncReconnectInterval] != "" {
   252  		tempDuration, err := time.ParseDuration(config[fluentdAsyncReconnectInterval])
   253  		if err != nil {
   254  			return result, fmt.Errorf("error occurs %v,invalid async connect interval (%s)", err, config[fluentdAsyncReconnectInterval])
   255  		}
   256  		if tempDuration != 0 && (tempDuration < minReconnectInterval || tempDuration > maxReconnectInterval) {
   257  			return result, fmt.Errorf("invalid async connect interval (%s), must be between %d and %d", config[fluentdAsyncReconnectInterval], minReconnectInterval.Milliseconds(), maxReconnectInterval.Milliseconds())
   258  		}
   259  		asyncReconnectInterval = int(tempDuration.Milliseconds())
   260  	}
   261  	subSecondPrecision := false
   262  	if config[fluentdSubSecondPrecision] != "" {
   263  		subSecondPrecision, err = strconv.ParseBool(config[fluentdSubSecondPrecision])
   264  		if err != nil {
   265  			return result, fmt.Errorf("error occurs %v,invalid sub second precision (%s)", err, config[fluentdSubSecondPrecision])
   266  		}
   267  	}
   268  	requestAck := false
   269  	if config[fluentRequestAck] != "" {
   270  		requestAck, err = strconv.ParseBool(config[fluentRequestAck])
   271  		if err != nil {
   272  			return result, fmt.Errorf("error occurs %v,invalid request ack (%s)", err, config[fluentRequestAck])
   273  		}
   274  	}
   275  	result = fluent.Config{
   276  		FluentPort:             location.port,
   277  		FluentHost:             location.host,
   278  		FluentNetwork:          location.protocol,
   279  		FluentSocketPath:       location.path,
   280  		BufferLimit:            bufferLimit,
   281  		RetryWait:              retryWait,
   282  		MaxRetry:               maxRetries,
   283  		Async:                  async,
   284  		AsyncReconnectInterval: asyncReconnectInterval,
   285  		SubSecondPrecision:     subSecondPrecision,
   286  		RequestAck:             requestAck,
   287  	}
   288  	return result, nil
   289  }