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 }