From c4d162188331839fabc7463303d05f5625a35c31 Mon Sep 17 00:00:00 2001 From: Jialun Cai Date: Tue, 17 Sep 2024 12:45:05 +1000 Subject: [PATCH] Add support for JSON-formatted logging --- .../app/controllermanager.go | 4 ++ .../controller-manager.go | 12 +--- pkg/log/flag.go | 67 +++++++++++++++++++ pkg/log/logger.go | 37 ++++++++++ pkg/log/slog.go | 65 ++++++++++++++++++ pkg/log/std.go | 25 +++++++ 6 files changed, 201 insertions(+), 9 deletions(-) create mode 100644 pkg/log/flag.go create mode 100644 pkg/log/slog.go create mode 100644 pkg/log/std.go diff --git a/cmd/cloud-controller-manager/app/controllermanager.go b/cmd/cloud-controller-manager/app/controllermanager.go index b1b8a5955c..07b2ba65e9 100644 --- a/cmd/cloud-controller-manager/app/controllermanager.go +++ b/cmd/cloud-controller-manager/app/controllermanager.go @@ -83,6 +83,8 @@ func NewCloudControllerManagerCommand() *cobra.Command { Use: "cloud-controller-manager", Long: `The Cloud controller manager is a daemon that embeds the cloud specific control loops shipped with Kubernetes.`, Run: func(cmd *cobra.Command, _ []string) { + log.Setup(log.OptionsFromCLIFlags(cmd.Flags())) + defer log.Flush() verflag.PrintAndExitIfRequested("Cloud Provider Azure") cliflag.PrintFlags(cmd.Flags()) @@ -203,6 +205,8 @@ func NewCloudControllerManagerCommand() *cobra.Command { cliflag.PrintSections(cmd.OutOrStdout(), namedFlagSets, cols) }) + log.BindCLIFlags(fs) + return cmd } diff --git a/cmd/cloud-controller-manager/controller-manager.go b/cmd/cloud-controller-manager/controller-manager.go index 78af7b0368..8631ff3825 100644 --- a/cmd/cloud-controller-manager/controller-manager.go +++ b/cmd/cloud-controller-manager/controller-manager.go @@ -24,25 +24,19 @@ import ( "os" "time" - "k8s.io/component-base/logs" _ "k8s.io/component-base/metrics/prometheus/clientgo" // load all the prometheus client-go plugins "sigs.k8s.io/cloud-provider-azure/cmd/cloud-controller-manager/app" + "sigs.k8s.io/cloud-provider-azure/pkg/log" _ "sigs.k8s.io/cloud-provider-azure/pkg/provider" ) func main() { - rand.Seed(time.Now().UnixNano()) + rand.Seed(time.Now().UnixNano()) // FIXME: should use crypto/rand for better randomness + defer log.Flush() command := app.NewCloudControllerManagerCommand() - // TODO: once we switch everything over to Cobra commands, we can go back to calling - // utilflag.InitFlags() (by removing its pflag.Parse() call). For now, we have to set the - // normalize func and add the go flag set by hand. - // utilflag.InitFlags() - logs.InitLogs() - defer logs.FlushLogs() - if err := command.Execute(); err != nil { os.Exit(1) } diff --git a/pkg/log/flag.go b/pkg/log/flag.go new file mode 100644 index 0000000000..b6422e8c23 --- /dev/null +++ b/pkg/log/flag.go @@ -0,0 +1,67 @@ +/* +Copyright 2024 The Kubernetes 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 log + +import ( + "time" + + "github.com/spf13/pflag" +) + +type Format string + +const ( + JSON Format = "json" + Text Format = "text" +) + +const ( + flagLogFormat = "log-format" + flagLogFlushFrequency = "log-flush-frequency" + + DefaultFormat = Text + DefaultFlushFrequency = 5 * time.Second +) + +func BindCLIFlags(fs *pflag.FlagSet) { + fs.String(flagLogFormat, "text", "The log format to use. One of: text, json.") + // log-flush-frequency is registered in kubernetes component-base. +} + +type Options struct { + Format Format + FlushFrequency time.Duration +} + +func OptionsFromCLIFlags(fs *pflag.FlagSet) *Options { + var o Options + + o.Format = Format(fs.Lookup(flagLogFormat).Value.String()) + if o.Format != JSON && o.Format != Text { + SetupError("Invalid log format", flagLogFormat, o.Format) + o.Format = DefaultFormat + } + + freq, err := fs.GetDuration(flagLogFlushFrequency) + if err != nil { + SetupError("Invalid log flush frequency", flagLogFlushFrequency, err) + o.FlushFrequency = DefaultFlushFrequency + } + o.FlushFrequency = freq + + return &o +} diff --git a/pkg/log/logger.go b/pkg/log/logger.go index 8afd9f42fd..a7982b82a2 100644 --- a/pkg/log/logger.go +++ b/pkg/log/logger.go @@ -20,8 +20,12 @@ import ( "context" "encoding/json" "fmt" + "log" + "log/slog" + "os" "github.com/go-logr/logr" + "k8s.io/component-base/logs" "k8s.io/klog/v2" ) @@ -65,3 +69,36 @@ func ValueAsMap(value any) map[string]any { return rv } + +// Setup initializes the logging system. +func Setup(opts *Options) { + log.SetOutput(logs.KlogWriter{}) + log.SetFlags(0) + klog.StartFlushDaemon(opts.FlushFrequency) + + if opts.Format != JSON { + return + } + + logger := slog.New(NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.Level(-127), // Set to minimum level; actual filtering is handled by klog.V(N) + AddSource: true, + ReplaceAttr: func(_ []string, attr slog.Attr) slog.Attr { + switch attr.Key { + case slog.TimeKey: + // Avoid non-deterministic time field + return slog.Attr{} + default: + return attr + } + }, + })) + + slog.SetDefault(logger) + klog.SetSlogLogger(logger) +} + +// Flush flushes logs immediately. +func Flush() { + klog.Flush() +} diff --git a/pkg/log/slog.go b/pkg/log/slog.go new file mode 100644 index 0000000000..1fa05d10f5 --- /dev/null +++ b/pkg/log/slog.go @@ -0,0 +1,65 @@ +/* +Copyright 2024 The Kubernetes 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 log + +import ( + "context" + "io" + "log/slog" +) + +const ( + TimeKey = slog.TimeKey + LevelKey = slog.LevelKey + VerbosityKey = "v" +) + +type SLogJSONHandler struct { + handler *slog.JSONHandler +} + +func NewJSONHandler(w io.Writer, opts *slog.HandlerOptions) *SLogJSONHandler { + return &SLogJSONHandler{ + handler: slog.NewJSONHandler(w, opts), + } +} + +func (h *SLogJSONHandler) Enabled(ctx context.Context, level slog.Level) bool { + return h.handler.Enabled(ctx, level) +} + +func (h *SLogJSONHandler) Handle(ctx context.Context, record slog.Record) error { + // Extract verbosity from negative log levels and set it as an attribute. + // This is necessary because slog will convert negative levels to DEBUG+N. + if record.Level < 0 { + verbosity := int(-record.Level) + record.Level = slog.LevelInfo + record.AddAttrs(slog.Int(VerbosityKey, verbosity)) + } else { + record.AddAttrs(slog.Int(VerbosityKey, 0)) + } + + return h.handler.Handle(ctx, record) +} + +func (h *SLogJSONHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return h.handler.WithAttrs(attrs) +} + +func (h *SLogJSONHandler) WithGroup(name string) slog.Handler { + return h.handler.WithGroup(name) +} diff --git a/pkg/log/std.go b/pkg/log/std.go new file mode 100644 index 0000000000..dafcc64916 --- /dev/null +++ b/pkg/log/std.go @@ -0,0 +1,25 @@ +/* +Copyright 2024 The Kubernetes 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 log + +import ( + "log/slog" +) + +func SetupError(msg string, args ...any) { + slog.Error(msg, args...) +}