knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/logging/config_test.go (about) 1 /* 2 Copyright 2018 The Knative Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package logging 18 19 import ( 20 "fmt" 21 "testing" 22 23 "github.com/google/go-cmp/cmp" 24 25 "go.uber.org/zap" 26 "go.uber.org/zap/zapcore" 27 corev1 "k8s.io/api/core/v1" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 ) 30 31 func TestNewLogger(t *testing.T) { 32 logger, _ := NewLogger("", "") 33 if logger == nil { 34 t.Error("expected a non-nil logger") 35 } 36 37 logger, _ = NewLogger("some invalid JSON here", "") 38 if logger == nil { 39 t.Error("expected a non-nil logger") 40 } 41 42 logger, atomicLevel := NewLogger("", "debug") 43 if logger == nil { 44 t.Error("expected a non-nil logger") 45 } 46 if atomicLevel.Level() != zapcore.DebugLevel { 47 t.Error("expected level to be debug") 48 } 49 50 // No good way to test if all the config is applied, 51 // but at the minimum, we can check and see if level is getting applied. 52 logger, atomicLevel = NewLogger(`{"level": "error", "outputPaths": ["stdout"], "errorOutputPaths": ["stderr"], "encoding": "json"}`, "") 53 if logger == nil { 54 t.Error("expected a non-nil logger") 55 } 56 if ce := logger.Desugar().Check(zap.InfoLevel, "test"); ce != nil { 57 t.Error("not expected to get info logs from the logger configured with error as min threshold") 58 } 59 if ce := logger.Desugar().Check(zap.ErrorLevel, "test"); ce == nil { 60 t.Error("expected to get error logs from the logger configured with error as min threshold") 61 } 62 if atomicLevel.Level() != zapcore.ErrorLevel { 63 t.Errorf("expected atomicLevel.Level() to be ErrorLevel but got %v.", atomicLevel.Level()) 64 } 65 66 logger, atomicLevel = NewLogger(`{"level": "info", "outputPaths": ["stdout"], "errorOutputPaths": ["stderr"], "encoding": "json"}`, "") 67 if logger == nil { 68 t.Error("expected a non-nil logger") 69 } 70 if ce := logger.Desugar().Check(zap.DebugLevel, "test"); ce != nil { 71 t.Error("not expected to get debug logs from the logger configured with info as min threshold") 72 } 73 if ce := logger.Desugar().Check(zap.InfoLevel, "test"); ce == nil { 74 t.Error("expected to get info logs from the logger configured with info as min threshold") 75 } 76 if atomicLevel.Level() != zapcore.InfoLevel { 77 t.Errorf("expected atomicLevel.Level() to be InfoLevel but got %v.", atomicLevel.Level()) 78 } 79 80 // Let's change the logging level using atomicLevel 81 atomicLevel.SetLevel(zapcore.ErrorLevel) 82 if ce := logger.Desugar().Check(zap.InfoLevel, "test"); ce != nil { 83 t.Error("not expected to get info logs from the logger configured with error as min threshold") 84 } 85 if ce := logger.Desugar().Check(zap.ErrorLevel, "test"); ce == nil { 86 t.Error("expected to get error logs from the logger configured with error as min threshold") 87 } 88 if atomicLevel.Level() != zapcore.ErrorLevel { 89 t.Errorf("expected atomicLevel.Level() to be ErrorLevel but got %v.", atomicLevel.Level()) 90 } 91 92 // Test logging override 93 logger, _ = NewLogger(`{"level": "error", "outputPaths": ["stdout"], "errorOutputPaths": ["stderr"], "encoding": "json"}`, "info") 94 if logger == nil { 95 t.Error("expected a non-nil logger") 96 } 97 if ce := logger.Desugar().Check(zap.DebugLevel, "test"); ce != nil { 98 t.Error("not expected to get debug logs from the logger configured with info as min threshold") 99 } 100 if ce := logger.Desugar().Check(zap.InfoLevel, "test"); ce == nil { 101 t.Error("expected to get info logs from the logger configured with info as min threshold") 102 } 103 104 // Invalid logging override 105 logger, _ = NewLogger(`{"level": "error", "outputPaths": ["stdout"], "errorOutputPaths": ["stderr"], "encoding": "json"}`, "randomstring") 106 if logger == nil { 107 t.Error("expected a non-nil logger") 108 } 109 if ce := logger.Desugar().Check(zap.InfoLevel, "test"); ce != nil { 110 t.Error("not expected to get info logs from the logger configured with error as min threshold") 111 } 112 if ce := logger.Desugar().Check(zap.ErrorLevel, "test"); ce == nil { 113 t.Error("expected to get error logs from the logger configured with error as min threshold") 114 } 115 } 116 117 func TestNewConfigNoEntry(t *testing.T) { 118 c, err := NewConfigFromConfigMap(&corev1.ConfigMap{ 119 ObjectMeta: metav1.ObjectMeta{ 120 Namespace: "knative-something", 121 Name: "config-logging", 122 }, 123 }) 124 if err != nil { 125 t.Error("Expected no errors. got:", err) 126 } 127 if got, want := c.LoggingConfig, ""; got != want { 128 t.Errorf("LoggingConfig = %v, want %v", got, want) 129 } 130 if got, want := len(c.LoggingLevel), 0; got != want { 131 t.Errorf("len(LoggingLevel) = %v, want %v", got, want) 132 } 133 } 134 135 func TestNewConfig(t *testing.T) { 136 const wantCfg = `{"level": "error", "outputPaths": ["stdout"], "errorOutputPaths": ["stderr"], "encoding": "json"}` 137 const wantLevel = zapcore.ErrorLevel 138 c, err := NewConfigFromConfigMap(&corev1.ConfigMap{ 139 ObjectMeta: metav1.ObjectMeta{ 140 Namespace: "knative-something", 141 Name: "config-logging", 142 }, 143 Data: map[string]string{ 144 "zap-logger-config": wantCfg, 145 "loglevel.queueproxy": wantLevel.String(), 146 }, 147 }) 148 if err != nil { 149 t.Error("Expected no errors. got:", err) 150 } 151 if got := c.LoggingConfig; got != wantCfg { 152 t.Errorf("LoggingConfig = %v, want %v", got, wantCfg) 153 } 154 if got := c.LoggingLevel["queueproxy"]; got != wantLevel { 155 t.Errorf("LoggingLevel[queueproxy] = %v, want %v", got, wantLevel) 156 } 157 } 158 159 func TestNewLoggerFromConfig(t *testing.T) { 160 const componentName = "queueproxy" 161 162 testCases := []struct { 163 name string 164 cfg *Config 165 expectLvl zapcore.Level 166 expectName string 167 }{{ 168 name: "Has component log level when component-specific level is defined", 169 cfg: makeTestConfig( 170 withGlobalLevel("error"), 171 withComponentLevel(componentName, "debug"), 172 ), 173 expectLvl: zapcore.DebugLevel, 174 expectName: componentName, 175 }, { 176 name: "Has global log level when no component-specific level is defined", 177 cfg: makeTestConfig( 178 withGlobalLevel("error"), 179 ), 180 expectLvl: zapcore.ErrorLevel, 181 expectName: componentName, 182 }, { 183 name: "Has default level when config is empty", 184 cfg: makeTestConfig(), 185 expectLvl: zapcore.InfoLevel, 186 expectName: componentName, 187 }} 188 189 for _, tc := range testCases { 190 t.Run(tc.name, func(t *testing.T) { 191 logger, atomicLevel := NewLoggerFromConfig(tc.cfg, componentName) 192 193 if got, want := atomicLevel.Level(), tc.expectLvl; got != want { 194 t.Errorf("Log Level = %q, want: %q", got, want) 195 } 196 197 loggerName := logger.Desugar().Check(zapcore.FatalLevel, "test").LoggerName 198 if loggerName != tc.expectName { 199 t.Errorf("Logger Name = %q, want: %q", loggerName, tc.expectName) 200 } 201 }) 202 } 203 } 204 205 func TestEmptyLevel(t *testing.T) { 206 c, err := NewConfigFromConfigMap(&corev1.ConfigMap{ 207 ObjectMeta: metav1.ObjectMeta{ 208 Namespace: "knative-something", 209 Name: "config-logging", 210 }, 211 Data: map[string]string{ 212 "zap-logger-config": `{"level": "error", "outputPaths": ["stdout"], "errorOutputPaths": ["stderr"], "encoding": "json"}`, 213 "loglevel.queueproxy": "", 214 }, 215 }) 216 if err != nil { 217 t.Error("Expected no errors. got:", err) 218 } 219 if l := c.LoggingLevel["queueproxy"]; l != zapcore.InfoLevel { 220 t.Error("Expected default Info level for LoggingLevel[queueproxy]. got:", l) 221 } 222 } 223 224 func TestDefaultLevel(t *testing.T) { 225 c, err := NewConfigFromConfigMap(&corev1.ConfigMap{ 226 ObjectMeta: metav1.ObjectMeta{ 227 Namespace: "knative-something", 228 Name: "config-logging", 229 }, 230 }) 231 if err != nil { 232 t.Error("Expected no errors. got:", err) 233 } 234 if l := c.LoggingLevel["queueproxy"]; l != zapcore.InfoLevel { 235 t.Error("Expected default Info level for LoggingLevel[queueproxy]. got:", l) 236 } 237 } 238 239 func TestInvalidComponentLevel(t *testing.T) { 240 _, err := NewConfigFromConfigMap(&corev1.ConfigMap{ 241 ObjectMeta: metav1.ObjectMeta{ 242 Namespace: "knative-something", 243 Name: "config-logging", 244 }, 245 Data: map[string]string{ 246 "zap-logger-config": `{"level": "error", "outputPaths": ["stdout"], "errorOutputPaths": ["stderr"], "encoding": "json"}`, 247 "loglevel.queueproxy": "invalid", 248 }, 249 }) 250 if err == nil { 251 t.Error("Expected errors when invalid level is present in logging config. got nothing") 252 } 253 } 254 255 func TestEmptyComponentName(t *testing.T) { 256 c, err := NewConfigFromConfigMap(&corev1.ConfigMap{ 257 ObjectMeta: metav1.ObjectMeta{ 258 Namespace: "knative-something", 259 Name: "config-logging", 260 }, 261 Data: map[string]string{ 262 "zap-logger-config": `{"level": "warn", "outputPaths": ["stdout"], "errorOutputPaths": ["stderr"], "encoding": "json"}`, 263 "loglevel.": zapcore.ErrorLevel.String(), 264 }, 265 }) 266 if err != nil { 267 t.Error("Expected no errors. got:", err) 268 } 269 // The empty string component should have been ignored, so it should be the default Info, rather 270 // than Error as set in the config map. 271 if got := c.LoggingLevel[""]; got != zapcore.InfoLevel { 272 t.Errorf(`LoggingLevel[""] = %v, want: InfoLevel`, got) 273 } 274 } 275 276 func TestUpdateLevelFromConfigMap(t *testing.T) { 277 const ( 278 componentLevel = zapcore.PanicLevel 279 globalLevel = zapcore.WarnLevel 280 defaultLevel = zapcore.InfoLevel 281 componentName = "controller" 282 componentLogKey = "loglevel." + componentName 283 ) 284 285 testCm := &corev1.ConfigMap{ 286 ObjectMeta: metav1.ObjectMeta{ 287 Namespace: "knative-something", 288 Name: "config-logging", 289 }, 290 Data: map[string]string{ 291 loggerConfigKey: fmt.Sprintf(`{"level": %q}`, globalLevel), 292 componentLogKey: componentLevel.String(), 293 }, 294 } 295 296 logger := zap.NewExample().Sugar() 297 298 t.Run("Successive component level updates", func(t *testing.T) { 299 // start at debug level 300 atomicLevel := zap.NewAtomicLevelAt(zapcore.DebugLevel) 301 302 testSequence := []struct { 303 setLevel string 304 wantLevel zapcore.Level 305 }{ 306 {"info", zapcore.InfoLevel}, 307 {"error", zapcore.ErrorLevel}, 308 {"invalid", zapcore.ErrorLevel}, 309 {"debug", zapcore.DebugLevel}, 310 {"debug", zapcore.DebugLevel}, 311 } 312 313 cm := testCm.DeepCopy() 314 315 UpdateLevelFromConfigMap(logger, atomicLevel, componentName)(cm) 316 317 // initial level check 318 has := atomicLevel.Level() 319 want := componentLevel 320 if has != want { 321 t.Errorf("Initial Log Level = %q, want: %q", has, want) 322 } 323 324 // update level sequentially 325 for _, tt := range testSequence { 326 t.Run(tt.setLevel, func(t *testing.T) { 327 cm.Data["loglevel.controller"] = tt.setLevel 328 UpdateLevelFromConfigMap(logger, atomicLevel, componentName)(cm) 329 330 has := atomicLevel.Level() 331 want := tt.wantLevel 332 if has != want { 333 t.Errorf("Log Level = %q, want: %q", has, want) 334 } 335 }) 336 } 337 }) 338 339 t.Run("Undefined component config", func(t *testing.T) { 340 // start at debug level 341 atomicLevel := zap.NewAtomicLevelAt(zapcore.DebugLevel) 342 343 cm := testCm.DeepCopy() 344 345 testSequence := []struct { 346 updateFn func(*corev1.ConfigMap) 347 wantLevel zapcore.Level 348 }{{ 349 // Component deleted, level set to global value 350 updateFn: func(cm *corev1.ConfigMap) { 351 delete(cm.Data, componentLogKey) 352 }, 353 wantLevel: globalLevel, 354 }, { 355 // Updated logger config, level set to new value 356 updateFn: func(cm *corev1.ConfigMap) { 357 cm.Data[loggerConfigKey] = `{"level": "error"}` 358 }, 359 wantLevel: zapcore.ErrorLevel, 360 }, { 361 // Invalid logger config, previous value retained 362 updateFn: func(cm *corev1.ConfigMap) { 363 cm.Data[loggerConfigKey] = "not_a_JSON" 364 }, 365 wantLevel: zapcore.ErrorLevel, 366 }, { 367 // Logger config deleted, level set to default value 368 updateFn: func(cm *corev1.ConfigMap) { 369 delete(cm.Data, loggerConfigKey) 370 }, 371 wantLevel: defaultLevel, 372 }} 373 374 for i, tt := range testSequence { 375 tt.updateFn(cm) 376 UpdateLevelFromConfigMap(logger, atomicLevel, componentName)(cm) 377 378 has := atomicLevel.Level() 379 want := tt.wantLevel 380 if has != want { 381 t.Errorf("%d: Expected log level to be %q, got %q", i, want, has) 382 } 383 } 384 }) 385 } 386 387 func TestLoggingConfig(t *testing.T) { 388 testCases := []struct { 389 name string 390 cfg *Config 391 want string 392 wantErr string 393 }{{ 394 name: "nil", 395 cfg: nil, 396 want: "", 397 wantErr: errEmptyJSONLogginString.Error(), 398 }, { 399 name: "happy", 400 cfg: &Config{ 401 LoggingConfig: "{}", 402 LoggingLevel: map[string]zapcore.Level{}, 403 }, 404 want: `{"zap-logger-config":"{}"}`, 405 }} 406 for _, tc := range testCases { 407 t.Run(tc.name, func(t *testing.T) { 408 json, err := ConfigToJSON(tc.cfg) 409 if err != nil { 410 t.Error("Error while converting logging config to json:", err) 411 } 412 // Test to json. 413 t.Run("to JSON", func(t *testing.T) { 414 if got, want := json, tc.want; !cmp.Equal(got, want) { 415 t.Errorf("unexpected (-want, +got) =\n%s", cmp.Diff(want, got)) 416 } 417 }) 418 t.Run("from JSON", func(t *testing.T) { 419 want := tc.cfg 420 got, gotErr := JSONToConfig(tc.want) 421 422 if gotErr != nil { 423 if diff := cmp.Diff(tc.wantErr, gotErr.Error()); diff != "" { 424 t.Error("unexpected err (-want, +got) =", diff) 425 } 426 } else if tc.wantErr != "" { 427 t.Error("expected err", tc.wantErr) 428 } 429 430 if diff := cmp.Diff(want, got); diff != "" { 431 t.Errorf("Unexpected Config: (-want, +got) =\n%s", diff) 432 } 433 }) 434 }) 435 } 436 } 437 438 func makeTestConfig(opts ...testConfigOption) *Config { 439 cfg := &Config{} 440 441 for _, opt := range opts { 442 cfg = opt(cfg) 443 } 444 445 return cfg 446 } 447 448 type testConfigOption func(*Config) *Config 449 450 func withGlobalLevel(lvl string) testConfigOption { 451 return func(cfg *Config) *Config { 452 cfg.LoggingConfig = fmt.Sprintf("{"+ 453 `"level": %q, `+ 454 `"outputPaths": ["stdout"], `+ 455 `"errorOutputPaths": ["stderr"], `+ 456 `"encoding": "json"`+ 457 "}", lvl) 458 459 return cfg 460 } 461 } 462 463 func withComponentLevel(name, lvl string) testConfigOption { 464 return func(cfg *Config) *Config { 465 logLvl, err := levelFromString(lvl) 466 if err != nil { 467 return cfg 468 } 469 470 if cfg.LoggingLevel == nil { 471 cfg.LoggingLevel = make(map[string]zapcore.Level, 1) 472 } 473 cfg.LoggingLevel[name] = *logLvl 474 return cfg 475 } 476 } 477 478 func TestConfigMapName(t *testing.T) { 479 if got, want := ConfigMapName(), "config-logging"; got != want { 480 t.Errorf("ConfigMapName = %q, want: %q", got, want) 481 } 482 t.Setenv(configMapNameEnv, "") 483 if got, want := ConfigMapName(), "config-logging"; got != want { 484 t.Errorf("ConfigMapName = %q, want: %q", got, want) 485 } 486 t.Setenv(configMapNameEnv, "slowly-dying-inside") 487 if got, want := ConfigMapName(), "slowly-dying-inside"; got != want { 488 t.Errorf("ConfigMapName = %q, want: %q", got, want) 489 } 490 }