Skip to content

Commit

Permalink
feat: support update klog log level dynamically based on configmap (#561
Browse files Browse the repository at this point in the history
)
  • Loading branch information
nanjingfm authored Mar 21, 2024
1 parent 39e68ad commit ede8aa5
Show file tree
Hide file tree
Showing 7 changed files with 297 additions and 1 deletion.
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

0 comments on commit ede8aa5

Please sign in to comment.