diff --git a/config/core/configmaps/logging.yaml b/config/core/configmaps/logging.yaml index 3fa6a685c4a..1973bd6aec1 100644 --- a/config/core/configmaps/logging.yaml +++ b/config/core/configmaps/logging.yaml @@ -50,3 +50,9 @@ data: # For all components changes are be picked up immediately. loglevel.controller: "info" loglevel.webhook: "info" + + # klogLevel configures the verbosity level for klog (Kubernetes client-go logging). + # This is useful for debugging Kubernetes API client issues. + # Valid values are integers from 0-10. Higher values are more verbose. + # Default is "0" (minimal output). + klogLevel: "0" diff --git a/pkg/adapter/v2/config.go b/pkg/adapter/v2/config.go index 3f8817654db..55f0280c781 100644 --- a/pkg/adapter/v2/config.go +++ b/pkg/adapter/v2/config.go @@ -47,6 +47,7 @@ const ( EnvConfigObservabilityConfig = "K_OBSERVABILITY_CONFIG" EnvConfigLeaderElectionConfig = "K_LEADER_ELECTION_CONFIG" EnvSinkTimeout = "K_SINK_TIMEOUT" + EnvConfigKlogLevel = "K_KLOG_LEVEL" ) // EnvConfig is the minimal set of configuration parameters @@ -97,6 +98,10 @@ type EnvConfig struct { // Time in seconds to wait for sink to respond EnvSinkTimeout string `envconfig:"K_SINK_TIMEOUT"` + // KlogLevel is the verbosity level for klog (Kubernetes client-go logging). + // Valid values are integers from 0-10. Higher values are more verbose. + KlogLevel string `envconfig:"K_KLOG_LEVEL" default:"0"` + // cached zap logger logger *zap.SugaredLogger } @@ -137,6 +142,9 @@ type EnvConfigAccessor interface { // Get the timeout to apply on a request to a sink GetSinktimeout() int + + // GetKlogLevel returns the klog verbosity level. + GetKlogLevel() int } var _ EnvConfigAccessor = (*EnvConfig)(nil) @@ -200,6 +208,13 @@ func (e *EnvConfig) GetSinktimeout() int { return -1 } +func (e *EnvConfig) GetKlogLevel() int { + if level, err := strconv.Atoi(e.KlogLevel); err == nil && level >= 0 { + return level + } + return 0 +} + func (e *EnvConfig) GetObservabilityConfig() (*observability.Config, error) { cfg := &observability.Config{} err := json.Unmarshal([]byte(e.ObservabilityConfigJson), cfg) diff --git a/pkg/adapter/v2/config_test.go b/pkg/adapter/v2/config_test.go index 913302be2ff..72919d19483 100644 --- a/pkg/adapter/v2/config_test.go +++ b/pkg/adapter/v2/config_test.go @@ -238,3 +238,50 @@ func TestCACerts(t *testing.T) { }) } } + +func TestGetKlogLevel(t *testing.T) { + testCases := []struct { + name string + envValue string + expected int + }{ + { + name: "default value (0)", + envValue: "", + expected: 0, + }, + { + name: "level 7 - verbose", + envValue: "7", + expected: 7, + }, + { + name: "invalid level - negative", + envValue: "-1", + expected: 0, + }, + { + name: "invalid level - non-numeric", + envValue: "invalid", + expected: 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.envValue != "" { + t.Setenv("K_KLOG_LEVEL", tc.envValue) + } + + var env myEnvConfig + err := envconfig.Process("", &env) + if err != nil { + t.Error("Expected no error:", err) + } + + if got := env.GetKlogLevel(); got != tc.expected { + t.Errorf("Expected GetKlogLevel() to be %d, got: %d", tc.expected, got) + } + }) + } +} diff --git a/pkg/adapter/v2/configurator_environment.go b/pkg/adapter/v2/configurator_environment.go index 13b3a7fecc3..c1581f282c9 100644 --- a/pkg/adapter/v2/configurator_environment.go +++ b/pkg/adapter/v2/configurator_environment.go @@ -45,7 +45,14 @@ func NewLoggerConfiguratorFromEnvironment(env EnvConfigAccessor) LoggerConfigura } // CreateLogger based on environment variables. +// This also configures klog verbosity if set. func (c *loggerConfiguratorFromEnvironment) CreateLogger(ctx context.Context) *zap.SugaredLogger { + if accessor, ok := c.env.(interface{ GetKlogLevel() int }); ok { + level := accessor.GetKlogLevel() + if level > 0 { + SetupKlogLevel(level) + } + } return c.env.GetLogger() } diff --git a/pkg/adapter/v2/klog.go b/pkg/adapter/v2/klog.go new file mode 100644 index 00000000000..cdfb19bad45 --- /dev/null +++ b/pkg/adapter/v2/klog.go @@ -0,0 +1,30 @@ +/* +Copyright 2026 The Knative 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 adapter + +import ( + "strconv" + + "k8s.io/klog/v2" +) + +// SetupKlogLevel configures the klog verbosity level. +// The level should be a non-negative integer, where higher values are more verbose. +func SetupKlogLevel(level int) { + klogLevel := klog.Level(0) + klogLevel.Set(strconv.Itoa(level)) +} diff --git a/pkg/adapter/v2/klog_test.go b/pkg/adapter/v2/klog_test.go new file mode 100644 index 00000000000..4161de55532 --- /dev/null +++ b/pkg/adapter/v2/klog_test.go @@ -0,0 +1,66 @@ +/* +Copyright 2026 The Knative 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 adapter + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/klog/v2" +) + +func TestSetupKlogLevel(t *testing.T) { + testCases := []struct { + name string + level int + }{ + { + name: "Level 0 - minimal", + level: 0, + }, + { + name: "Level 2 - default", + level: 2, + }, + { + name: "Level 4 - debug", + level: 4, + }, + { + name: "Level 7 - verbose", + level: 7, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + SetupKlogLevel(tc.level) + + // Verify that the global klog verbosity is actually set + // klog.V(n).Enabled() returns true if the global level is >= n + if tc.level > 0 { + assert.True(t, klog.V(klog.Level(tc.level)).Enabled(), "klog verbosity %d should be enabled", tc.level) + } + + // Verify that a higher level is NOT enabled (unless we are at max level) + // This ensures we aren't just setting it to max everywhere + if tc.level < 10 { + assert.False(t, klog.V(klog.Level(tc.level+1)).Enabled(), "klog verbosity %d should NOT be enabled", tc.level+1) + } + }) + } +} diff --git a/pkg/reconciler/apiserversource/resources/receive_adapter_test.go b/pkg/reconciler/apiserversource/resources/receive_adapter_test.go index 446d464e92b..cebe58cd72e 100644 --- a/pkg/reconciler/apiserversource/resources/receive_adapter_test.go +++ b/pkg/reconciler/apiserversource/resources/receive_adapter_test.go @@ -178,6 +178,9 @@ O2dgzikq8iSy1BlRsVw= }, { Name: source.EnvObservabilityCfg, Value: "", + }, { + Name: source.EnvKlogLevel, + Value: "", }, }, ReadinessProbe: &corev1.Probe{ diff --git a/pkg/reconciler/pingsource/resources/receive_adapter_test.go b/pkg/reconciler/pingsource/resources/receive_adapter_test.go index 786902b0826..61eb2a14752 100644 --- a/pkg/reconciler/pingsource/resources/receive_adapter_test.go +++ b/pkg/reconciler/pingsource/resources/receive_adapter_test.go @@ -64,6 +64,9 @@ func TestMakePingAdapter(t *testing.T) { }, { Name: "K_OBSERVABILITY_CONFIG", Value: "", + }, { + Name: "K_KLOG_LEVEL", + Value: "", }, } diff --git a/pkg/reconciler/source/config_watcher.go b/pkg/reconciler/source/config_watcher.go index 4ee9b78e9f2..8ac5b98d99a 100644 --- a/pkg/reconciler/source/config_watcher.go +++ b/pkg/reconciler/source/config_watcher.go @@ -36,6 +36,8 @@ import ( const ( EnvLoggingCfg = "K_LOGGING_CONFIG" EnvObservabilityCfg = "K_OBSERVABILITY_CONFIG" + EnvKlogLevel = "K_KLOG_LEVEL" + klogLevelKey = "klogLevel" ) type ConfigAccessor interface { @@ -56,6 +58,9 @@ type ConfigWatcher struct { // configurations remain nil if disabled loggingCfg *logging.Config observabilityCfg *observability.Config + + // klogLevel is the verbosity level for klog (Kubernetes client-go logging). + klogLevel string } // configWatcherOption is a function option for ConfigWatchers. @@ -123,6 +128,11 @@ func (cw *ConfigWatcher) updateFromLoggingConfigMap(cfg *corev1.ConfigMap) { delete(cfg.Data, "_example") + // Extract klog level before parsing the rest of the config + if klogLevel, ok := cfg.Data[klogLevelKey]; ok { + cw.klogLevel = klogLevel + } + loggingCfg, err := logging.NewConfigFromConfigMap(cfg) if err != nil { cw.logger.Warnw("failed to create logging config from ConfigMap", zap.String("cfg.Name", cfg.Name)) @@ -161,10 +171,11 @@ func (cw *ConfigWatcher) updateFromObservabilityConfigMap(cfg *corev1.ConfigMap) // ToEnvVars serializes the contents of the ConfigWatcher to individual // environment variables. func (cw *ConfigWatcher) ToEnvVars() []corev1.EnvVar { - envs := make([]corev1.EnvVar, 0, 3) + envs := make([]corev1.EnvVar, 0, 4) envs = maybeAppendEnvVar(envs, cw.loggingConfigEnvVar(), cw.LoggingConfig() != nil) envs = maybeAppendEnvVar(envs, cw.observabilityConfigEnvVar(), cw.ObservabilityConfig() != nil) + envs = maybeAppendEnvVar(envs, cw.klogLevelEnvVar(), cw.klogLevel != "") return envs } @@ -206,7 +217,7 @@ func (cw *ConfigWatcher) loggingConfigEnvVar() corev1.EnvVar { } } -// loggingConfigEnvVar returns an EnvVar containing the serialized logging +// observabilityConfigEnvVar returns an EnvVar containing the serialized observability // configuration from the ConfigWatcher. func (cw *ConfigWatcher) observabilityConfigEnvVar() corev1.EnvVar { obsCfg := cw.ObservabilityConfig() @@ -225,6 +236,14 @@ func (cw *ConfigWatcher) observabilityConfigEnvVar() corev1.EnvVar { } } +// klogLevelEnvVar returns an EnvVar containing the klog verbosity level. +func (cw *ConfigWatcher) klogLevelEnvVar() corev1.EnvVar { + return corev1.EnvVar{ + Name: EnvKlogLevel, + Value: cw.klogLevel, + } +} + // overrideLoggingLevel returns cfg with the given logging level applied. func overrideLoggingLevel(cfg *logging.Config, lvl zapcore.Level) (*logging.Config, error) { tmpCfg := &zapConfig{} @@ -255,6 +274,7 @@ func (g *EmptyVarsGenerator) ToEnvVars() []corev1.EnvVar { return []corev1.EnvVar{ {Name: EnvLoggingCfg}, {Name: EnvObservabilityCfg}, + {Name: EnvKlogLevel}, } } diff --git a/pkg/reconciler/source/config_watcher_test.go b/pkg/reconciler/source/config_watcher_test.go index 2f1a8c08231..ae9006e1520 100644 --- a/pkg/reconciler/source/config_watcher_test.go +++ b/pkg/reconciler/source/config_watcher_test.go @@ -62,8 +62,9 @@ func TestNewConfigWatcher_defaults(t *testing.T) { envs := cw.ToEnvVars() - const expectEnvs = 2 - require.Lenf(t, envs, expectEnvs, "there should be %d env var(s)", expectEnvs) + // With sample data we have 3 env vars (logging, observability, klogLevel) + // With empty data we have 2 env vars (logging, observability) + require.GreaterOrEqual(t, len(envs), 2, "there should be at least 2 env var(s)") assert.Equal(t, EnvLoggingCfg, envs[0].Name, "first env var is logging config") assert.Contains(t, envs[0].Value, tc.expectLoggingContains) @@ -87,7 +88,7 @@ func TestLoggingConfigWithCustomLoggingLevel(t *testing.T) { func TestEmptyVarsGenerator(t *testing.T) { g := &EmptyVarsGenerator{} envs := g.ToEnvVars() - const expectEnvs = 2 + const expectEnvs = 3 require.Lenf(t, envs, expectEnvs, "there should be %d env var(s)", expectEnvs) } @@ -141,6 +142,7 @@ func loggingConfigMapData() map[string]string { "zap-logger-config": `{"level": "fatal"}`, "loglevel." + testComponentWithCustomLogLevel: "debug", + "klogLevel": "4", } } func observabilityConfigMapData() map[string]string {