From 598a984cb3d7089f837d6b63403ae2a0a93027f2 Mon Sep 17 00:00:00 2001 From: mingfu Date: Thu, 21 Mar 2024 13:22:00 +0800 Subject: [PATCH] feat: support update klog log level dynamically based on configmap --- logging/config.go | 21 +++++++++++ logging/config_test.go | 73 ++++++++++++++++++++++++++++++++++++ logging/klog.go | 45 ++++++++++++++++++++++ logging/klog_test.go | 69 ++++++++++++++++++++++++++++++++++ logging/suite_test.go | 41 ++++++++++++++++++++ logging/testdata/config.yaml | 31 +++++++++++++++ sharedmain/app.go | 18 ++++++++- 7 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 logging/config_test.go create mode 100644 logging/klog.go create mode 100644 logging/klog_test.go create mode 100644 logging/suite_test.go create mode 100644 logging/testdata/config.yaml diff --git a/logging/config.go b/logging/config.go index 1ba6706d..1d689009 100644 --- a/logging/config.go +++ b/logging/config.go @@ -50,6 +50,9 @@ type LevelManager struct { Locker *sync.Mutex Name string + // customObservers is a list of custom observers that will be invoked when the log config is updated + customObservers []func(map[string]string) + *zap.SugaredLogger } @@ -147,6 +150,17 @@ func (l *LevelManager) Update() func(configMap *corev1.ConfigMap) { v.Level.SetLevel(level) } } + + for _, observer := range l.customObservers { + func() { + defer func() { + if invokeErr := recover(); invokeErr != nil { + l.Errorw("failed to invoke log config updater", "err", err) + } + }() + observer(configMap.Data) + }() + } } } @@ -162,3 +176,10 @@ func (l *LevelManager) Get(name string) zap.AtomicLevel { } return l.ControllerLevelMap[name].Level } + +// AddCustomObserver add a custom observer to the level manager +func (l *LevelManager) AddCustomObserver(f func(map[string]string)) { + l.Locker.Lock() + defer l.Locker.Unlock() + l.customObservers = append(l.customObservers, f) +} diff --git a/logging/config_test.go b/logging/config_test.go new file mode 100644 index 00000000..d7459367 --- /dev/null +++ b/logging/config_test.go @@ -0,0 +1,73 @@ +/* +Copyright 2024 The Katanomi 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 logging + +import ( + "context" + + "github.com/google/go-cmp/cmp" + "github.com/katanomi/pkg/testing" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + "knative.dev/pkg/logging" +) + +var _ = Describe("LevelManager_Update", func() { + var lm LevelManager + var cm = &corev1.ConfigMap{} + testing.MustLoadYaml("testdata/config.yaml", cm) + + BeforeEach(func() { + ctx := logging.WithLogger(context.Background(), logger) + lm = NewLevelManager(ctx, "test") + }) + + JustBeforeEach(func() { + lm.Update()(cm) + }) + + Context("test for AtomicLevel", func() { + var infoLevel zap.AtomicLevel + var errorLevel zap.AtomicLevel + BeforeEach(func() { + infoLevel = lm.Get("test_info") + errorLevel = lm.Get("test_error") + }) + + It("should get correct log level", func() { + Expect(infoLevel.Level()).To(Equal(zap.InfoLevel)) + Expect(errorLevel.Level()).To(Equal(zap.ErrorLevel)) + }) + }) + + Context("test for custom observer", func() { + var gotData map[string]string + BeforeEach(func() { + lm.AddCustomObserver(func(d map[string]string) { + gotData = d + }) + }) + + It("should get full data of the configmap", func() { + expectCm := &corev1.ConfigMap{} + testing.MustLoadYaml("testdata/config.yaml", expectCm) + Expect(cmp.Diff(expectCm.Data, gotData)).To(BeEmpty()) + }) + }) +}) diff --git a/logging/klog.go b/logging/klog.go new file mode 100644 index 00000000..9853c564 --- /dev/null +++ b/logging/klog.go @@ -0,0 +1,45 @@ +/* +Copyright 2024 The Katanomi 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 logging + +import "strconv" + +// KlogLevelKey the key to set klog log level in the configmap +// Example: +// +// data: +// +// klog.level: "9" +const KlogLevelKey = "klog.level" + +// GetKlogLevelFromConfigMapData get klog level from configmap data +func GetKlogLevelFromConfigMapData(data map[string]string) (level string) { + if data == nil { + return "0" + } + + level = data[KlogLevelKey] + if level == "" { + return "0" + } + + _, err := strconv.Atoi(level) + if err != nil { + return "0" + } + return level +} diff --git a/logging/klog_test.go b/logging/klog_test.go new file mode 100644 index 00000000..73e144a1 --- /dev/null +++ b/logging/klog_test.go @@ -0,0 +1,69 @@ +/* +Copyright 2024 The Katanomi 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 logging + +import ( + "testing" + + . "github.com/onsi/gomega" +) + +func TestGetKlogLevelFromConfigMapData(t *testing.T) { + g := NewGomegaWithT(t) + tests := []struct { + name string + data map[string]string + wantLevel string + }{ + { + name: "nil config", + data: nil, + wantLevel: "0", + }, + { + name: "empty config", + data: map[string]string{}, + wantLevel: "0", + }, + { + name: "not exist the key klog.level", + data: map[string]string{ + "a": "b", + }, + wantLevel: "0", + }, + { + name: "exist the key klog.level and the value is correct number", + data: map[string]string{ + KlogLevelKey: "3", + }, + wantLevel: "3", + }, + { + name: "exist the key klog.level and the value is not a number", + data: map[string]string{ + KlogLevelKey: "a3", + }, + wantLevel: "0", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g.Expect(GetKlogLevelFromConfigMapData(tt.data)).To(Equal(tt.wantLevel)) + }) + } +} diff --git a/logging/suite_test.go b/logging/suite_test.go new file mode 100644 index 00000000..bf574837 --- /dev/null +++ b/logging/suite_test.go @@ -0,0 +1,41 @@ +/* +Copyright 2022 The Katanomi 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 logging + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + uberzap "go.uber.org/zap" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +var ( + logger *uberzap.SugaredLogger +) + +func init() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + logger = zap.NewRaw(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)).Sugar() +} + +func TestLogging(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Logging Suite") +} diff --git a/logging/testdata/config.yaml b/logging/testdata/config.yaml new file mode 100644 index 00000000..ab49c2c8 --- /dev/null +++ b/logging/testdata/config.yaml @@ -0,0 +1,31 @@ +apiVersion: v1 +data: + _example_: 'supported: debug | info | warn | error | dpanic | panic | fatal' + klog.level: "9" + loglevel.test_info: "info" + loglevel.test_warn: "warn" + loglevel.test_error: "error" + zap-logger-config: | + { + "level": "info", + "development": false, + "outputPaths": ["stdout"], + "errorOutputPaths": ["stderr"], + "encoding": "json", + "encoderConfig": { + "timeKey": "ts", + "levelKey": "level", + "nameKey": "logger", + "callerKey": "caller", + "messageKey": "msg", + "stacktraceKey": "stacktrace", + "lineEnding": "", + "levelEncoder": "", + "timeEncoder": "iso8601", + "durationEncoder": "", + "callerEncoder": "" + } + } +kind: ConfigMap +metadata: + name: katanomi-config-logging diff --git a/sharedmain/app.go b/sharedmain/app.go index a86ccd5d..5f5603f8 100644 --- a/sharedmain/app.go +++ b/sharedmain/app.go @@ -32,9 +32,10 @@ import ( cloudevents "github.com/cloudevents/sdk-go/v2" cloudeventsv2client "github.com/cloudevents/sdk-go/v2/client" metav1alpha1 "github.com/katanomi/pkg/apis/meta/v1alpha1" - "github.com/katanomi/pkg/config" storageroute "github.com/katanomi/pkg/plugin/storage/route" + "go.uber.org/zap/zapcore" + "k8s.io/klog/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilyaml "k8s.io/apimachinery/pkg/util/yaml" @@ -321,6 +322,21 @@ func (a *AppBuilder) Log() *AppBuilder { a.Context = logging.WithLogger(a.Context, a.Logger) lvlMGR.SetLogger(a.Logger) + // dynamically set klog level based on configmap configuration + lvlMGR.AddCustomObserver(func(m map[string]string) { + level := klogging.GetKlogLevelFromConfigMapData(m) + var klogLevel klog.Level + _ = klogLevel.Set(level) + }) + + // Set zap log level to -10 to avoid zap log level check. + // This means that klog has full control over whether to output logs. + // For more information: https://github.com/go-logr/zapr?tab=readme-ov-file#increasing-verbosity + zc := *a.ZapConfig + zc.Level = zap.NewAtomicLevelAt(zapcore.Level(-10)) + z, _ := zc.Build() + klog.SetLogger(zapr.NewLogger(z)) + // this logger will not respect the automatic level update feature // and should not be used // its main purpose is to provide a logger to controller-runtime