This is an automated email from the ASF dual-hosted git repository.

ccondit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/yunikorn-k8shim.git


The following commit(s) were added to refs/heads/master by this push:
     new 292e26fd [YUNIKORN-1797] K8Shim: Implement scoped logging (#611)
292e26fd is described below

commit 292e26fd9d910be39bb98f3bc01b7445179dff9e
Author: Craig Condit <[email protected]>
AuthorDate: Tue Jun 20 09:12:51 2023 -0500

    [YUNIKORN-1797] K8Shim: Implement scoped logging (#611)
    
    Add API support for scoped logging to the Kubernetes shim and admission
    controller. This will allow for fine-grained logging control per-subsystem.
    
    Closes: #611
    
    Signed-off-by: Craig Condit <[email protected]>
---
 Makefile                                |   6 +
 go.mod                                  |   2 +-
 go.sum                                  |   4 +-
 pkg/admission/conf/am_conf.go           |   8 +-
 pkg/admission/conf/am_conf_test.go      |   8 -
 pkg/client/apifactory_mock.go           |   1 -
 pkg/cmd/admissioncontroller/main.go     |   1 +
 pkg/cmd/shim/main.go                    |   3 +-
 pkg/conf/schedulerconf.go               |  21 +--
 pkg/conf/schedulerconf_test.go          |   3 -
 pkg/log/filtered_core.go                |  54 +++++++
 pkg/log/logger.go                       | 223 +++++++++++++++++++++++++-
 pkg/log/logger_test.go                  | 272 ++++++++++++++++++++++++++++++++
 pkg/schedulerplugin/scheduler_plugin.go |   3 +-
 14 files changed, 570 insertions(+), 39 deletions(-)

diff --git a/Makefile b/Makefile
index 8b8458a1..c710ec23 100644
--- a/Makefile
+++ b/Makefile
@@ -403,6 +403,12 @@ test: clean
        go test ./pkg/... -cover -race -tags deadlock 
-coverprofile=coverage.txt -covermode=atomic
        go vet $(REPO)...
 
+# Run benchmarks
+.PHONY: bench
+bench:
+       @echo "running benchmarks"
+       go test -v -run '^Benchmark' -bench . ./pkg/...
+
 # Generate FSM graphs (dot/png)
 .PHONY: fsm_graph
 fsm_graph: clean
diff --git a/go.mod b/go.mod
index 8076ff04..dd210270 100644
--- a/go.mod
+++ b/go.mod
@@ -22,7 +22,7 @@ go 1.20
 
 require (
        github.com/GoogleCloudPlatform/spark-on-k8s-operator 
v0.0.0-20201215015655-2e8b733f5ad0
-       github.com/apache/yunikorn-core v0.0.0-20230614110833-fecbbc81241c
+       github.com/apache/yunikorn-core v0.0.0-20230620141112-5d04f9c0dd32
        github.com/apache/yunikorn-scheduler-interface 
v0.0.0-20230614104003-12c630245d86
        github.com/google/uuid v1.2.0
        github.com/gorilla/mux v1.8.0
diff --git a/go.sum b/go.sum
index 499c882a..b3f133b2 100644
--- a/go.sum
+++ b/go.sum
@@ -104,8 +104,8 @@ github.com/alecthomas/units 
v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod 
h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
 github.com/antihax/optional v1.0.0/go.mod 
h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/antlr/antlr4/runtime/Go/antlr 
v0.0.0-20210826220005-b48c857c3a0e/go.mod 
h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
-github.com/apache/yunikorn-core v0.0.0-20230614110833-fecbbc81241c 
h1:coSVYzpPFsqVGvU9WPLQr0y1l7ZGpIio/S8xuU38qdw=
-github.com/apache/yunikorn-core v0.0.0-20230614110833-fecbbc81241c/go.mod 
h1:1K9P/AYVE5RcErlgzyzn/rvHAH4/+fQP7bFRkxvZEW4=
+github.com/apache/yunikorn-core v0.0.0-20230620141112-5d04f9c0dd32 
h1:eKGNJ/4SM6r34wHZsfc9JUPqV0lslMVGkaPiZUIcoVo=
+github.com/apache/yunikorn-core v0.0.0-20230620141112-5d04f9c0dd32/go.mod 
h1:1K9P/AYVE5RcErlgzyzn/rvHAH4/+fQP7bFRkxvZEW4=
 github.com/apache/yunikorn-scheduler-interface 
v0.0.0-20230614104003-12c630245d86 
h1:GLBGSuJYNjcCdMbKwId2u1BZyhKNzQaocnLqQkRhBLE=
 github.com/apache/yunikorn-scheduler-interface 
v0.0.0-20230614104003-12c630245d86/go.mod 
h1:r4amgZ4uYT1K0KpfYoNV4Ao2GVIYOUU0Rxru89g0WsQ=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod 
h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
diff --git a/pkg/admission/conf/am_conf.go b/pkg/admission/conf/am_conf.go
index c049bd2e..5cf0a1a9 100644
--- a/pkg/admission/conf/am_conf.go
+++ b/pkg/admission/conf/am_conf.go
@@ -26,7 +26,6 @@ import (
        "sync"
 
        "go.uber.org/zap"
-       "go.uber.org/zap/zapcore"
        v1 "k8s.io/api/core/v1"
        informersv1 "k8s.io/client-go/informers/core/v1"
        "k8s.io/client-go/tools/cache"
@@ -306,10 +305,6 @@ func (acc *AdmissionControllerConf) 
updateConfigMaps(configMaps []*v1.ConfigMap,
        // hot refresh
        acc.enableConfigHotRefresh = parseConfigBool(configs, 
schedulerconf.CMSvcEnableConfigHotRefresh, 
schedulerconf.DefaultEnableConfigHotRefresh)
 
-       // logging
-       logLevel := parseConfigInt(configs, schedulerconf.CMLogLevel, 
schedulerconf.DefaultLoggingLevel)
-       log.GetZapConfigs().Level.SetLevel(zapcore.Level(logLevel))
-
        // scheduler
        acc.policyGroup = parseConfigString(configs, 
schedulerconf.CMSvcPolicyGroup, schedulerconf.DefaultPolicyGroup)
 
@@ -334,6 +329,9 @@ func (acc *AdmissionControllerConf) 
updateConfigMaps(configMaps []*v1.ConfigMap,
        // labeling
        acc.defaultQueueName = parseConfigString(configs, 
AMFilteringDefaultQueueName, DefaultFilteringQueueName)
 
+       // logging
+       log.UpdateLoggingConfig(configs)
+
        acc.dumpConfigurationInternal()
 }
 
diff --git a/pkg/admission/conf/am_conf_test.go 
b/pkg/admission/conf/am_conf_test.go
index 11fdc6cc..600656b6 100644
--- a/pkg/admission/conf/am_conf_test.go
+++ b/pkg/admission/conf/am_conf_test.go
@@ -21,12 +21,10 @@ package conf
 import (
        "testing"
 
-       "go.uber.org/zap/zapcore"
        "gotest.tools/v3/assert"
        v1 "k8s.io/api/core/v1"
 
        schedulerconf "github.com/apache/yunikorn-k8shim/pkg/conf"
-       "github.com/apache/yunikorn-k8shim/pkg/log"
 )
 
 func TestConfigMapVars(t *testing.T) {
@@ -89,12 +87,6 @@ func TestConfigMapVars(t *testing.T) {
        assert.Equal(t, conf.GetTrustControllers(), 
DefaultAccessControlTrustControllers)
        assert.Equal(t, conf.GetGenerateUniqueAppIds(), 
DefaultFilteringGenerateUniqueAppIds)
 
-       // test faulty settings for int values
-       NewAdmissionControllerConf([]*v1.ConfigMap{nil, {Data: 
map[string]string{
-               schedulerconf.CMLogLevel: "not int",
-       }}})
-       assert.Equal(t, log.GetZapConfigs().Level.Level(), 
zapcore.Level(schedulerconf.DefaultLoggingLevel))
-
        // test faulty settings for regexp values
        conf = NewAdmissionControllerConf([]*v1.ConfigMap{nil, {Data: 
map[string]string{
                AMFilteringProcessNamespaces: "?",
diff --git a/pkg/client/apifactory_mock.go b/pkg/client/apifactory_mock.go
index 7c60a898..7c2ad65b 100644
--- a/pkg/client/apifactory_mock.go
+++ b/pkg/client/apifactory_mock.go
@@ -72,7 +72,6 @@ func NewMockedAPIProvider(showError bool) *MockedAPIProvider {
                                PolicyGroup:          "queues",
                                Interval:             0,
                                KubeConfig:           "",
-                               LoggingLevel:         0,
                                VolumeBindTimeout:    0,
                                TestMode:             true,
                                EventChannelCapacity: 0,
diff --git a/pkg/cmd/admissioncontroller/main.go 
b/pkg/cmd/admissioncontroller/main.go
index 09e7b221..273d61bb 100644
--- a/pkg/cmd/admissioncontroller/main.go
+++ b/pkg/cmd/admissioncontroller/main.go
@@ -52,6 +52,7 @@ type WebHook struct {
 }
 
 func main() {
+       log.SetDefaultLogger(log.Admission)
        configMaps, err := 
client.LoadBootstrapConfigMaps(schedulerconf.GetSchedulerNamespace())
        if err != nil {
                log.Logger().Fatal("Failed to load initial configmaps", 
zap.Error(err))
diff --git a/pkg/cmd/shim/main.go b/pkg/cmd/shim/main.go
index 851595cf..cd50f5a2 100644
--- a/pkg/cmd/shim/main.go
+++ b/pkg/cmd/shim/main.go
@@ -55,6 +55,7 @@ func main() {
        conf.SiSHA = siSHA
        conf.ShimSHA = shimSHA
 
+       log.SetDefaultLogger(log.K8Shim)
        log.Logger().Info(fmt.Sprintf("Build info: version=%s date=%s 
isPluginVersion=%t goVersion=%s arch=%s coreSHA=%s siSHA=%s shimSHA=%s", 
version, date, false, goVersion, arch, coreSHA, siSHA, shimSHA))
 
        configMaps, err := 
client.LoadBootstrapConfigMaps(conf.GetSchedulerNamespace())
@@ -68,7 +69,7 @@ func main() {
        }
 
        log.Logger().Info("Starting scheduler", zap.String("name", 
constants.SchedulerName))
-       serviceContext := entrypoint.StartAllServicesWithLogger(log.Logger(), 
log.GetZapConfigs())
+       serviceContext := 
entrypoint.StartAllServicesWithLogger(log.RootLogger(), log.GetZapConfigs())
 
        if sa, ok := serviceContext.RMProxy.(api.SchedulerAPI); ok {
                ss := shim.NewShimScheduler(sa, conf.GetSchedulerConf(), 
configMaps)
diff --git a/pkg/conf/schedulerconf.go b/pkg/conf/schedulerconf.go
index e5d1f623..759d8c86 100644
--- a/pkg/conf/schedulerconf.go
+++ b/pkg/conf/schedulerconf.go
@@ -65,9 +65,6 @@ const (
        CMSvcPlaceholderImage             = PrefixService + "placeholderImage"
        CMSvcNodeInstanceTypeNodeLabelKey = PrefixService + 
"nodeInstanceTypeNodeLabelKey"
 
-       // log
-       CMLogLevel = PrefixLog + "level"
-
        // kubernetes
        CMKubeQPS   = PrefixKubernetes + "qps"
        CMKubeBurst = PrefixKubernetes + "burst"
@@ -83,8 +80,6 @@ const (
        DefaultOperatorPlugins        = "general"
        DefaultDisableGangScheduling  = false
        DefaultEnableConfigHotRefresh = true
-       DefaultLoggingLevel           = 0
-       DefaultLogEncoding            = "console"
        DefaultKubeQPS                = 1000
        DefaultKubeBurst              = 1000
 )
@@ -112,7 +107,6 @@ type SchedulerConf struct {
        PolicyGroup              string        `json:"policyGroup"`
        Interval                 time.Duration `json:"schedulingIntervalSecond"`
        KubeConfig               string        
`json:"absoluteKubeConfigFilePath"`
-       LoggingLevel             int           `json:"loggingLevel"`
        VolumeBindTimeout        time.Duration `json:"volumeBindTimeout"`
        TestMode                 bool          `json:"testMode"`
        EventChannelCapacity     int           `json:"eventChannelCapacity"`
@@ -140,7 +134,6 @@ func (conf *SchedulerConf) Clone() *SchedulerConf {
                PolicyGroup:            conf.PolicyGroup,
                Interval:               conf.Interval,
                KubeConfig:             conf.KubeConfig,
-               LoggingLevel:           conf.LoggingLevel,
                VolumeBindTimeout:      conf.VolumeBindTimeout,
                TestMode:               conf.TestMode,
                EventChannelCapacity:   conf.EventChannelCapacity,
@@ -182,13 +175,13 @@ func UpdateConfigMaps(configMaps []*v1.ConfigMap, initial 
bool) error {
 
        // update scheduler config with merged version
        SetSchedulerConf(newConf)
-       conf := GetSchedulerConf()
+       _ = GetSchedulerConf()
 
        // update logger configuration
-       log.GetZapConfigs().Level.SetLevel(zapcore.Level(conf.LoggingLevel))
+       log.UpdateLoggingConfig(config)
 
        // update Kubernetes logger configuration
-       updateKubeLogger(conf)
+       updateKubeLogger()
 
        // dump new scheduler configuration
        DumpConfiguration()
@@ -327,7 +320,6 @@ func CreateDefaultConfig() *SchedulerConf {
                PolicyGroup:              DefaultPolicyGroup,
                Interval:                 DefaultSchedulingInterval,
                KubeConfig:               GetDefaultKubeConfigPath(),
-               LoggingLevel:             DefaultLoggingLevel,
                VolumeBindTimeout:        DefaultVolumeBindTimeout,
                TestMode:                 false,
                EventChannelCapacity:     DefaultEventChannelCapacity,
@@ -366,9 +358,6 @@ func parseConfig(config map[string]string, prev 
*SchedulerConf) (*SchedulerConf,
        parser.stringVar(&conf.PlaceHolderImage, CMSvcPlaceholderImage)
        parser.stringVar(&conf.InstanceTypeNodeLabelKey, 
CMSvcNodeInstanceTypeNodeLabelKey)
 
-       // log
-       parser.intVar(&conf.LoggingLevel, CMLogLevel)
-
        // kubernetes
        parser.intVar(&conf.KubeQPS, CMKubeQPS)
        parser.intVar(&conf.KubeBurst, CMKubeBurst)
@@ -434,14 +423,14 @@ func (cp *configParser) durationVar(p *time.Duration, 
name string) {
        }
 }
 
-func updateKubeLogger(conf *SchedulerConf) {
+func updateKubeLogger() {
        // if log level is debug, enable klog and set its log level verbosity 
to 4 (represents debug level),
        // For details refer to the Logging Conventions of klog at
        // 
https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md
 
        // danger, this can only be called once!
        kubeLoggerOnce.Do(func() {
-               if zapcore.Level(conf.LoggingLevel).Enabled(zapcore.DebugLevel) 
{
+               if log.Log(log.Kubernetes).Core().Enabled(zapcore.DebugLevel) {
                        klog.InitFlags(nil)
                        // cannot really handle the error here ignore it
                        //nolint:errcheck
diff --git a/pkg/conf/schedulerconf_test.go b/pkg/conf/schedulerconf_test.go
index 11c31b60..210d654b 100644
--- a/pkg/conf/schedulerconf_test.go
+++ b/pkg/conf/schedulerconf_test.go
@@ -65,7 +65,6 @@ func assertDefaults(t *testing.T, conf *SchedulerConf) {
        assert.Equal(t, conf.ClusterID, DefaultClusterID)
        assert.Equal(t, conf.PolicyGroup, DefaultPolicyGroup)
        assert.Equal(t, conf.ClusterVersion, BuildVersion)
-       assert.Equal(t, conf.LoggingLevel, DefaultLoggingLevel)
        assert.Equal(t, conf.EventChannelCapacity, DefaultEventChannelCapacity)
        assert.Equal(t, conf.DispatchTimeout, DefaultDispatchTimeout)
        assert.Equal(t, conf.KubeQPS, DefaultKubeQPS)
@@ -90,7 +89,6 @@ func TestParseConfigMap(t *testing.T) {
                {CMSvcEnableConfigHotRefresh, "EnableConfigHotRefresh", false},
                {CMSvcPlaceholderImage, "PlaceHolderImage", "test-image"},
                {CMSvcNodeInstanceTypeNodeLabelKey, "InstanceTypeNodeLabelKey", 
"node.kubernetes.io/instance-type"},
-               {CMLogLevel, "LoggingLevel", -1},
                {CMKubeQPS, "KubeQPS", 2345},
                {CMKubeBurst, "KubeBurst", 3456},
        }
@@ -123,7 +121,6 @@ func TestUpdateConfigMapNonReloadable(t *testing.T) {
                {CMSvcDisableGangScheduling, "DisableGangScheduling", true, 
false},
                {CMSvcPlaceholderImage, "PlaceHolderImage", "test-image", 
false},
                {CMSvcNodeInstanceTypeNodeLabelKey, "InstanceTypeNodeLabelKey", 
"node.kubernetes.io/instance-type", false},
-               {CMLogLevel, "LoggingLevel", -1, true},
                {CMKubeQPS, "KubeQPS", 2345, false},
                {CMKubeBurst, "KubeBurst", 3456, false},
        }
diff --git a/pkg/log/filtered_core.go b/pkg/log/filtered_core.go
new file mode 100644
index 00000000..1e2dcf6a
--- /dev/null
+++ b/pkg/log/filtered_core.go
@@ -0,0 +1,54 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you 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 "go.uber.org/zap/zapcore"
+
+type filteredCore struct {
+       level zapcore.Level
+       inner zapcore.Core
+}
+
+var _ zapcore.Core = filteredCore{}
+
+func (f filteredCore) Enabled(level zapcore.Level) bool {
+       if level < f.level {
+               return false
+       }
+       return f.inner.Enabled(level)
+}
+
+func (f filteredCore) With(fields []zapcore.Field) zapcore.Core {
+       return f.inner.With(fields)
+}
+
+func (f filteredCore) Check(entry zapcore.Entry, ce *zapcore.CheckedEntry) 
*zapcore.CheckedEntry {
+       if entry.Level < f.level {
+               return ce
+       }
+       return f.inner.Check(entry, ce)
+}
+
+func (f filteredCore) Write(entry zapcore.Entry, fields []zapcore.Field) error 
{
+       return f.inner.Write(entry, fields)
+}
+
+func (f filteredCore) Sync() error {
+       return f.inner.Sync()
+}
diff --git a/pkg/log/logger.go b/pkg/log/logger.go
index 2ba4fb18..81c73a5c 100644
--- a/pkg/log/logger.go
+++ b/pkg/log/logger.go
@@ -20,7 +20,10 @@ package log
 
 import (
        "fmt"
+       "strconv"
+       "strings"
        "sync"
+       "sync/atomic"
 
        "go.uber.org/zap"
        "go.uber.org/zap/zapcore"
@@ -30,12 +33,122 @@ var once sync.Once
 var logger *zap.Logger
 var zapConfigs *zap.Config
 
+// LoggerHandle is used to efficiently look up logger references
+type LoggerHandle struct {
+       id   int
+       name string
+}
+
+func (h LoggerHandle) String() string {
+       return h.name
+}
+
+// Logger constants for configuration
+const (
+       nullLogger  = ""
+       defaultLog  = "log.level"
+       logPrefix   = "log."
+       levelSuffix = ".level"
+)
+
+// Predefined loggers: when adding new loggers, ids must be sequential, and 
all must be added to the loggers slice in the same order
+var (
+       K8Shim     = &LoggerHandle{id: 1, name: "k8shim"}
+       Kubernetes = &LoggerHandle{id: 2, name: "kubernetes"}
+       Admission  = &LoggerHandle{id: 3, name: "admission"}
+       Test       = &LoggerHandle{id: 4, name: "test"}
+)
+
+// this tracks all the known logger handles, used to preallocate the real 
logger instances when configuration changes
+var loggers = []*LoggerHandle{
+       K8Shim,
+       Kubernetes,
+       Admission,
+       Test,
+}
+
+// structure to hold all current logger configuration state
+type loggerConfig struct {
+       loggers []*zap.Logger
+}
+
+// tracks the currently used set of loggers; replaced completely whenever 
configuration changes
+var currentLoggerConfig = atomic.Pointer[loggerConfig]{}
+
+// tracks the default logger handle, which is used for legacy log.Logger() 
calls
+var defaultLogger = atomic.Pointer[LoggerHandle]{}
+
+// Logger retrieves the global logger. This is for compatibility with legacy 
code and
+// should not be used for new log messages; use Log(loggerHandle) instead
 func Logger() *zap.Logger {
+       once.Do(initLogger)
+       return Log(defaultLogger.Load())
+}
+
+// RootLogger retrieves the root logger, used to pass the configured logger to 
the scheduler core on startup
+func RootLogger() *zap.Logger {
        once.Do(initLogger)
        return logger
 }
 
+// Log retrieves a named logger
+func Log(handle *LoggerHandle) *zap.Logger {
+       once.Do(initLogger)
+       if handle == nil || handle.id == 0 {
+               handle = defaultLogger.Load()
+       }
+       conf := currentLoggerConfig.Load()
+       return conf.loggers[handle.id-1]
+}
+
+// createLogger creates a new, named logger that has log levels filtered based 
on the given configuration.
+// levelMap contains a mapping of fully-qualified logger names to log levels
+func createLogger(levelMap map[string]zapcore.Level, name string) *zap.Logger {
+       level := loggerLevel(levelMap, name)
+       return logger.Named(name).WithOptions(zap.WrapCore(func(inner 
zapcore.Core) zapcore.Core {
+               return filteredCore{inner: inner, level: level}
+       }))
+}
+
+// loggerLevel computes the log level that should be associated with an 
arbitrarily named logger.
+// The levelMap is used to look up the level. If not found, the parent logger 
is looked up until no further
+// parents can be tried, consulting levelMap for each possible match. For 
example, given a logger named
+// k8shim.context.cache, the following keys will be looked up in levelMap:
+//
+//     "k8shim.context.cache"
+//     "k8shim.context"
+//     "k8shim"
+//     "" (default logger)
+//
+// The first key that returns a match determines the log level returned. This 
allows a level of
+// "k8shim" to control all the child loggers as well if there is not a more 
specific override.
+// If no configuration can be found, even for the default empty logger, 
InfoLevel will be returned.
+func loggerLevel(levelMap map[string]zapcore.Level, name string) zapcore.Level 
{
+       for ; name != nullLogger; name = parentLogger(name) {
+               if level, ok := levelMap[name]; ok {
+                       return level
+               }
+       }
+       if level, ok := levelMap[nullLogger]; ok {
+               return level
+       }
+       return zapcore.InfoLevel
+}
+
+// parentLogger returns the name of the parent logger or the empty string "" 
if no parent exists.
+// Loggers are named with periods (.) as separators; i.e. the parent logger of 
"a.b.c" is "a.b", the parent of
+// "a.b" is "a", and the parent of "a" is "".
+func parentLogger(name string) string {
+       i := strings.LastIndex(name, ".")
+       if i < 0 {
+               // no remaining periods; parent is the null logger
+               return nullLogger
+       }
+       return name[0:i]
+}
+
 func initLogger() {
+       defaultLogger.Store(K8Shim)
        outputPaths := []string{"stdout"}
 
        zapConfigs = &zap.Config{
@@ -49,7 +162,7 @@ func initLogger() {
                        MessageKey:    "message",
                        LevelKey:      "level",
                        TimeKey:       "time",
-                       NameKey:       "name",
+                       NameKey:       "logger",
                        CallerKey:     "caller",
                        StacktraceKey: "stacktrace",
                        LineEnding:    zapcore.DefaultLineEnding,
@@ -72,6 +185,9 @@ func initLogger() {
                logger = zap.NewNop()
        }
 
+       // initialize sub-loggers
+       initLoggingConfig(nil)
+
        // make sure logs are flushed
        //nolint:errcheck
        defer logger.Sync()
@@ -82,3 +198,108 @@ func GetZapConfigs() *zap.Config {
        _ = Logger()
        return zapConfigs
 }
+
+// SetDefaultLogger allows customization of the default logger
+func SetDefaultLogger(handle *LoggerHandle) {
+       once.Do(initLogger)
+       defaultLogger.Store(handle)
+}
+
+// UpdateLoggingConfig is used to reconfigure logging. This uses config keys 
of the form log.{logger}.level={level}.
+// The default level is set by log.level={level}. The {level} value can be 
either numeric (-1 through 5), or
+// textual (DEBUG, INFO, WARN, ERROR, DPANIC, PANIC, or ERROR). See zapcore 
documentation for more details.
+func UpdateLoggingConfig(config map[string]string) {
+       once.Do(initLogger)
+       initLoggingConfig(config)
+}
+
+// initLoggingConfig replaces the existing set of loggers with new ones 
configured according to the given
+// configuration. All keys of the form "log.{name}.level" will be parsed and a 
map of logger name -> logger level
+// will be created. For each defined logger handle (see above), a new logger 
instance is created using the
+// most specific configuration found. For example, a logger named "a.b.c" will 
be configured by "log.a.b.c.level".
+// If this key does not exist, "a.b" will be consulted, then "a", and finally 
"". If no configuration is found for a
+// given key, Info will be used. Finally, the finest log level specified in 
any configuration will be used to set the
+// zap log level of the root logger. So if two loggers (one INFO, and one 
DEBUG) are configured, the root logger will
+// be set to DEBUG level. If both loggers were at INFO level, the root logger 
would be set to INFO.
+// Each configured logger will filter log messages at its own level by 
wrapping the underlying zap.Core implementation
+// with one that checks the enabled log level first.
+func initLoggingConfig(config map[string]string) {
+       levelMap := make(map[string]zapcore.Level)
+       levelMap[nullLogger] = zapcore.InfoLevel
+       zapLoggers := make([]*zap.Logger, len(loggers))
+
+       // override default level if found (log.level key)
+       if defaultLevel, ok := config[defaultLog]; ok {
+               if levelRef := parseLevel(defaultLevel); levelRef != nil {
+                       levelMap[nullLogger] = *levelRef
+               }
+       }
+
+       // parse out log entries and build level map
+       for k, v := range config {
+               // disallow spaces and periods
+               if strings.Contains(k, "..") || strings.Contains(k, " ") {
+                       continue
+               }
+               // ensure config key starts with "log."
+               name, ok := strings.CutPrefix(k, logPrefix)
+               if !ok {
+                       continue
+               }
+               // ensure config key ends with ".level"
+               name, ok = strings.CutSuffix(name, levelSuffix)
+               if !ok {
+                       continue
+               }
+               // if value is a valid log level, store it in the level map
+               if levelRef := parseLevel(v); levelRef != nil {
+                       levelMap[name] = *levelRef
+               }
+       }
+
+       // compute the finest log level necessary to allow all loggers to 
succeed
+       minLevel := zapcore.InvalidLevel - 1
+       for _, v := range levelMap {
+               if minLevel > v {
+                       minLevel = v
+               }
+       }
+
+       // create each configured logger and initialize the overall 
configuration
+       for i := 0; i < len(loggers); i++ {
+               zapLoggers[i] = createLogger(levelMap, loggers[i].name)
+       }
+       newLoggerConfig := loggerConfig{loggers: zapLoggers}
+
+       // update the root zap logger level
+       zapConfigs.Level.SetLevel(minLevel)
+
+       // atomically update the set of loggers
+       currentLoggerConfig.Store(&newLoggerConfig)
+}
+
+// parseLevel parses a textual (or numeric) log level into a zapcore.Level 
instance. Both numeric (-1 <= level <= 5)
+// and textual (DEBUG, INFO, WARN, ERROR, DPANIC, PANIC, FATAL) are supported.
+func parseLevel(level string) *zapcore.Level {
+       // parse text
+       zapLevel, err := zapcore.ParseLevel(level)
+       if err == nil {
+               return &zapLevel
+       }
+
+       // parse numeric
+       levelNum, err := strconv.ParseInt(level, 10, 31)
+       if err == nil {
+               zapLevel = zapcore.Level(levelNum)
+               if zapLevel < zapcore.DebugLevel {
+                       zapLevel = zapcore.DebugLevel
+               }
+               if zapLevel >= zapcore.InvalidLevel {
+                       zapLevel = zapcore.InvalidLevel - 1
+               }
+               return &zapLevel
+       }
+
+       // parse failed
+       return nil
+}
diff --git a/pkg/log/logger_test.go b/pkg/log/logger_test.go
new file mode 100644
index 00000000..e8e800ad
--- /dev/null
+++ b/pkg/log/logger_test.go
@@ -0,0 +1,272 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you 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 (
+       "fmt"
+       "os"
+       "testing"
+       "time"
+
+       "go.uber.org/zap"
+       "go.uber.org/zap/zapcore"
+       "gotest.tools/v3/assert"
+)
+
+var logDir string
+var logFile string
+
+var iterations = 100000
+
+func TestLoggerIds(t *testing.T) {
+       _ = Logger()
+
+       // validate logger count
+       assert.Equal(t, 4, len(loggers), "wrong logger count")
+
+       // validate that all loggers are populated and have sequential ids
+       for i := 0; i < len(loggers); i++ {
+               handle := loggers[i]
+               assert.Assert(t, handle != nil, "nil handle for index", i)
+               assert.Equal(t, handle.id, i+1, "wrong id", handle.name)
+       }
+}
+
+func BenchmarkLegacyLoggerDebug(b *testing.B) {
+       benchmarkLegacyLoggerDebug(b.N)
+}
+
+func TestLegacyLoggerDebug(t *testing.T) {
+       nsOp := benchmarkLegacyLoggerDebug(iterations)
+       RootLogger().Info("log.Logger() performance", zap.Int64("debug 
(ns/op)", nsOp))
+}
+
+func benchmarkLegacyLoggerDebug(iterations int) int64 {
+       _ = Logger()
+       initTestLogger()
+       defer resetTestLogger()
+       start := time.Now()
+       for i := 0; i < iterations; i++ {
+               RootLogger().Debug("test", zap.String("foo", "bar"))
+       }
+       return (time.Since(start).Nanoseconds()) / int64(iterations)
+}
+
+func BenchmarkLegacyLoggerInfo(b *testing.B) {
+       benchmarkLegacyLoggerInfo(b.N)
+}
+
+func TestLegacyLoggerInfo(t *testing.T) {
+       nsOp := benchmarkLegacyLoggerInfo(iterations)
+       RootLogger().Info("log.Logger() performance", zap.Int64("info (ns/op)", 
nsOp))
+}
+
+func benchmarkLegacyLoggerInfo(iterations int) int64 {
+       _ = Logger()
+       initTestLogger()
+       defer resetTestLogger()
+       start := time.Now()
+       for i := 0; i < iterations; i++ {
+               RootLogger().Info("test", zap.String("foo", "bar"))
+       }
+       return (time.Since(start).Nanoseconds()) / int64(iterations)
+}
+
+func BenchmarkScopedLoggerDebug(b *testing.B) {
+       benchmarkScopedLoggerDebug(b.N)
+}
+
+func TestScopedLoggerDebug(t *testing.T) {
+       nsOp := benchmarkScopedLoggerDebug(iterations)
+       Log(Test).Info("log.Log(...) performance (root=INFO)", zap.Int64("debug 
(ns/op)", nsOp))
+}
+
+func benchmarkScopedLoggerDebug(iterations int) int64 {
+       _ = Logger()
+       initTestLogger()
+       defer resetTestLogger()
+       UpdateLoggingConfig(map[string]string{
+               "log.level": "INFO",
+       })
+       start := time.Now()
+       for i := 0; i < iterations; i++ {
+               Log(K8Shim).Debug("test", zap.String("foo", "bar"))
+       }
+       return (time.Since(start).Nanoseconds()) / int64(iterations)
+}
+
+func BenchmarkScopedLoggerInfo(b *testing.B) {
+       benchmarkScopedLoggerInfo(b.N)
+}
+
+func TestScopedLoggerInfo(t *testing.T) {
+       nsOp := benchmarkScopedLoggerInfo(iterations)
+       Log(Test).Info("log.Log(...) performance (root=INFO)", zap.Int64("info 
(ns/op)", nsOp))
+}
+
+func benchmarkScopedLoggerInfo(iterations int) int64 {
+       _ = Logger()
+       initTestLogger()
+       defer resetTestLogger()
+       UpdateLoggingConfig(map[string]string{
+               "log.level": "INFO",
+       })
+       start := time.Now()
+       for i := 0; i < iterations; i++ {
+               Log(K8Shim).Info("test", zap.String("foo", "bar"))
+       }
+       return (time.Since(start).Nanoseconds()) / int64(iterations)
+}
+
+func BenchmarkScopedLoggerDebugEnabled(b *testing.B) {
+       benchmarkScopedLoggerDebugEnabled(b.N)
+}
+
+func TestScopedLoggerDebugEnabled(t *testing.T) {
+       nsOp := benchmarkScopedLoggerDebugEnabled(iterations)
+       Log(Test).Info("log.Log(...) performance (root=DEBUG)", 
zap.Int64("debug (ns/op)", nsOp))
+}
+
+func benchmarkScopedLoggerDebugEnabled(iterations int) int64 {
+       _ = Logger()
+       initTestLogger()
+       defer resetTestLogger()
+       UpdateLoggingConfig(map[string]string{
+               "log.test.level": "DEBUG",
+       })
+       start := time.Now()
+       for i := 0; i < iterations; i++ {
+               Log(Test).Debug("test", zap.String("foo", "bar"))
+       }
+       return (time.Since(start).Nanoseconds()) / int64(iterations)
+}
+
+func BenchmarkScopedLoggerInfoFiltered(b *testing.B) {
+       benchmarkScopedLoggerInfoFiltered(b.N)
+}
+
+func TestScopedLoggerInfoFiltered(t *testing.T) {
+       nsOp := benchmarkScopedLoggerInfoFiltered(iterations)
+       Log(Test).Info("log.Log(...) performance (root=DEBUG)", zap.Int64("info 
(ns/op)", nsOp))
+}
+
+func benchmarkScopedLoggerInfoFiltered(iterations int) int64 {
+       _ = Logger()
+       initTestLogger()
+       defer resetTestLogger()
+       UpdateLoggingConfig(map[string]string{
+               "log.test.level": "DEBUG",
+       })
+       start := time.Now()
+       for i := 0; i < iterations; i++ {
+               Log(K8Shim).Info("test", zap.String("foo", "bar"))
+       }
+       return (time.Since(start).Nanoseconds()) / int64(iterations)
+}
+
+func TestParseLevel(t *testing.T) {
+       assert.Equal(t, zapcore.DebugLevel, *parseLevel("-2"), "out of range 
low")
+       assert.Equal(t, zapcore.DebugLevel, *parseLevel("-1"))
+       assert.Equal(t, zapcore.InfoLevel, *parseLevel("0"))
+       assert.Equal(t, zapcore.WarnLevel, *parseLevel("1"))
+       assert.Equal(t, zapcore.ErrorLevel, *parseLevel("2"))
+       assert.Equal(t, zapcore.DPanicLevel, *parseLevel("3"))
+       assert.Equal(t, zapcore.PanicLevel, *parseLevel("4"))
+       assert.Equal(t, zapcore.FatalLevel, *parseLevel("5"))
+       assert.Equal(t, zapcore.FatalLevel, *parseLevel("6"), "out of range 
high")
+       assert.Assert(t, parseLevel("+2-3") == nil, "parse error")
+       assert.Equal(t, zapcore.DebugLevel, *parseLevel("Debug"))
+       assert.Equal(t, zapcore.InfoLevel, *parseLevel("iNFO"))
+       assert.Equal(t, zapcore.WarnLevel, *parseLevel("WaRn"))
+       assert.Equal(t, zapcore.ErrorLevel, *parseLevel("ERROR"))
+       assert.Equal(t, zapcore.DPanicLevel, *parseLevel("dpanic"))
+       assert.Equal(t, zapcore.PanicLevel, *parseLevel("PAnIC"))
+       assert.Equal(t, zapcore.FatalLevel, *parseLevel("faTal"))
+       assert.Assert(t, parseLevel("x") == nil, "parse error")
+}
+
+func TestParentLogger(t *testing.T) {
+       assert.Equal(t, "", parentLogger(""), "nullLogger")
+       assert.Equal(t, "", parentLogger("a"), "level 1")
+       assert.Equal(t, "a", parentLogger("a.b"), "level 2")
+       assert.Equal(t, "a.b", parentLogger("a.b.c"), "level 3")
+}
+
+func resetTestLogger() {
+       // flush log
+       logger.Sync() //nolint:errcheck
+
+       // init default logger
+       initLogger()
+
+       // update logger config to defaults
+       UpdateLoggingConfig(map[string]string{})
+
+       if logFile != "" {
+               logFile = ""
+       }
+       if logDir != "" {
+               if err := os.RemoveAll(logDir); err != nil {
+                       fmt.Printf("Error removing log dir: %s", err.Error())
+               }
+       }
+}
+
+// initTestLogger is basically the same as the default initLogger() function 
but uses a temporary file.
+// this ensures that the logging API is actually used, while allowing us to 
avoid massive log spam to stdout
+func initTestLogger() {
+       path, err := os.MkdirTemp("", "log*")
+       if err != nil {
+               panic(err)
+       }
+       logDir = path
+       logFile = fmt.Sprintf("%s/log.stdout", logDir)
+       outputPaths := []string{logFile}
+       zapConfigs = &zap.Config{
+               Level:             zap.NewAtomicLevelAt(zapcore.Level(0)),
+               Development:       false,
+               DisableCaller:     false,
+               DisableStacktrace: false,
+               Sampling:          nil,
+               Encoding:          "console",
+               EncoderConfig: zapcore.EncoderConfig{
+                       MessageKey:    "message",
+                       LevelKey:      "level",
+                       TimeKey:       "time",
+                       NameKey:       "logger",
+                       CallerKey:     "caller",
+                       StacktraceKey: "stacktrace",
+                       LineEnding:    zapcore.DefaultLineEnding,
+                       // note: 
https://godoc.org/go.uber.org/zap/zapcore#EncoderConfig
+                       // only EncodeName is optional all others must be set
+                       EncodeLevel:    zapcore.CapitalLevelEncoder,
+                       EncodeTime:     zapcore.ISO8601TimeEncoder,
+                       EncodeDuration: zapcore.StringDurationEncoder,
+                       EncodeCaller:   zapcore.ShortCallerEncoder,
+               },
+               OutputPaths:      outputPaths,
+               ErrorOutputPaths: []string{"stderr"},
+       }
+
+       logger, err = zapConfigs.Build()
+       if err != nil {
+               panic(err)
+       }
+       defer logger.Sync() //nolint:errcheck
+}
diff --git a/pkg/schedulerplugin/scheduler_plugin.go 
b/pkg/schedulerplugin/scheduler_plugin.go
index 5d1c465a..3a906352 100644
--- a/pkg/schedulerplugin/scheduler_plugin.go
+++ b/pkg/schedulerplugin/scheduler_plugin.go
@@ -216,6 +216,7 @@ func (sp *YuniKornSchedulerPlugin) PostBind(_ 
context.Context, _ *framework.Cycl
 
 // NewSchedulerPlugin initializes a new plugin and returns it
 func NewSchedulerPlugin(_ runtime.Object, handle framework.Handle) 
(framework.Plugin, error) {
+       log.SetDefaultLogger(log.K8Shim)
        log.Logger().Info(fmt.Sprintf("Build info: version=%s date=%s 
isPluginVersion=%t goVersion=%s arch=%s coreSHA=%s siSHA=%s shimSHA=%s", 
conf.BuildVersion, conf.BuildDate, conf.IsPluginVersion, conf.GoVersion, 
conf.Arch, conf.CoreSHA, conf.SiSHA, conf.ShimSHA))
 
        configMaps, err := 
client.LoadBootstrapConfigMaps(conf.GetSchedulerNamespace())
@@ -229,7 +230,7 @@ func NewSchedulerPlugin(_ runtime.Object, handle 
framework.Handle) (framework.Pl
        }
 
        // start the YK core scheduler
-       serviceContext := entrypoint.StartAllServicesWithLogger(log.Logger(), 
log.GetZapConfigs())
+       serviceContext := 
entrypoint.StartAllServicesWithLogger(log.RootLogger(), log.GetZapConfigs())
        if sa, ok := serviceContext.RMProxy.(api.SchedulerAPI); ok {
                // we need our own informer factory here because the informers 
we get from the framework handle aren't yet initialized
                informerFactory := 
informers.NewSharedInformerFactory(handle.ClientSet(), 0)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to