istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/log/config_test.go (about)

     1  // Copyright 2017 Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package log
    16  
    17  import (
    18  	"log"
    19  	"os"
    20  	"regexp"
    21  	"strconv"
    22  	"strings"
    23  	"sync"
    24  	"testing"
    25  	"time"
    26  
    27  	"go.uber.org/zap"
    28  	"go.uber.org/zap/zapcore"
    29  	"google.golang.org/grpc/grpclog"
    30  )
    31  
    32  const timePattern = "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9][0-9][0-9][0-9][0-9]Z"
    33  
    34  type testDateEncoder struct {
    35  	zapcore.PrimitiveArrayEncoder
    36  	output string
    37  }
    38  
    39  func (tde *testDateEncoder) AppendString(s string) {
    40  	tde.output = s
    41  }
    42  
    43  func TestTimestampProperYear(t *testing.T) {
    44  	testEnc := &testDateEncoder{}
    45  	cases := []struct {
    46  		name  string
    47  		input time.Time
    48  		want  string
    49  	}{
    50  		{"1", time.Date(1, time.April, 1, 1, 1, 1, 1, time.UTC), "0001"},
    51  		{"1989", time.Date(1989, time.February, 1, 1, 1, 1, 1, time.UTC), "1989"},
    52  		{"2017", time.Date(2017, time.January, 1, 1, 1, 1, 1, time.UTC), "2017"},
    53  		{"2083", time.Date(2083, time.March, 1, 1, 1, 1, 1, time.UTC), "2083"},
    54  		{"2573", time.Date(2573, time.June, 1, 1, 1, 1, 1, time.UTC), "2573"},
    55  		{"9999", time.Date(9999, time.May, 1, 1, 1, 1, 1, time.UTC), "9999"},
    56  	}
    57  
    58  	for _, v := range cases {
    59  		t.Run(v.name, func(t *testing.T) {
    60  			formatDate(v.input, testEnc)
    61  			if !strings.HasPrefix(testEnc.output, v.want) {
    62  				t.Errorf("formatDate(%v) => %s, want year: %s", v.input, testEnc.output, v.want)
    63  			}
    64  		})
    65  	}
    66  }
    67  
    68  func TestTimestampProperMicros(t *testing.T) {
    69  	testEnc := &testDateEncoder{}
    70  	cases := []struct {
    71  		name  string
    72  		input time.Time
    73  		want  string
    74  	}{
    75  		{"1", time.Date(2017, time.April, 1, 1, 1, 1, 1000, time.UTC), "1"},
    76  		{"99", time.Date(1989, time.February, 1, 1, 1, 1, 99000, time.UTC), "99"},
    77  		{"999", time.Date(2017, time.January, 1, 1, 1, 1, 999000, time.UTC), "999"},
    78  		{"9999", time.Date(2083, time.March, 1, 1, 1, 1, 9999000, time.UTC), "9999"},
    79  		{"99999", time.Date(2083, time.March, 1, 1, 1, 1, 99999000, time.UTC), "99999"},
    80  		{"999999", time.Date(2083, time.March, 1, 1, 1, 1, 999999000, time.UTC), "999999"},
    81  	}
    82  
    83  	for _, v := range cases {
    84  		t.Run(v.name, func(t *testing.T) {
    85  			formatDate(v.input, testEnc)
    86  			if !strings.HasSuffix(testEnc.output, v.want+"Z") {
    87  				t.Errorf("formatDate(%v) => %s, want micros: %s", v.input, testEnc.output, v.want)
    88  			}
    89  		})
    90  	}
    91  }
    92  
    93  func TestOverrides(t *testing.T) {
    94  	resetGlobals()
    95  	s := RegisterScope("TestOverrides", "For testing")
    96  
    97  	o := DefaultOptions()
    98  	o.outputLevels = "default:debug,all:info"
    99  	if err := Configure(o); err != nil {
   100  		t.Errorf("Expecting success, got %v", err)
   101  	} else if s.GetOutputLevel() != InfoLevel {
   102  		t.Errorf("Expecting InfoLevel, got %v", s.GetOutputLevel())
   103  	}
   104  
   105  	o = DefaultOptions()
   106  	o.stackTraceLevels = "default:debug,all:info"
   107  	if err := Configure(o); err != nil {
   108  		t.Errorf("Expecting success, got %v", err)
   109  	} else if s.GetStackTraceLevel() != InfoLevel {
   110  		t.Errorf("Expecting InfoLevel, got %v", s.GetStackTraceLevel())
   111  	}
   112  
   113  	o = DefaultOptions()
   114  	o.logCallers = "all"
   115  	if err := Configure(o); err != nil {
   116  		t.Errorf("Expecting success, got %v", err)
   117  	} else if !s.GetLogCallers() {
   118  		t.Error("Expecting true, got false")
   119  	}
   120  }
   121  
   122  func TestGrpc(t *testing.T) {
   123  	resetGlobals()
   124  
   125  	o := DefaultOptions()
   126  	o.outputLevels = "grpc:info"
   127  	if err := Configure(o); err != nil {
   128  		t.Errorf("Expecting success, got %v", err)
   129  	} else if !o.logGRPC {
   130  		t.Errorf("Expecting LogGrpc, got %v", o.logGRPC)
   131  	}
   132  }
   133  
   134  func TestOddballs(t *testing.T) {
   135  	resetGlobals()
   136  
   137  	o := DefaultOptions()
   138  	_ = Configure(o)
   139  
   140  	o = DefaultOptions()
   141  	o.outputLevels = "default,,"
   142  	err := Configure(o)
   143  	if err == nil {
   144  		t.Error("Got success, expected failure")
   145  	}
   146  
   147  	o = DefaultOptions()
   148  	o.outputLevels = "foobar"
   149  	err = Configure(o)
   150  	if err == nil {
   151  		t.Error("Got success, expected failure")
   152  	}
   153  
   154  	o = DefaultOptions()
   155  	o.outputLevels = "foobar:debug"
   156  	err = Configure(o)
   157  	if err != nil {
   158  		t.Error("Got success, expected failure")
   159  	}
   160  
   161  	o = DefaultOptions()
   162  	o.stackTraceLevels = "default,,"
   163  	err = Configure(o)
   164  	if err == nil {
   165  		t.Error("Got success, expected failure")
   166  	}
   167  
   168  	o = DefaultOptions()
   169  	o.stackTraceLevels = "foobar"
   170  	err = Configure(o)
   171  	if err == nil {
   172  		t.Error("Got success, expected failure")
   173  	}
   174  
   175  	o = DefaultOptions()
   176  	o.stackTraceLevels = "foobar:debug"
   177  	err = Configure(o)
   178  	if err != nil {
   179  		t.Error("Got success, expected failure")
   180  	}
   181  
   182  	o = DefaultOptions()
   183  	o.logCallers = "foobar"
   184  	err = Configure(o)
   185  	if err != nil {
   186  		t.Error("Got success, expected failure")
   187  	}
   188  
   189  	o = DefaultOptions()
   190  	// using invalid filename
   191  	o.OutputPaths = []string{"//"}
   192  	err = Configure(o)
   193  	if err == nil {
   194  		t.Errorf("Got success, expecting error")
   195  	}
   196  
   197  	o = DefaultOptions()
   198  	o.ErrorOutputPaths = []string{"//"}
   199  	err = Configure(o)
   200  	if err == nil {
   201  		t.Errorf("Got success, expecting error")
   202  	}
   203  }
   204  
   205  func TestRotateNoStdout(t *testing.T) {
   206  	// Ensure that rotation is setup properly
   207  
   208  	dir := t.TempDir()
   209  
   210  	file := dir + "/rot.log"
   211  
   212  	o := DefaultOptions()
   213  	o.OutputPaths = []string{}
   214  	o.RotateOutputPath = file
   215  	if err := Configure(o); err != nil {
   216  		t.Fatalf("Unable to configure logging: %v", err)
   217  	}
   218  
   219  	defaultScope.Error("HELLO")
   220  	Sync() // nolint: errcheck
   221  
   222  	content, err := os.ReadFile(file)
   223  	if err != nil {
   224  		t.Errorf("Got failure '%v', expecting success", err)
   225  	}
   226  
   227  	lines := strings.Split(string(content), "\n")
   228  	if !strings.Contains(lines[0], "HELLO") {
   229  		t.Errorf("Expecting for first line of log to contain HELLO, got %s", lines[0])
   230  	}
   231  }
   232  
   233  func TestRotateAndStdout(t *testing.T) {
   234  	dir := t.TempDir()
   235  
   236  	file := dir + "/rot.log"
   237  
   238  	stdoutLines, _ := captureStdout(func() {
   239  		o := DefaultOptions()
   240  		o.RotateOutputPath = file
   241  		if err := Configure(o); err != nil {
   242  			t.Fatalf("Unable to configure logger: %v", err)
   243  		}
   244  
   245  		defaultScope.Error("HELLO")
   246  		Sync() // nolint: errcheck
   247  
   248  		content, err := os.ReadFile(file)
   249  		if err != nil {
   250  			t.Errorf("Got failure '%v', expecting success", err)
   251  		}
   252  
   253  		rotLines := strings.Split(string(content), "\n")
   254  		if !strings.Contains(rotLines[0], "HELLO") {
   255  			t.Errorf("Expecting for first line of log to contain HELLO, got %s", rotLines[0])
   256  		}
   257  	})
   258  
   259  	if !strings.Contains(stdoutLines[0], "HELLO") {
   260  		t.Errorf("Expecting for first line of log to contain HELLO, got %s", stdoutLines[0])
   261  	}
   262  }
   263  
   264  func TestRotateMaxBackups(t *testing.T) {
   265  	dir := t.TempDir()
   266  	file := dir + "/rot.log"
   267  
   268  	o := DefaultOptions()
   269  	o.OutputPaths = []string{}
   270  	o.RotateOutputPath = file
   271  	o.RotationMaxSize = 1
   272  	o.RotationMaxBackups = 2
   273  	o.RotationMaxAge = 30
   274  
   275  	if err := Configure(o); err != nil {
   276  		t.Fatalf("Unable to configure logger: %v", err)
   277  	}
   278  
   279  	// construct a log string that contains 128 characters
   280  	line := ""
   281  	for i := 0; i < 8; i++ {
   282  		line += "0123456789ABCDEF" // 16 characters
   283  	}
   284  
   285  	// make sure that all log outputs is much larger than 2M
   286  	wg := sync.WaitGroup{}
   287  	for i := 0; i < 4; i++ {
   288  		wg.Add(1)
   289  		go func() {
   290  			for ii := 0; ii < 1024; ii++ {
   291  				for j := 0; j < 8; j++ {
   292  					log.Println(line)
   293  				}
   294  			}
   295  			wg.Done()
   296  		}()
   297  	}
   298  	wg.Wait()
   299  	Sync()
   300  
   301  	ticker := time.NewTicker(100 * time.Millisecond)
   302  	defer ticker.Stop()
   303  
   304  	var rd []os.DirEntry
   305  
   306  	for {
   307  		select {
   308  		case <-ticker.C:
   309  			rd, err := os.ReadDir(dir)
   310  			if err != nil {
   311  				t.Fatalf("Unable to read dir: %v", err)
   312  			}
   313  			if len(rd) == o.RotationMaxBackups+1 {
   314  				// perfect, we're done
   315  				return
   316  			}
   317  		case <-time.After(5 * time.Second):
   318  			for _, f := range rd {
   319  				t.Log(f.Name())
   320  			}
   321  			t.Errorf("Expecting at most %d backup logs, but %s has %d", o.RotationMaxBackups, dir, len(rd)-1)
   322  		}
   323  	}
   324  }
   325  
   326  func TestCapture(t *testing.T) {
   327  	lines, _ := captureStdout(func() {
   328  		o := DefaultOptions()
   329  		o.logCallers = "default"
   330  		o.SetDefaultOutputLevel(DefaultScopeName, DebugLevel)
   331  		o.SetDefaultOutputLevel(GrpcScopeName, DebugLevel)
   332  		_ = Configure(o)
   333  
   334  		// output to the plain golang "log" package
   335  		log.Println("golang")
   336  
   337  		// output to the gRPC logging package
   338  		grpclog.Error("grpc-error")
   339  		grpclog.Warning("grpc-warn")
   340  		grpclog.Info("grpc-info")
   341  
   342  		// output directly to zap
   343  		zap.L().Error("zap-error")
   344  		zap.L().Warn("zap-warn")
   345  		zap.L().Info("zap-info")
   346  		zap.L().Debug("zap-debug")
   347  
   348  		l := zap.L().With(zap.String("a", "b"))
   349  		l.Error("zap-with")
   350  
   351  		entry := zapcore.Entry{
   352  			Message: "zap-write",
   353  			Level:   zapcore.ErrorLevel,
   354  		}
   355  		_ = zap.L().Core().Write(entry, nil)
   356  
   357  		defaultScope.SetOutputLevel(NoneLevel)
   358  		grpcScope.SetOutputLevel(NoneLevel)
   359  
   360  		// all these get thrown out since the level is set to none
   361  		log.Println("golang-2")
   362  		grpclog.Error("grpc-error-2")
   363  		grpclog.Warning("grpc-warn-2")
   364  		grpclog.Info("grpc-info-2")
   365  		zap.L().Error("zap-error-2")
   366  		zap.L().Warn("zap-warn-2")
   367  		zap.L().Info("zap-info-2")
   368  		zap.L().Debug("zap-debug-2")
   369  	})
   370  
   371  	patterns := []string{
   372  		timePattern + "\tinfo\tlog/config_test.go:.*\tgolang",
   373  		timePattern + "\terror\tlog/config_test.go:.*\tgrpc-error", // gRPC errors and warnings come out as info
   374  		timePattern + "\twarn\tlog/config_test.go:.*\tgrpc-warn",
   375  		timePattern + "\tinfo\tlog/config_test.go:.*\tgrpc-info",
   376  		timePattern + "\terror\tlog/config_test.go:.*\tzap-error",
   377  		timePattern + "\twarn\tlog/config_test.go:.*\tzap-warn",
   378  		timePattern + "\tinfo\tlog/config_test.go:.*\tzap-info",
   379  		timePattern + "\tdebug\tlog/config_test.go:.*\tzap-debug",
   380  		timePattern + "\terror\tlog/config_test.go:.*\tzap-with",
   381  		"error\tzap-write",
   382  		"",
   383  	}
   384  
   385  	if len(lines) > len(patterns) {
   386  		t.Errorf("Expecting %d lines of output, but got %d", len(patterns), len(lines))
   387  
   388  		for i := len(patterns); i < len(lines); i++ {
   389  			t.Errorf("  Extra line of output: %s", lines[i])
   390  		}
   391  	}
   392  
   393  	for i, pat := range patterns {
   394  		t.Run(strconv.Itoa(i), func(t *testing.T) {
   395  			match, _ := regexp.MatchString(pat, lines[i])
   396  			if !match {
   397  				t.Errorf("Got '%s', expecting to match '%s'", lines[i], pat)
   398  			}
   399  		})
   400  	}
   401  
   402  	lines, _ = captureStdout(func() {
   403  		o := DefaultOptions()
   404  		o.stackTraceLevels = "default:debug"
   405  		o.SetDefaultOutputLevel(DefaultScopeName, DebugLevel)
   406  		_ = Configure(o)
   407  		log.Println("golang")
   408  	})
   409  
   410  	for _, line := range lines {
   411  		// see if the captured output contains the current file name
   412  		if strings.Contains(line, "config_test.go") {
   413  			return
   414  		}
   415  	}
   416  
   417  	t.Error("Could not find stack trace info in output")
   418  }
   419  
   420  // Runs the given function while capturing everything sent to stdout
   421  func captureStdout(f func()) ([]string, error) {
   422  	tf, err := os.CreateTemp("", "log_test")
   423  	if err != nil {
   424  		return nil, err
   425  	}
   426  
   427  	old := os.Stdout
   428  	os.Stdout = tf
   429  
   430  	f()
   431  
   432  	os.Stdout = old
   433  	path := tf.Name()
   434  	_ = tf.Sync()
   435  	_ = tf.Close()
   436  
   437  	content, err := os.ReadFile(path)
   438  	_ = os.Remove(path)
   439  
   440  	if err != nil {
   441  		return nil, err
   442  	}
   443  
   444  	return strings.Split(string(content), "\n"), nil
   445  }
   446  
   447  func resetGlobals() {
   448  	scopes = make(map[string]*Scope, 1)
   449  	defaultScope, grpcScope = registerDefaultScopes()
   450  }