Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support update klog log level dynamically based on configmap #561

Merged
merged 1 commit into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions logging/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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)
}()
}
}
}

Expand All @@ -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)
}
73 changes: 73 additions & 0 deletions logging/config_test.go
Original file line number Diff line number Diff line change
@@ -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())
})
})
})
45 changes: 45 additions & 0 deletions logging/klog.go
Original file line number Diff line number Diff line change
@@ -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
}
69 changes: 69 additions & 0 deletions logging/klog_test.go
Original file line number Diff line number Diff line change
@@ -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))
})
}
}
41 changes: 41 additions & 0 deletions logging/suite_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
31 changes: 31 additions & 0 deletions logging/testdata/config.yaml
Original file line number Diff line number Diff line change
@@ -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
18 changes: 17 additions & 1 deletion sharedmain/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down