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  }