k8s.io/component-base/logs: allow overriding os.Stdout and os.Stderr

This is useful for tests which need to discard or capture the output.
This commit is contained in:
Patrick Ohly 2022-12-21 16:41:59 +01:00
parent 9b86f457e9
commit a41424d4c8
4 changed files with 55 additions and 45 deletions

View File

@ -19,7 +19,9 @@ package v1
import (
"flag"
"fmt"
"io"
"math"
"os"
"strings"
"time"
@ -63,18 +65,41 @@ func NewLoggingConfiguration() *LoggingConfiguration {
// The optional FeatureGate controls logging features. If nil, the default for
// these features is used.
func ValidateAndApply(c *LoggingConfiguration, featureGate featuregate.FeatureGate) error {
return ValidateAndApplyAsField(c, featureGate, nil)
return validateAndApply(c, nil, featureGate, nil)
}
// ValidateAndApplyWithOptions is a variant of ValidateAndApply which accepts
// additional options beyond those that can be configured through the API. This
// is meant for testing.
func ValidateAndApplyWithOptions(c *LoggingConfiguration, options *LoggingOptions, featureGate featuregate.FeatureGate) error {
return validateAndApply(c, options, featureGate, nil)
}
// +k8s:deepcopy-gen=false
// LoggingOptions can be used with ValidateAndApplyWithOptions to override
// certain global defaults.
type LoggingOptions struct {
// ErrorStream can be used to override the os.Stderr default.
ErrorStream io.Writer
// InfoStream can be used to override the os.Stdout default.
InfoStream io.Writer
}
// ValidateAndApplyAsField is a variant of ValidateAndApply that should be used
// when the LoggingConfiguration is embedded in some larger configuration
// structure.
func ValidateAndApplyAsField(c *LoggingConfiguration, featureGate featuregate.FeatureGate, fldPath *field.Path) error {
return validateAndApply(c, nil, featureGate, fldPath)
}
func validateAndApply(c *LoggingConfiguration, options *LoggingOptions, featureGate featuregate.FeatureGate, fldPath *field.Path) error {
errs := Validate(c, featureGate, fldPath)
if len(errs) > 0 {
return errs.ToAggregate()
}
return apply(c, featureGate)
return apply(c, options, featureGate)
}
// Validate can be used to check for invalid settings without applying them.
@ -157,7 +182,7 @@ func featureEnabled(featureGate featuregate.FeatureGate, feature featuregate.Fea
return enabled
}
func apply(c *LoggingConfiguration, featureGate featuregate.FeatureGate) error {
func apply(c *LoggingConfiguration, options *LoggingOptions, featureGate featuregate.FeatureGate) error {
contextualLoggingEnabled := contextualLoggingDefault
if featureGate != nil {
contextualLoggingEnabled = featureGate.Enabled(ContextualLogging)
@ -168,7 +193,13 @@ func apply(c *LoggingConfiguration, featureGate featuregate.FeatureGate) error {
if format.factory == nil {
klog.ClearLogger()
} else {
log, control := format.factory.Create(*c)
if options == nil {
options = &LoggingOptions{
ErrorStream: os.Stderr,
InfoStream: os.Stdout,
}
}
log, control := format.factory.Create(*c, *options)
if control.SetVerbosityLevel != nil {
setverbositylevel.Mutex.Lock()
defer setverbositylevel.Mutex.Unlock()

View File

@ -61,7 +61,7 @@ type RuntimeControl struct {
// non-default log format.
type LogFormatFactory interface {
// Create returns a logger with the requested configuration.
Create(c LoggingConfiguration) (logr.Logger, RuntimeControl)
Create(c LoggingConfiguration, o LoggingOptions) (logr.Logger, RuntimeControl)
}
// RegisterLogFormat registers support for a new logging format. This must be called

View File

@ -18,7 +18,6 @@ package json
import (
"io"
"os"
"sync/atomic"
"time"
@ -116,7 +115,7 @@ func (f Factory) Feature() featuregate.Feature {
return logsapi.LoggingBetaOptions
}
func (f Factory) Create(c logsapi.LoggingConfiguration) (logr.Logger, logsapi.RuntimeControl) {
func (f Factory) Create(c logsapi.LoggingConfiguration, o logsapi.LoggingOptions) (logr.Logger, logsapi.RuntimeControl) {
// We intentionally avoid all os.File.Sync calls. Output is unbuffered,
// therefore we don't need to flush, and calling the underlying fsync
// would just slow down writing.
@ -125,9 +124,9 @@ func (f Factory) Create(c logsapi.LoggingConfiguration) (logr.Logger, logsapi.Ru
// written to the output stream before the process terminates, but
// doesn't need to worry about data not being written because of a
// system crash or powerloss.
stderr := zapcore.Lock(AddNopSync(os.Stderr))
stderr := zapcore.Lock(AddNopSync(o.ErrorStream))
if c.Options.JSON.SplitStream {
stdout := zapcore.Lock(AddNopSync(os.Stdout))
stdout := zapcore.Lock(AddNopSync(o.InfoStream))
size := c.Options.JSON.InfoBufferSize.Value()
if size > 0 {
// Prevent integer overflow.

View File

@ -32,7 +32,6 @@ import (
"testing"
"time"
"github.com/go-logr/logr"
logsapi "k8s.io/component-base/logs/api/v1"
logsjson "k8s.io/component-base/logs/json"
"k8s.io/klog/v2"
@ -175,9 +174,6 @@ func benchmarkOutputFormats(b *testing.B, config loadGeneratorConfig, discard bo
generateOutput(b, config, nil, out)
})
b.Run("JSON", func(b *testing.B) {
c := logsapi.NewLoggingConfiguration()
var logger logr.Logger
var flush func()
var out1, out2 *os.File
if !discard {
var err error
@ -192,46 +188,30 @@ func benchmarkOutputFormats(b *testing.B, config loadGeneratorConfig, discard bo
}
defer out2.Close()
}
o := logsapi.LoggingOptions{}
if discard {
o.ErrorStream = io.Discard
o.InfoStream = io.Discard
} else {
o.ErrorStream = out1
o.InfoStream = out1
}
b.Run("single-stream", func(b *testing.B) {
if discard {
l, control := logsjson.NewJSONLogger(c.Verbosity, logsjson.AddNopSync(&output), nil, nil)
logger = l
flush = control.Flush
} else {
stderr := os.Stderr
os.Stderr = out1
defer func() {
os.Stderr = stderr
}()
l, control := logsjson.Factory{}.Create(*c)
logger = l
flush = control.Flush
}
c := logsapi.NewLoggingConfiguration()
logger, control := logsjson.Factory{}.Create(*c, o)
klog.SetLogger(logger)
defer klog.ClearLogger()
generateOutput(b, config, flush, out1)
generateOutput(b, config, control.Flush, out1)
})
b.Run("split-stream", func(b *testing.B) {
if discard {
l, control := logsjson.NewJSONLogger(c.Verbosity, logsjson.AddNopSync(&output), logsjson.AddNopSync(&output), nil)
logger = l
flush = control.Flush
} else {
stdout, stderr := os.Stdout, os.Stderr
os.Stdout, os.Stderr = out1, out2
defer func() {
os.Stdout, os.Stderr = stdout, stderr
}()
c := logsapi.NewLoggingConfiguration()
c.Options.JSON.SplitStream = true
l, control := logsjson.Factory{}.Create(*c)
logger = l
flush = control.Flush
}
c := logsapi.NewLoggingConfiguration()
c.Options.JSON.SplitStream = true
logger, control := logsjson.Factory{}.Create(*c, o)
klog.SetLogger(logger)
defer klog.ClearLogger()
generateOutput(b, config, flush, out1, out2)
generateOutput(b, config, control.Flush, out1, out2)
})
})
}