...

Source file src/k8s.io/component-base/logs/api/v1/options.go

Documentation: k8s.io/component-base/logs/api/v1

     1  /*
     2  Copyright 2021 The Kubernetes 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 v1
    18  
    19  import (
    20  	"errors"
    21  	"flag"
    22  	"fmt"
    23  	"io"
    24  	"math"
    25  	"os"
    26  	"strings"
    27  	"sync/atomic"
    28  	"time"
    29  
    30  	"github.com/google/go-cmp/cmp"
    31  	"github.com/spf13/pflag"
    32  
    33  	"k8s.io/klog/v2"
    34  	"k8s.io/klog/v2/textlogger"
    35  
    36  	"k8s.io/apimachinery/pkg/api/resource"
    37  	"k8s.io/apimachinery/pkg/util/validation/field"
    38  	cliflag "k8s.io/component-base/cli/flag"
    39  	"k8s.io/component-base/featuregate"
    40  	"k8s.io/component-base/logs/internal/setverbositylevel"
    41  	"k8s.io/component-base/logs/klogflags"
    42  )
    43  
    44  const (
    45  	// LogFlushFreqDefault is the default for the corresponding command line
    46  	// parameter.
    47  	LogFlushFreqDefault = 5 * time.Second
    48  )
    49  
    50  const (
    51  	// LogFlushFreqFlagName is the name of the command line parameter.
    52  	// Depending on how flags get added, it is either a stand-alone
    53  	// value (logs.AddFlags) or part of LoggingConfiguration.
    54  	LogFlushFreqFlagName = "log-flush-frequency"
    55  )
    56  
    57  // NewLoggingConfiguration returns a struct holding the default logging configuration.
    58  func NewLoggingConfiguration() *LoggingConfiguration {
    59  	c := LoggingConfiguration{}
    60  	SetRecommendedLoggingConfiguration(&c)
    61  	return &c
    62  }
    63  
    64  // Applying configurations multiple times is not safe unless it's guaranteed that there
    65  // are no goroutines which might call logging functions. The default for ValidateAndApply
    66  // and ValidateAndApplyWithOptions is to return an error when called more than once.
    67  // Binaries and unit tests can override that behavior.
    68  var ReapplyHandling = ReapplyHandlingError
    69  
    70  type ReapplyHandlingType int
    71  
    72  const (
    73  	// ReapplyHandlingError is the default: calling ValidateAndApply or
    74  	// ValidateAndApplyWithOptions again returns an error.
    75  	ReapplyHandlingError ReapplyHandlingType = iota
    76  	// ReapplyHandlingIgnoreUnchanged silently ignores any additional calls of
    77  	// ValidateAndApply or ValidateAndApplyWithOptions if the configuration
    78  	// is unchanged, otherwise they return an error.
    79  	ReapplyHandlingIgnoreUnchanged
    80  )
    81  
    82  // ValidateAndApply combines validation and application of the logging configuration.
    83  // This should be invoked as early as possible because then the rest of the program
    84  // startup (including validation of other options) will already run with the final
    85  // logging configuration.
    86  //
    87  // The optional FeatureGate controls logging features. If nil, the default for
    88  // these features is used.
    89  //
    90  // Logging options must be applied as early as possible during the program
    91  // startup. Some changes are global and cannot be done safely when there are
    92  // already goroutines running.
    93  func ValidateAndApply(c *LoggingConfiguration, featureGate featuregate.FeatureGate) error {
    94  	return validateAndApply(c, nil, featureGate, nil)
    95  }
    96  
    97  // ValidateAndApplyWithOptions is a variant of ValidateAndApply which accepts
    98  // additional options beyond those that can be configured through the API. This
    99  // is meant for testing.
   100  //
   101  // Logging options must be applied as early as possible during the program
   102  // startup. Some changes are global and cannot be done safely when there are
   103  // already goroutines running.
   104  func ValidateAndApplyWithOptions(c *LoggingConfiguration, options *LoggingOptions, featureGate featuregate.FeatureGate) error {
   105  	return validateAndApply(c, options, featureGate, nil)
   106  }
   107  
   108  // +k8s:deepcopy-gen=false
   109  
   110  // LoggingOptions can be used with ValidateAndApplyWithOptions to override
   111  // certain global defaults.
   112  type LoggingOptions struct {
   113  	// ErrorStream can be used to override the os.Stderr default.
   114  	ErrorStream io.Writer
   115  
   116  	// InfoStream can be used to override the os.Stdout default.
   117  	InfoStream io.Writer
   118  }
   119  
   120  // ValidateAndApplyAsField is a variant of ValidateAndApply that should be used
   121  // when the LoggingConfiguration is embedded in some larger configuration
   122  // structure.
   123  func ValidateAndApplyAsField(c *LoggingConfiguration, featureGate featuregate.FeatureGate, fldPath *field.Path) error {
   124  	return validateAndApply(c, nil, featureGate, fldPath)
   125  }
   126  
   127  func validateAndApply(c *LoggingConfiguration, options *LoggingOptions, featureGate featuregate.FeatureGate, fldPath *field.Path) error {
   128  	errs := Validate(c, featureGate, fldPath)
   129  	if len(errs) > 0 {
   130  		return errs.ToAggregate()
   131  	}
   132  	return apply(c, options, featureGate)
   133  }
   134  
   135  // Validate can be used to check for invalid settings without applying them.
   136  // Most binaries should validate and apply the logging configuration as soon
   137  // as possible via ValidateAndApply. The field path is optional: nil
   138  // can be passed when the struct is not embedded in some larger struct.
   139  func Validate(c *LoggingConfiguration, featureGate featuregate.FeatureGate, fldPath *field.Path) field.ErrorList {
   140  	errs := field.ErrorList{}
   141  	if c.Format != DefaultLogFormat {
   142  		// WordSepNormalizeFunc is just a guess. Commands should use it,
   143  		// but we cannot know for sure.
   144  		allFlags := unsupportedLoggingFlags(cliflag.WordSepNormalizeFunc)
   145  		for _, f := range allFlags {
   146  			if f.DefValue != f.Value.String() {
   147  				errs = append(errs, field.Invalid(fldPath.Child("format"), c.Format, fmt.Sprintf("Non-default format doesn't honor flag: %s", f.Name)))
   148  			}
   149  		}
   150  	}
   151  	format, err := logRegistry.get(c.Format)
   152  	if err != nil {
   153  		errs = append(errs, field.Invalid(fldPath.Child("format"), c.Format, "Unsupported log format"))
   154  	} else if format != nil {
   155  		if format.feature != LoggingStableOptions {
   156  			enabled := featureGates()[format.feature].Default
   157  			if featureGate != nil {
   158  				enabled = featureGate.Enabled(format.feature)
   159  			}
   160  			if !enabled {
   161  				errs = append(errs, field.Forbidden(fldPath.Child("format"), fmt.Sprintf("Log format %s is disabled, see %s feature", c.Format, format.feature)))
   162  			}
   163  		}
   164  	}
   165  
   166  	// The type in our struct is uint32, but klog only accepts positive int32.
   167  	if c.Verbosity > math.MaxInt32 {
   168  		errs = append(errs, field.Invalid(fldPath.Child("verbosity"), c.Verbosity, fmt.Sprintf("Must be <= %d", math.MaxInt32)))
   169  	}
   170  	vmoduleFldPath := fldPath.Child("vmodule")
   171  	if len(c.VModule) > 0 && c.Format != "" && c.Format != "text" {
   172  		errs = append(errs, field.Forbidden(vmoduleFldPath, "Only supported for text log format"))
   173  	}
   174  	for i, item := range c.VModule {
   175  		if item.FilePattern == "" {
   176  			errs = append(errs, field.Required(vmoduleFldPath.Index(i), "File pattern must not be empty"))
   177  		}
   178  		if strings.ContainsAny(item.FilePattern, "=,") {
   179  			errs = append(errs, field.Invalid(vmoduleFldPath.Index(i), item.FilePattern, "File pattern must not contain equal sign or comma"))
   180  		}
   181  		if item.Verbosity > math.MaxInt32 {
   182  			errs = append(errs, field.Invalid(vmoduleFldPath.Index(i), item.Verbosity, fmt.Sprintf("Must be <= %d", math.MaxInt32)))
   183  		}
   184  	}
   185  
   186  	errs = append(errs, validateFormatOptions(c, featureGate, fldPath.Child("options"))...)
   187  	return errs
   188  }
   189  
   190  func validateFormatOptions(c *LoggingConfiguration, featureGate featuregate.FeatureGate, fldPath *field.Path) field.ErrorList {
   191  	errs := field.ErrorList{}
   192  	errs = append(errs, validateTextOptions(c, featureGate, fldPath.Child("text"))...)
   193  	errs = append(errs, validateJSONOptions(c, featureGate, fldPath.Child("json"))...)
   194  	return errs
   195  }
   196  
   197  func validateTextOptions(c *LoggingConfiguration, featureGate featuregate.FeatureGate, fldPath *field.Path) field.ErrorList {
   198  	errs := field.ErrorList{}
   199  	if gate := LoggingAlphaOptions; c.Options.Text.SplitStream && !featureEnabled(featureGate, gate) {
   200  		errs = append(errs, field.Forbidden(fldPath.Child("splitStream"), fmt.Sprintf("Feature %s is disabled", gate)))
   201  	}
   202  	if gate := LoggingAlphaOptions; c.Options.Text.InfoBufferSize.Value() != 0 && !featureEnabled(featureGate, gate) {
   203  		errs = append(errs, field.Forbidden(fldPath.Child("infoBufferSize"), fmt.Sprintf("Feature %s is disabled", gate)))
   204  	}
   205  	return errs
   206  }
   207  
   208  func validateJSONOptions(c *LoggingConfiguration, featureGate featuregate.FeatureGate, fldPath *field.Path) field.ErrorList {
   209  	errs := field.ErrorList{}
   210  	if gate := LoggingAlphaOptions; c.Options.JSON.SplitStream && !featureEnabled(featureGate, gate) {
   211  		errs = append(errs, field.Forbidden(fldPath.Child("splitStream"), fmt.Sprintf("Feature %s is disabled", gate)))
   212  	}
   213  	if gate := LoggingAlphaOptions; c.Options.JSON.InfoBufferSize.Value() != 0 && !featureEnabled(featureGate, gate) {
   214  		errs = append(errs, field.Forbidden(fldPath.Child("infoBufferSize"), fmt.Sprintf("Feature %s is disabled", gate)))
   215  	}
   216  	return errs
   217  }
   218  
   219  func featureEnabled(featureGate featuregate.FeatureGate, feature featuregate.Feature) bool {
   220  	enabled := false
   221  	if featureGate != nil {
   222  		enabled = featureGate.Enabled(feature)
   223  	}
   224  	return enabled
   225  }
   226  
   227  func apply(c *LoggingConfiguration, options *LoggingOptions, featureGate featuregate.FeatureGate) error {
   228  	p := &parameters{
   229  		C:                        c,
   230  		Options:                  options,
   231  		ContextualLoggingEnabled: contextualLoggingDefault,
   232  	}
   233  	if featureGate != nil {
   234  		p.ContextualLoggingEnabled = featureGate.Enabled(ContextualLogging)
   235  	}
   236  
   237  	oldP := applyParameters.Load()
   238  	if oldP != nil {
   239  		switch ReapplyHandling {
   240  		case ReapplyHandlingError:
   241  			return errors.New("logging configuration was already applied earlier, changing it is not allowed")
   242  		case ReapplyHandlingIgnoreUnchanged:
   243  			if diff := cmp.Diff(oldP, p); diff != "" {
   244  				return fmt.Errorf("the logging configuration should not be changed after setting it once (- old setting, + new setting):\n%s", diff)
   245  			}
   246  			return nil
   247  		default:
   248  			return fmt.Errorf("invalid value %d for ReapplyHandling", ReapplyHandling)
   249  		}
   250  	}
   251  	applyParameters.Store(p)
   252  
   253  	// if log format not exists, use nil loggr
   254  	format, _ := logRegistry.get(c.Format)
   255  	if format.factory == nil {
   256  		klog.ClearLogger()
   257  	} else {
   258  		if options == nil {
   259  			options = &LoggingOptions{
   260  				ErrorStream: os.Stderr,
   261  				InfoStream:  os.Stdout,
   262  			}
   263  		}
   264  		log, control := format.factory.Create(*c, *options)
   265  		if control.SetVerbosityLevel != nil {
   266  			setverbositylevel.Mutex.Lock()
   267  			defer setverbositylevel.Mutex.Unlock()
   268  			setverbositylevel.Callbacks = append(setverbositylevel.Callbacks, control.SetVerbosityLevel)
   269  		}
   270  		opts := []klog.LoggerOption{
   271  			klog.ContextualLogger(p.ContextualLoggingEnabled),
   272  			klog.FlushLogger(control.Flush),
   273  		}
   274  		if writer, ok := log.GetSink().(textlogger.KlogBufferWriter); ok {
   275  			opts = append(opts, klog.WriteKlogBuffer(writer.WriteKlogBuffer))
   276  		}
   277  		klog.SetLoggerWithOptions(log, opts...)
   278  	}
   279  	if err := loggingFlags.Lookup("v").Value.Set(VerbosityLevelPflag(&c.Verbosity).String()); err != nil {
   280  		return fmt.Errorf("internal error while setting klog verbosity: %v", err)
   281  	}
   282  	if err := loggingFlags.Lookup("vmodule").Value.Set(VModuleConfigurationPflag(&c.VModule).String()); err != nil {
   283  		return fmt.Errorf("internal error while setting klog vmodule: %v", err)
   284  	}
   285  	klog.StartFlushDaemon(c.FlushFrequency.Duration.Duration)
   286  	klog.EnableContextualLogging(p.ContextualLoggingEnabled)
   287  	return nil
   288  }
   289  
   290  type parameters struct {
   291  	C                        *LoggingConfiguration
   292  	Options                  *LoggingOptions
   293  	ContextualLoggingEnabled bool
   294  }
   295  
   296  var applyParameters atomic.Pointer[parameters]
   297  
   298  // ResetForTest restores the default settings. This is not thread-safe and should only
   299  // be used when there are no goroutines running. The intended users are unit
   300  // tests in other packages.
   301  func ResetForTest(featureGate featuregate.FeatureGate) error {
   302  	oldP := applyParameters.Load()
   303  	if oldP == nil {
   304  		// Nothing to do.
   305  		return nil
   306  	}
   307  
   308  	// This makes it possible to call apply again without triggering errors.
   309  	applyParameters.Store(nil)
   310  
   311  	// Restore defaults. Shouldn't fail, but check anyway.
   312  	config := NewLoggingConfiguration()
   313  	if err := ValidateAndApply(config, featureGate); err != nil {
   314  		return fmt.Errorf("apply default configuration: %v", err)
   315  	}
   316  
   317  	// And again...
   318  	applyParameters.Store(nil)
   319  
   320  	return nil
   321  }
   322  
   323  // AddFlags adds command line flags for the configuration.
   324  func AddFlags(c *LoggingConfiguration, fs *pflag.FlagSet) {
   325  	addFlags(c, fs)
   326  }
   327  
   328  // AddGoFlags is a variant of AddFlags for a standard FlagSet.
   329  func AddGoFlags(c *LoggingConfiguration, fs *flag.FlagSet) {
   330  	addFlags(c, goFlagSet{FlagSet: fs})
   331  }
   332  
   333  // flagSet is the interface implemented by pflag.FlagSet, with
   334  // just those methods defined which are needed by addFlags.
   335  type flagSet interface {
   336  	BoolVar(p *bool, name string, value bool, usage string)
   337  	DurationVar(p *time.Duration, name string, value time.Duration, usage string)
   338  	StringVar(p *string, name string, value string, usage string)
   339  	Var(value pflag.Value, name string, usage string)
   340  	VarP(value pflag.Value, name, shorthand, usage string)
   341  }
   342  
   343  // goFlagSet implements flagSet for a stdlib flag.FlagSet.
   344  type goFlagSet struct {
   345  	*flag.FlagSet
   346  }
   347  
   348  func (fs goFlagSet) Var(value pflag.Value, name string, usage string) {
   349  	fs.FlagSet.Var(value, name, usage)
   350  }
   351  
   352  func (fs goFlagSet) VarP(value pflag.Value, name, shorthand, usage string) {
   353  	// Ignore shorthand, it's not needed and not supported.
   354  	fs.FlagSet.Var(value, name, usage)
   355  }
   356  
   357  // addFlags can be used with both flag.FlagSet and pflag.FlagSet. The internal
   358  // interface definition avoids duplicating this code.
   359  func addFlags(c *LoggingConfiguration, fs flagSet) {
   360  	formats := logRegistry.list()
   361  	fs.StringVar(&c.Format, "logging-format", c.Format, fmt.Sprintf("Sets the log format. Permitted formats: %s.", formats))
   362  	// No new log formats should be added after generation is of flag options
   363  	logRegistry.freeze()
   364  
   365  	fs.DurationVar(&c.FlushFrequency.Duration.Duration, LogFlushFreqFlagName, c.FlushFrequency.Duration.Duration, "Maximum number of seconds between log flushes")
   366  	fs.VarP(VerbosityLevelPflag(&c.Verbosity), "v", "v", "number for the log level verbosity")
   367  	fs.Var(VModuleConfigurationPflag(&c.VModule), "vmodule", "comma-separated list of pattern=N settings for file-filtered logging (only works for text log format)")
   368  
   369  	fs.BoolVar(&c.Options.Text.SplitStream, "log-text-split-stream", false, "[Alpha] In text format, write error messages to stderr and info messages to stdout. The default is to write a single stream to stdout. Enable the LoggingAlphaOptions feature gate to use this.")
   370  	fs.Var(&c.Options.Text.InfoBufferSize, "log-text-info-buffer-size", "[Alpha] In text format with split output streams, the info messages can be buffered for a while to increase performance. The default value of zero bytes disables buffering. The size can be specified as number of bytes (512), multiples of 1000 (1K), multiples of 1024 (2Ki), or powers of those (3M, 4G, 5Mi, 6Gi). Enable the LoggingAlphaOptions feature gate to use this.")
   371  
   372  	// JSON options. We only register them if "json" is a valid format. The
   373  	// config file API however always has them.
   374  	if _, err := logRegistry.get("json"); err == nil {
   375  		fs.BoolVar(&c.Options.JSON.SplitStream, "log-json-split-stream", false, "[Alpha] In JSON format, write error messages to stderr and info messages to stdout. The default is to write a single stream to stdout. Enable the LoggingAlphaOptions feature gate to use this.")
   376  		fs.Var(&c.Options.JSON.InfoBufferSize, "log-json-info-buffer-size", "[Alpha] In JSON format with split output streams, the info messages can be buffered for a while to increase performance. The default value of zero bytes disables buffering. The size can be specified as number of bytes (512), multiples of 1000 (1K), multiples of 1024 (2Ki), or powers of those (3M, 4G, 5Mi, 6Gi). Enable the LoggingAlphaOptions feature gate to use this.")
   377  	}
   378  }
   379  
   380  // SetRecommendedLoggingConfiguration sets the default logging configuration
   381  // for fields that are unset.
   382  //
   383  // Consumers who embed LoggingConfiguration in their own configuration structs
   384  // may set custom defaults and then should call this function to add the
   385  // global defaults.
   386  func SetRecommendedLoggingConfiguration(c *LoggingConfiguration) {
   387  	if c.Format == "" {
   388  		c.Format = "text"
   389  	}
   390  	if c.FlushFrequency.Duration.Duration == 0 {
   391  		c.FlushFrequency.Duration.Duration = LogFlushFreqDefault
   392  		c.FlushFrequency.SerializeAsString = true
   393  	}
   394  	setRecommendedOutputRouting(&c.Options.Text.OutputRoutingOptions)
   395  	setRecommendedOutputRouting(&c.Options.JSON.OutputRoutingOptions)
   396  }
   397  
   398  func setRecommendedOutputRouting(o *OutputRoutingOptions) {
   399  	var empty resource.QuantityValue
   400  	if o.InfoBufferSize == empty {
   401  		o.InfoBufferSize = resource.QuantityValue{
   402  			// This is similar, but not quite the same as a default
   403  			// constructed instance.
   404  			Quantity: *resource.NewQuantity(0, resource.DecimalSI),
   405  		}
   406  		// This sets the unexported Quantity.s which will be compared
   407  		// by reflect.DeepEqual in some tests.
   408  		_ = o.InfoBufferSize.String()
   409  	}
   410  }
   411  
   412  // loggingFlags captures the state of the logging flags, in particular their default value
   413  // before flag parsing. It is used by unsupportedLoggingFlags.
   414  var loggingFlags pflag.FlagSet
   415  
   416  func init() {
   417  	var fs flag.FlagSet
   418  	klogflags.Init(&fs)
   419  	loggingFlags.AddGoFlagSet(&fs)
   420  }
   421  
   422  // List of logs (k8s.io/klog + k8s.io/component-base/logs) flags supported by all logging formats
   423  var supportedLogsFlags = map[string]struct{}{
   424  	"v": {},
   425  }
   426  
   427  // unsupportedLoggingFlags lists unsupported logging flags. The normalize
   428  // function is optional.
   429  func unsupportedLoggingFlags(normalizeFunc func(f *pflag.FlagSet, name string) pflag.NormalizedName) []*pflag.Flag {
   430  	// k8s.io/component-base/logs and klog flags
   431  	pfs := &pflag.FlagSet{}
   432  	loggingFlags.VisitAll(func(flag *pflag.Flag) {
   433  		if _, found := supportedLogsFlags[flag.Name]; !found {
   434  			// Normalization changes flag.Name, so make a copy.
   435  			clone := *flag
   436  			pfs.AddFlag(&clone)
   437  		}
   438  	})
   439  
   440  	// Apply normalization.
   441  	pfs.SetNormalizeFunc(normalizeFunc)
   442  
   443  	var allFlags []*pflag.Flag
   444  	pfs.VisitAll(func(flag *pflag.Flag) {
   445  		allFlags = append(allFlags, flag)
   446  	})
   447  	return allFlags
   448  }
   449  

View as plain text