diff --git a/Gopkg.lock b/Gopkg.lock index 17a086cac88..389b831d045 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1567,6 +1567,7 @@ "github.com/coreos/prometheus-operator/pkg/client/versioned/typed/monitoring/v1", "github.com/ghodss/yaml", "github.com/go-logr/logr", + "github.com/go-logr/zapr", "github.com/markbates/inflect", "github.com/martinlindhe/base36", "github.com/mattbaird/jsonpatch", @@ -1581,6 +1582,8 @@ "github.com/spf13/viper", "github.com/stretchr/testify/assert", "github.com/stretchr/testify/require", + "go.uber.org/zap", + "go.uber.org/zap/zapcore", "golang.org/x/tools/imports", "gopkg.in/yaml.v2", "k8s.io/api/apps/v1", diff --git a/commands/operator-sdk/cmd/run/ansible.go b/commands/operator-sdk/cmd/run/ansible.go index d13e33f493c..190323db6e3 100644 --- a/commands/operator-sdk/cmd/run/ansible.go +++ b/commands/operator-sdk/cmd/run/ansible.go @@ -17,6 +17,7 @@ package run import ( "github.com/operator-framework/operator-sdk/pkg/ansible" aoflags "github.com/operator-framework/operator-sdk/pkg/ansible/flags" + "github.com/operator-framework/operator-sdk/pkg/log/zap" "github.com/spf13/cobra" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" @@ -32,7 +33,7 @@ func RunAnsibleCmd() *cobra.Command { in a Pod inside a cluster. Developers wanting to run their operator locally should use "up local" instead.`, RunE: func(cmd *cobra.Command, args []string) error { - logf.SetLogger(logf.ZapLogger(false)) + logf.SetLogger(zap.Logger()) return ansible.Run(flags) }, diff --git a/commands/operator-sdk/cmd/run/helm.go b/commands/operator-sdk/cmd/run/helm.go index 326d2d884df..122c96561dd 100644 --- a/commands/operator-sdk/cmd/run/helm.go +++ b/commands/operator-sdk/cmd/run/helm.go @@ -17,6 +17,7 @@ package run import ( "github.com/operator-framework/operator-sdk/pkg/helm" hoflags "github.com/operator-framework/operator-sdk/pkg/helm/flags" + "github.com/operator-framework/operator-sdk/pkg/log/zap" "github.com/spf13/cobra" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" @@ -32,7 +33,7 @@ func RunHelmCmd() *cobra.Command { in a Pod inside a cluster. Developers wanting to run their operator locally should use "up local" instead.`, RunE: func(cmd *cobra.Command, args []string) error { - logf.SetLogger(logf.ZapLogger(false)) + logf.SetLogger(zap.Logger()) return helm.Run(flags) }, diff --git a/commands/operator-sdk/cmd/up/local.go b/commands/operator-sdk/cmd/up/local.go index 97651b2a8d6..db0b7093735 100644 --- a/commands/operator-sdk/cmd/up/local.go +++ b/commands/operator-sdk/cmd/up/local.go @@ -31,6 +31,7 @@ import ( "github.com/operator-framework/operator-sdk/pkg/helm" hoflags "github.com/operator-framework/operator-sdk/pkg/helm/flags" "github.com/operator-framework/operator-sdk/pkg/k8sutil" + "github.com/operator-framework/operator-sdk/pkg/log/zap" "github.com/operator-framework/operator-sdk/pkg/scaffold" sdkVersion "github.com/operator-framework/operator-sdk/version" @@ -136,7 +137,7 @@ func upLocal() error { } func upLocalAnsible() error { - logf.SetLogger(logf.ZapLogger(false)) + logf.SetLogger(zap.Logger()) if err := setupOperatorEnv(); err != nil { return err } @@ -144,7 +145,7 @@ func upLocalAnsible() error { } func upLocalHelm() error { - logf.SetLogger(logf.ZapLogger(false)) + logf.SetLogger(zap.Logger()) if err := setupOperatorEnv(); err != nil { return err } diff --git a/doc/user/logging.md b/doc/user/logging.md index df702d7d27b..6a6aa30910d 100644 --- a/doc/user/logging.md +++ b/doc/user/logging.md @@ -8,11 +8,15 @@ Operators set the logger for all operator logging in [`cmd/manager/main.go`][cod ```Go import ( + "github.com/operator-framework/operator-sdk/pkg/log/zap" + "github.com/spf13/pflag" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" ) func main() { - logf.SetLogger(logf.ZapLogger(false)) + pflag.CommandLine.AddFlagSet(zap.FlagSet()) + pflag.Parse() + logf.SetLogger(zap.Logger()) log := logf.Log.WithName("cmd") ... @@ -25,7 +29,19 @@ func main() { By using `controller-runtime/pkg/runtime/log`, your logger is propagated through `controller-runtime`. Any logs produced by `controller-runtime` code will be through your logger, and therefore have the same formatting and destination. -In the above example, `logf.ZapLogger()` takes a boolean flag to set development parameters. Passing in `true` will set the logger to log in development mode; debug log statements will trigger, and error log statements will include stack traces. +### Default zap logger + +Operator SDK uses a `zap`-based `logr` backend when scaffolding new projects. To assist with configuring and using this logger, the SDK includes several helper functions. + +In the above example, we add the zap flagset to the operator's command line flags with `zap.FlagSet()`, and then set the controller-runtime logger with `zap.Logger()`. + +By default, `zap.Logger()` will return a logger that is ready for production use. It uses a JSON encoder, logs starting at the `info` level, and has [sampling][zap_sampling] enabled. To customize the default behavior, users can use the zap flagset and specify flags on the command line. The zap flagset includes the following flags that can be used to configure the logger: +* `--zap-devel` - Enables the zap development config (changes defaults to console encoder, debug log level, and disables sampling) (default: `false`) +* `--zap-encoder` string - Sets the zap log encoding (`json` or `console`) +* `--zap-level` string - Sets the zap log level (`debug`, `info`, or `error`) +* `--zap-sample` - Enables zap's sampling mode + +**NOTE:** Although the `logr` interface supports multiple debug levels (e.g. `log.V(1).Info()`, `log.V(2).Info()`, etc.), zap supports only a single debug level with `log.V(1).Info()`. Log statements with higher debug levels will not be printed with the zap's `logr` backend. ## Creating a structured log statement @@ -102,4 +118,5 @@ If you do not want to use `logr` as your logging tool, you can remove `logr`-spe [godoc_logr_logger]:https://godoc.org/github.com/go-logr/logr#Logger [site_struct_logging]:https://www.client9.com/structured-logging-in-golang/ [code_memcached_controller]:../../example/memcached-operator/memcached_controller.go.tmpl -[code_set_logger]:https://github.com/operator-framework/operator-sdk/blob/948139171fff0e802c9e68f87cb95939941772ef/pkg/scaffold/cmd.go#L68-L72 +[code_set_logger]:https://github.com/operator-framework/operator-sdk/blob/ecd02000616f11303f1adecd3d4ceb4a8561a9ec/pkg/scaffold/cmd.go#L90-L94 +[zap_sampling]:https://github.com/uber-go/zap/blob/master/FAQ.md#why-sample-application-logs diff --git a/images/scorecard-proxy/cmd/proxy/main.go b/images/scorecard-proxy/cmd/proxy/main.go index 6c4ddd662f2..d3a4b6d01bf 100644 --- a/images/scorecard-proxy/cmd/proxy/main.go +++ b/images/scorecard-proxy/cmd/proxy/main.go @@ -15,22 +15,25 @@ package main import ( - "flag" "os" proxy "github.com/operator-framework/operator-sdk/pkg/ansible/proxy" "github.com/operator-framework/operator-sdk/pkg/ansible/proxy/controllermap" "github.com/operator-framework/operator-sdk/pkg/k8sutil" + "github.com/operator-framework/operator-sdk/pkg/log/zap" log "github.com/sirupsen/logrus" + "github.com/spf13/pflag" "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/controller-runtime/pkg/manager" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" ) func main() { - flag.Parse() - logf.SetLogger(logf.ZapLogger(false)) + pflag.CommandLine.AddFlagSet(zap.FlagSet()) + pflag.Parse() + + logf.SetLogger(zap.Logger()) namespace, found := os.LookupEnv(k8sutil.WatchNamespaceEnvVar) if found { diff --git a/pkg/ansible/flags/flag.go b/pkg/ansible/flags/flag.go index 278f6c6e381..0830351ace7 100644 --- a/pkg/ansible/flags/flag.go +++ b/pkg/ansible/flags/flag.go @@ -16,6 +16,7 @@ package flags import ( "github.com/operator-framework/operator-sdk/pkg/internal/flags" + "github.com/operator-framework/operator-sdk/pkg/log/zap" "github.com/spf13/pflag" ) @@ -29,5 +30,6 @@ type AnsibleOperatorFlags struct { func AddTo(flagSet *pflag.FlagSet, helpTextPrefix ...string) *AnsibleOperatorFlags { aof := &AnsibleOperatorFlags{} aof.WatchFlags.AddTo(flagSet, helpTextPrefix...) + flagSet.AddFlagSet(zap.FlagSet()) return aof } diff --git a/pkg/helm/flags/flag.go b/pkg/helm/flags/flag.go index 239ee19d011..ee41e869e9d 100644 --- a/pkg/helm/flags/flag.go +++ b/pkg/helm/flags/flag.go @@ -16,6 +16,7 @@ package flags import ( "github.com/operator-framework/operator-sdk/pkg/internal/flags" + "github.com/operator-framework/operator-sdk/pkg/log/zap" "github.com/spf13/pflag" ) @@ -29,5 +30,6 @@ type HelmOperatorFlags struct { func AddTo(flagSet *pflag.FlagSet, helpTextPrefix ...string) *HelmOperatorFlags { hof := &HelmOperatorFlags{} hof.WatchFlags.AddTo(flagSet, helpTextPrefix...) + flagSet.AddFlagSet(zap.FlagSet()) return hof } diff --git a/pkg/log/zap/flags.go b/pkg/log/zap/flags.go new file mode 100644 index 00000000000..67d0080edd8 --- /dev/null +++ b/pkg/log/zap/flags.go @@ -0,0 +1,131 @@ +// Copyright 2019 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package zap + +import ( + "fmt" + "strconv" + "strings" + + "github.com/spf13/pflag" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var ( + zapFlagSet *pflag.FlagSet + + development bool + encoderVal encoderValue + levelVal levelValue + sampleVal sampleValue +) + +func init() { + zapFlagSet = pflag.NewFlagSet("zap", pflag.ExitOnError) + zapFlagSet.BoolVar(&development, "zap-devel", false, "Enable zap development mode (changes defaults to console encoder, debug log level, and disables sampling)") + zapFlagSet.Var(&encoderVal, "zap-encoder", "Zap log encoding ('json' or 'console')") + zapFlagSet.Var(&levelVal, "zap-level", "Zap log level (one of 'debug', 'info', 'error')") + zapFlagSet.Var(&sampleVal, "zap-sample", "Enable zap log sampling") +} + +func FlagSet() *pflag.FlagSet { + return zapFlagSet +} + +type encoderValue struct { + set bool + encoder zapcore.Encoder + str string +} + +func (v *encoderValue) Set(e string) error { + v.set = true + switch e { + case "json": + v.encoder = jsonEncoder() + case "console": + v.encoder = consoleEncoder() + default: + return fmt.Errorf("unknown encoder \"%s\"", e) + } + v.str = e + return nil +} + +func (v encoderValue) String() string { + return v.str +} + +func (v encoderValue) Type() string { + return "encoder" +} + +func jsonEncoder() zapcore.Encoder { + encoderConfig := zap.NewProductionEncoderConfig() + return zapcore.NewJSONEncoder(encoderConfig) +} + +func consoleEncoder() zapcore.Encoder { + encoderConfig := zap.NewDevelopmentEncoderConfig() + return zapcore.NewConsoleEncoder(encoderConfig) +} + +type levelValue struct { + set bool + level zapcore.Level +} + +func (v *levelValue) Set(l string) error { + v.set = true + lower := strings.ToLower(l) + switch lower { + case "debug", "info", "error": + return v.level.Set(l) + } + return fmt.Errorf("invalid log level \"%s\"", l) +} + +func (v levelValue) String() string { + return v.level.String() +} + +func (v levelValue) Type() string { + return "level" +} + +type sampleValue struct { + set bool + sample bool +} + +func (v *sampleValue) Set(s string) error { + var err error + v.set = true + v.sample, err = strconv.ParseBool(s) + return err +} + +func (v sampleValue) String() string { + return strconv.FormatBool(v.sample) +} + +func (v sampleValue) IsBoolFlag() bool { + return true +} + +func (v sampleValue) Type() string { + return "sample" +} diff --git a/pkg/log/zap/logger.go b/pkg/log/zap/logger.go new file mode 100644 index 00000000000..316669544bb --- /dev/null +++ b/pkg/log/zap/logger.go @@ -0,0 +1,84 @@ +// Copyright 2019 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package zap + +import ( + "io" + "os" + "time" + + "github.com/go-logr/logr" + "github.com/go-logr/zapr" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" +) + +func Logger() logr.Logger { + return LoggerTo(os.Stderr) +} + +func LoggerTo(destWriter io.Writer) logr.Logger { + syncer := zapcore.AddSync(destWriter) + conf := getConfig(destWriter) + + conf.encoder = &logf.KubeAwareEncoder{Encoder: conf.encoder, Verbose: conf.level.Level() < 0} + if conf.sample { + conf.opts = append(conf.opts, zap.WrapCore(func(core zapcore.Core) zapcore.Core { + return zapcore.NewSampler(core, time.Second, 100, 100) + })) + } + conf.opts = append(conf.opts, zap.AddCallerSkip(1), zap.ErrorOutput(syncer)) + log := zap.New(zapcore.NewCore(conf.encoder, syncer, conf.level)) + log = log.WithOptions(conf.opts...) + return zapr.NewLogger(log) +} + +type config struct { + encoder zapcore.Encoder + level zap.AtomicLevel + sample bool + opts []zap.Option +} + +func getConfig(destWriter io.Writer) config { + var c config + + // Set the defaults depending on the log mode (development vs. production) + if development { + c.encoder = consoleEncoder() + c.level = zap.NewAtomicLevelAt(zap.DebugLevel) + c.opts = append(c.opts, zap.Development(), zap.AddStacktrace(zap.ErrorLevel)) + c.sample = false + } else { + c.encoder = jsonEncoder() + c.level = zap.NewAtomicLevelAt(zap.InfoLevel) + c.opts = append(c.opts, zap.AddStacktrace(zap.WarnLevel)) + c.sample = true + } + + // Override the defaults if the flags were set explicitly on the command line + if encoderVal.set { + c.encoder = encoderVal.encoder + } + if levelVal.set { + c.level = zap.NewAtomicLevelAt(levelVal.level) + } + if sampleVal.set { + c.sample = sampleVal.sample + } + + return c +} diff --git a/pkg/scaffold/ansible/main.go b/pkg/scaffold/ansible/main.go index 8e998428e39..a85a681ae12 100644 --- a/pkg/scaffold/ansible/main.go +++ b/pkg/scaffold/ansible/main.go @@ -40,16 +40,16 @@ import ( aoflags "github.com/operator-framework/operator-sdk/pkg/ansible/flags" "github.com/operator-framework/operator-sdk/pkg/ansible" + "github.com/operator-framework/operator-sdk/pkg/log/zap" "github.com/spf13/pflag" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" ) func main() { - logf.SetLogger(logf.ZapLogger(false)) - aflags := aoflags.AddTo(pflag.CommandLine) pflag.Parse() + logf.SetLogger(zap.Logger()) if err := ansible.Run(aflags); err != nil { logf.Log.WithName("cmd").Error(err, "") diff --git a/pkg/scaffold/cmd.go b/pkg/scaffold/cmd.go index 15aa2f80754..1447b035d4d 100644 --- a/pkg/scaffold/cmd.go +++ b/pkg/scaffold/cmd.go @@ -48,8 +48,10 @@ import ( "github.com/operator-framework/operator-sdk/pkg/k8sutil" "github.com/operator-framework/operator-sdk/pkg/leader" + "github.com/operator-framework/operator-sdk/pkg/log/zap" "github.com/operator-framework/operator-sdk/pkg/metrics" sdkVersion "github.com/operator-framework/operator-sdk/version" + "github.com/spf13/pflag" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -71,13 +73,25 @@ func printVersion() { } func main() { - flag.Parse() + // Add the zap logger flag set to the CLI. The flag set must + // be added before calling pflag.Parse(). + pflag.CommandLine.AddFlagSet(zap.FlagSet()) + // Add flags registered by imported packages (e.g. glog and + // controller-runtime) + pflag.CommandLine.AddGoFlagSet(flag.CommandLine) + + pflag.Parse() + + // Use a zap logr.Logger implementation. If none of the zap + // flags are configured (or if the zap flag set is not being + // used), this defaults to a production zap logger. + // // The logger instantiated here can be changed to any logger // implementing the logr.Logger interface. This logger will // be propagated through the whole operator, generating // uniform and structured logs. - logf.SetLogger(logf.ZapLogger(false)) + logf.SetLogger(zap.Logger()) printVersion() diff --git a/pkg/scaffold/cmd_test.go b/pkg/scaffold/cmd_test.go index 62201b83a66..fb04fb7e6b1 100644 --- a/pkg/scaffold/cmd_test.go +++ b/pkg/scaffold/cmd_test.go @@ -46,8 +46,10 @@ import ( "github.com/example-inc/app-operator/pkg/controller" "github.com/operator-framework/operator-sdk/pkg/k8sutil" "github.com/operator-framework/operator-sdk/pkg/leader" + "github.com/operator-framework/operator-sdk/pkg/log/zap" "github.com/operator-framework/operator-sdk/pkg/metrics" sdkVersion "github.com/operator-framework/operator-sdk/version" + "github.com/spf13/pflag" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -69,13 +71,25 @@ func printVersion() { } func main() { - flag.Parse() + // Add the zap logger flag set to the CLI. The flag set must + // be added before calling pflag.Parse(). + pflag.CommandLine.AddFlagSet(zap.FlagSet()) + // Add flags registered by imported packages (e.g. glog and + // controller-runtime) + pflag.CommandLine.AddGoFlagSet(flag.CommandLine) + + pflag.Parse() + + // Use a zap logr.Logger implementation. If none of the zap + // flags are configured (or if the zap flag set is not being + // used), this defaults to a production zap logger. + // // The logger instantiated here can be changed to any logger // implementing the logr.Logger interface. This logger will // be propagated through the whole operator, generating // uniform and structured logs. - logf.SetLogger(logf.ZapLogger(false)) + logf.SetLogger(zap.Logger()) printVersion() diff --git a/pkg/scaffold/helm/main.go b/pkg/scaffold/helm/main.go index 3390c969ce2..c1440005aad 100644 --- a/pkg/scaffold/helm/main.go +++ b/pkg/scaffold/helm/main.go @@ -40,16 +40,16 @@ import ( hoflags "github.com/operator-framework/operator-sdk/pkg/helm/flags" "github.com/operator-framework/operator-sdk/pkg/helm" + "github.com/operator-framework/operator-sdk/pkg/log/zap" "github.com/spf13/pflag" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" ) func main() { - logf.SetLogger(logf.ZapLogger(false)) - hflags := hoflags.AddTo(pflag.CommandLine) pflag.Parse() + logf.SetLogger(zap.Logger()) if err := helm.Run(hflags); err != nil { logf.Log.WithName("cmd").Error(err, "") diff --git a/test/test-framework/cmd/manager/main.go b/test/test-framework/cmd/manager/main.go index da68cdd953b..0a3eefb1b53 100644 --- a/test/test-framework/cmd/manager/main.go +++ b/test/test-framework/cmd/manager/main.go @@ -23,10 +23,12 @@ import ( "github.com/operator-framework/operator-sdk/pkg/k8sutil" "github.com/operator-framework/operator-sdk/pkg/leader" + "github.com/operator-framework/operator-sdk/pkg/log/zap" "github.com/operator-framework/operator-sdk/pkg/ready" "github.com/operator-framework/operator-sdk/test/test-framework/pkg/apis" "github.com/operator-framework/operator-sdk/test/test-framework/pkg/controller" sdkVersion "github.com/operator-framework/operator-sdk/version" + "github.com/spf13/pflag" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -43,13 +45,25 @@ func printVersion() { } func main() { - flag.Parse() + // Add the zap logger flag set to the CLI. The flag set must + // be added before calling pflag.Parse(). + pflag.CommandLine.AddFlagSet(zap.FlagSet()) + // Add flags registered by imported packages (e.g. glog and + // controller-runtime) + pflag.CommandLine.AddGoFlagSet(flag.CommandLine) + + pflag.Parse() + + // Use a zap logr.Logger implementation. If none of the zap + // flags are configured (or if the zap flag set is not being + // used), this defaults to a production zap logger. + // // The logger instantiated here can be changed to any logger // implementing the logr.Logger interface. This logger will // be propagated through the whole operator, generating // uniform and structured logs. - logf.SetLogger(logf.ZapLogger(false)) + logf.SetLogger(zap.Logger()) printVersion()