trpc.group/trpc-go/trpc-go@v1.0.2/log/rollwriter/roll_writer_test.go (about) 1 // 2 // 3 // Tencent is pleased to support the open source community by making tRPC available. 4 // 5 // Copyright (C) 2023 THL A29 Limited, a Tencent company. 6 // All rights reserved. 7 // 8 // If you have downloaded a copy of the tRPC source code from Tencent, 9 // please note that tRPC source code is licensed under the Apache 2.0 License, 10 // A copy of the Apache 2.0 License is included in this file. 11 // 12 // 13 14 package rollwriter 15 16 import ( 17 "errors" 18 "fmt" 19 "io/fs" 20 "log" 21 "os" 22 "os/exec" 23 "path" 24 "path/filepath" 25 "runtime" 26 "strings" 27 "sync" 28 "testing" 29 "time" 30 31 "github.com/stretchr/testify/assert" 32 "github.com/stretchr/testify/require" 33 "go.uber.org/zap" 34 "go.uber.org/zap/zapcore" 35 ) 36 37 // functional test 38 // go test -v -cover 39 // benchmark test 40 // go test -bench=. -benchtime=20s -run=Bench 41 42 const ( 43 testTimes = 100000 44 testRoutines = 256 45 ) 46 47 func TestRollWriter(t *testing.T) { 48 49 // empty file name. 50 t.Run("empty_log_name", func(t *testing.T) { 51 logDir := t.TempDir() 52 _, err := NewRollWriter("") 53 assert.Error(t, err, "NewRollWriter: invalid log path") 54 55 // print log file list. 56 printLogFiles(logDir) 57 }) 58 59 // no rolling. 60 t.Run("roll_by_default", func(t *testing.T) { 61 logDir := t.TempDir() 62 logName := "test.log" 63 w, err := NewRollWriter(filepath.Join(logDir, logName)) 64 assert.NoError(t, err, "NewRollWriter: create logger ok") 65 log.SetOutput(w) 66 for i := 0; i < testTimes; i++ { 67 log.Printf("this is a test log: %d\n", i) 68 } 69 70 // check number of rolling log files(current files + backup files). 71 time.Sleep(20 * time.Millisecond) 72 logFiles := getLogBackups(logDir, logName) 73 if len(logFiles) != 1 { 74 t.Errorf("Number of log backup files should be 1") 75 } 76 require.Nil(t, w.Close()) 77 78 // print log file list. 79 printLogFiles(logDir) 80 }) 81 82 // roll by size. 83 t.Run("roll_by_size", func(t *testing.T) { 84 logDir := t.TempDir() 85 logName := "test_size.log" 86 const ( 87 maxBackup = 2 88 maxSize = 1 89 maxAge = 1 90 ) 91 w, err := NewRollWriter(filepath.Join(logDir, logName), 92 WithMaxSize(maxSize), 93 WithMaxAge(maxAge), 94 WithMaxBackups(maxBackup), 95 ) 96 assert.NoError(t, err, "NewRollWriter: create logger ok") 97 log.SetOutput(w) 98 for i := 0; i < testTimes; i++ { 99 log.Printf("this is a test log: %d\n", i) 100 } 101 102 w.notify() 103 // check number of rolling log files. 104 var logFiles []os.FileInfo 105 require.Eventuallyf(t, 106 func() bool { 107 logFiles = getLogBackups(logDir, logName) 108 return len(logFiles) == maxBackup+1 109 }, 110 5*time.Second, 111 time.Second, 112 "Number of log files should be %d, current: %d, %+v", 113 maxBackup+1, len(logFiles), func() []string { 114 names := make([]string, 0, len(logFiles)) 115 for _, f := range logFiles { 116 names = append(names, f.Name()) 117 } 118 return names 119 }(), 120 ) 121 122 // check rolling log file size(allow to exceed a little). 123 for _, file := range logFiles { 124 if file.Size() > 1*1024*1024+1024 { 125 t.Errorf("Log file size exceeds max_size") 126 } 127 } 128 require.Nil(t, w.Close()) 129 130 // print log file list. 131 printLogFiles(logDir) 132 }) 133 134 // rolling by time. 135 t.Run("roll_by_time", func(t *testing.T) { 136 logDir := t.TempDir() 137 logName := "test_time.log" 138 const ( 139 maxBackup = 3 140 maxSize = 1 141 maxAge = 1 142 ) 143 w, err := NewRollWriter(filepath.Join(logDir, logName), 144 WithRotationTime(".%Y%m%d"), 145 WithMaxSize(maxSize), 146 WithMaxAge(maxAge), 147 WithMaxBackups(maxBackup), 148 WithCompress(true), 149 ) 150 assert.NoError(t, err, "NewRollWriter: create logger ok") 151 log.SetOutput(w) 152 for i := 0; i < testTimes; i++ { 153 log.Printf("this is a test log: %d\n", i) 154 } 155 156 w.notify() 157 // check number of rolling log files. 158 var logFiles []os.FileInfo 159 require.Eventuallyf(t, 160 func() bool { 161 logFiles = getLogBackups(logDir, logName) 162 return len(logFiles) == maxBackup+1 163 }, 164 5*time.Second, 165 time.Second, 166 "Number of log files should be %d, current: %d, %+v", 167 maxBackup+1, len(logFiles), func() []string { 168 names := make([]string, 0, len(logFiles)) 169 for _, f := range logFiles { 170 names = append(names, f.Name()) 171 } 172 return names 173 }(), 174 ) 175 176 // check rolling log file size(allow to exceed a little). 177 for _, file := range logFiles { 178 if file.Size() > 1*1024*1024+1024 { 179 t.Errorf("Log file size exceeds max_size") 180 } 181 } 182 183 // check number of compressed files. 184 compressFileNum := 0 185 for _, file := range logFiles { 186 if strings.HasSuffix(file.Name(), compressSuffix) { 187 compressFileNum++ 188 } 189 } 190 if compressFileNum != 3 { 191 t.Errorf("Number of compress log files should be 3") 192 } 193 require.Nil(t, w.Close()) 194 195 // print log file list. 196 printLogFiles(logDir) 197 }) 198 } 199 200 func TestAsyncRollWriter(t *testing.T) { 201 logDir := t.TempDir() 202 const flushThreshold = 4 * 1024 203 204 // no rolling(asynchronous mod) 205 t.Run("roll_by_default_async", func(t *testing.T) { 206 logName := "test.log" 207 w, err := NewRollWriter(filepath.Join(logDir, logName)) 208 assert.NoError(t, err, "NewRollWriter: create logger ok") 209 210 asyncWriter := NewAsyncRollWriter(w, WithLogQueueSize(10), WithWriteLogSize(1024), 211 WithWriteLogInterval(100), WithDropLog(true)) 212 log.SetOutput(asyncWriter) 213 for i := 0; i < testTimes; i++ { 214 log.Printf("this is a test log: %d\n", i) 215 } 216 217 // check number of rolling log files. 218 time.Sleep(20 * time.Millisecond) 219 logFiles := getLogBackups(logDir, logName) 220 if len(logFiles) != 1 { 221 t.Errorf("Number of log backup files should be 1") 222 } 223 require.Nil(t, asyncWriter.Close()) 224 }) 225 226 // rolling by size(asynchronous mod) 227 t.Run("roll_by_size_async", func(t *testing.T) { 228 logName := "test_size.log" 229 w, err := NewRollWriter(filepath.Join(logDir, logName), 230 WithMaxSize(1), 231 WithMaxAge(1), 232 ) 233 assert.NoError(t, err, "NewRollWriter: create logger ok") 234 235 asyncWriter := NewAsyncRollWriter(w, WithWriteLogSize(flushThreshold)) 236 log.SetOutput(asyncWriter) 237 for i := 0; i < testTimes; i++ { 238 log.Printf("this is a test log: %d\n", i) 239 } 240 241 // check number of rolling log files. 242 time.Sleep(200 * time.Millisecond) 243 logFiles := getLogBackups(logDir, logName) 244 if len(logFiles) != 5 { 245 t.Errorf("Number of log backup files should be 5") 246 } 247 248 // check rolling log file size(asynchronous mod, may exceed 4K at most) 249 for _, file := range logFiles { 250 if file.Size() > 1*1024*1024+flushThreshold*2 { 251 t.Errorf("Log file size exceeds max_size") 252 } 253 } 254 require.Nil(t, asyncWriter.Close()) 255 }) 256 257 // rolling by time(asynchronous mod) 258 t.Run("roll_by_time_async", func(t *testing.T) { 259 logName := "test_time.log" 260 w, err := NewRollWriter(filepath.Join(logDir, logName), 261 WithRotationTime(".%Y%m%d"), 262 WithMaxSize(1), 263 WithMaxAge(1), 264 WithCompress(true), 265 ) 266 assert.NoError(t, err, "NewRollWriter: create logger ok") 267 268 asyncWriter := NewAsyncRollWriter(w, WithWriteLogSize(flushThreshold)) 269 log.SetOutput(asyncWriter) 270 for i := 0; i < testTimes; i++ { 271 log.Printf("this is a test log: %d\n", i) 272 } 273 require.Nil(t, asyncWriter.Sync()) 274 275 // check number of rolling log files. 276 time.Sleep(200 * time.Millisecond) 277 logFiles := getLogBackups(logDir, logName) 278 if len(logFiles) != 5 { 279 t.Errorf("Number of log files should be 5, current: %d", len(logFiles)) 280 } 281 282 // check rolling log file size(asynchronous, may exceed 4K at most) 283 for _, file := range logFiles { 284 if file.Size() > 1*1024*1024+flushThreshold*2 { 285 t.Errorf("Log file size exceeds max_size") 286 } 287 } 288 289 // number of compressed files. 290 compressFileNum := 0 291 for _, file := range logFiles { 292 if strings.HasSuffix(file.Name(), compressSuffix) { 293 compressFileNum++ 294 } 295 } 296 if compressFileNum != 4 { 297 t.Errorf("Number of compress log files should be 4") 298 } 299 require.Nil(t, asyncWriter.Close()) 300 }) 301 302 // wait 1 second. 303 time.Sleep(1 * time.Second) 304 305 // print log file list. 306 printLogFiles(logDir) 307 } 308 309 func TestRollWriterRace(t *testing.T) { 310 logDir := t.TempDir() 311 312 writer, err := NewRollWriter( 313 filepath.Join(logDir, "test.log"), 314 WithRotationTime(".%Y%m%d"), 315 ) 316 require.Nil(t, err) 317 writer.opts.MaxSize = 1 318 319 wg := sync.WaitGroup{} 320 wg.Add(2) 321 go func() { 322 defer wg.Done() 323 for i := 0; i < 100; i++ { 324 writer.Write([]byte(fmt.Sprintf("this is a test log: 1-%d\n", i))) 325 } 326 }() 327 328 go func() { 329 defer wg.Done() 330 for i := 0; i < 100; i++ { 331 writer.Write([]byte(fmt.Sprintf("this is a test log: 2-%d\n", i))) 332 } 333 }() 334 wg.Wait() 335 336 time.Sleep(time.Second) // Wait till all the files are closed. 337 require.Nil(t, writer.Close()) 338 } 339 340 func TestAsyncRollWriterRace(t *testing.T) { 341 logDir := t.TempDir() 342 343 writer, _ := NewRollWriter( 344 filepath.Join(logDir, "test.log"), 345 WithRotationTime(".%Y%m%d"), 346 ) 347 writer.opts.MaxSize = 1 348 w := NewAsyncRollWriter(writer) 349 350 wg := sync.WaitGroup{} 351 wg.Add(2) 352 go func() { 353 defer wg.Done() 354 for i := 0; i < 100; i++ { 355 w.Write([]byte(fmt.Sprintf("this is a test log: 1-%d\n", i))) 356 } 357 }() 358 359 go func() { 360 defer wg.Done() 361 for i := 0; i < 100; i++ { 362 w.Write([]byte(fmt.Sprintf("this is a test log: 2-%d\n", i))) 363 } 364 }() 365 wg.Wait() 366 367 time.Sleep(20 * time.Millisecond) 368 require.Nil(t, w.Close()) 369 } 370 371 func TestAsyncRollWriterSyncTwice(t *testing.T) { 372 w := NewAsyncRollWriter(&noopWriteCloser{}) 373 w.Write([]byte("hello")) 374 require.Nil(t, w.Sync()) 375 require.Nil(t, w.Sync()) 376 require.Nil(t, w.Close()) 377 } 378 379 func TestRollWriterError(t *testing.T) { 380 logDir := t.TempDir() 381 t.Run("reopen file", func(t *testing.T) { 382 r, err := NewRollWriter(path.Join(logDir, "trpc.log")) 383 require.Nil(t, err) 384 r.os = errOS{openFileErr: errAlwaysFail} 385 r.reopenFile() 386 require.Nil(t, r.Close()) 387 }) 388 t.Run("delay close and rename file", func(t *testing.T) { 389 r, err := NewRollWriter(path.Join(logDir, "trpc.log")) 390 require.Nil(t, err) 391 r.os = errOS{renameErr: errAlwaysFail} 392 f, err := os.CreateTemp(logDir, "trpc.log") 393 require.Nil(t, err) 394 r.delayCloseAndRenameFile(&closeAndRenameFile{file: f, rename: path.Join(logDir, "tmp.log")}) 395 time.Sleep(30 * time.Millisecond) 396 require.Nil(t, r.Close()) 397 }) 398 t.Run("match log file", func(t *testing.T) { 399 r, err := NewRollWriter(path.Join(logDir, "trpc.log")) 400 require.Nil(t, err) 401 r.os = errOS{statErr: errAlwaysFail} 402 _, err = r.matchLogFile("trpc.log.20230130", "trpc.log") 403 require.NotNil(t, err) 404 require.Nil(t, r.Close()) 405 }) 406 t.Run("remove files", func(t *testing.T) { 407 r, err := NewRollWriter(path.Join(logDir, "trpc.log")) 408 require.Nil(t, err) 409 r.os = errOS{removeErr: errAlwaysFail} 410 r.removeFiles([]logInfo{{time.Time{}, &noopFileInfo{}}}) 411 require.Nil(t, r.Close()) 412 }) 413 t.Run("compress file", func(t *testing.T) { 414 file := path.Join(logDir, "trpc.log") 415 r, err := NewRollWriter(file) 416 require.Nil(t, err) 417 r.os = errOS{openErr: errAlwaysFail} 418 require.NotNil(t, r.compressFile(file, "")) 419 r.os = errOS{openFileErr: errAlwaysFail} 420 f, err := os.Create(file) 421 require.Nil(t, err) 422 require.Nil(t, f.Close()) 423 require.NotNil(t, r.compressFile(file, "")) 424 require.Nil(t, r.Close()) 425 }) 426 } 427 428 type noopFileInfo struct{} 429 430 func (*noopFileInfo) Name() string { 431 return "trpc.log" 432 } 433 434 func (*noopFileInfo) IsDir() bool { 435 return false 436 } 437 438 func (*noopFileInfo) Type() os.FileMode { 439 return fs.ModeAppend 440 } 441 442 func (*noopFileInfo) Info() (os.FileInfo, error) { 443 return &noopFileInfo{}, nil 444 } 445 446 func (*noopFileInfo) Sys() interface{} { 447 return nil 448 } 449 450 func (*noopFileInfo) Size() int64 { 451 return 0 452 } 453 454 func (*noopFileInfo) Mode() os.FileMode { 455 return os.ModePerm 456 } 457 458 func (*noopFileInfo) ModTime() time.Time { 459 return time.Time{} 460 } 461 462 var errAlwaysFail = errors.New("always fail") 463 464 type errOS struct { 465 openErr error 466 openFileErr error 467 mkdirAllErr error 468 renameErr error 469 statErr error 470 removeErr error 471 } 472 473 func (o errOS) Open(name string) (*os.File, error) { 474 if o.openErr != nil { 475 return nil, o.openErr 476 } 477 return os.Open(name) 478 } 479 480 func (o errOS) OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error) { 481 if o.openFileErr != nil { 482 return nil, o.openFileErr 483 } 484 return os.OpenFile(name, flag, perm) 485 } 486 487 func (o errOS) MkdirAll(path string, perm fs.FileMode) error { 488 if o.mkdirAllErr != nil { 489 return o.mkdirAllErr 490 } 491 return os.MkdirAll(path, perm) 492 } 493 494 func (o errOS) Rename(oldpath string, newpath string) error { 495 if o.renameErr != nil { 496 return o.renameErr 497 } 498 return os.Rename(oldpath, newpath) 499 } 500 501 func (o errOS) Stat(name string) (fs.FileInfo, error) { 502 if o.statErr != nil { 503 return nil, o.statErr 504 } 505 return os.Stat(name) 506 } 507 508 func (o errOS) Remove(name string) error { 509 if o.removeErr != nil { 510 return o.removeErr 511 } 512 return os.Remove(name) 513 } 514 515 type noopWriteCloser struct{} 516 517 func (*noopWriteCloser) Write(p []byte) (n int, err error) { return } 518 519 func (*noopWriteCloser) Close() (err error) { return } 520 521 // BenchmarkRollWriterBySize benchmarks RollWriter by size. 522 func BenchmarkRollWriterBySize(b *testing.B) { 523 logDir := b.TempDir() 524 525 // init RollWriter. 526 writer, _ := NewRollWriter(filepath.Join(logDir, "test.log")) 527 core := zapcore.NewCore( 528 zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()), 529 zapcore.AddSync(writer), 530 zapcore.DebugLevel, 531 ) 532 logger := zap.New( 533 core, 534 zap.AddCaller(), 535 ) 536 537 // warm up. 538 for i := 0; i < testTimes; i++ { 539 logger.Debug(fmt.Sprint("this is a test log: ", i)) 540 } 541 542 b.SetParallelism(testRoutines / runtime.GOMAXPROCS(0)) 543 b.ResetTimer() 544 b.RunParallel(func(pb *testing.PB) { 545 for pb.Next() { 546 logger.Debug("this is a test log") 547 } 548 }) 549 } 550 551 // BenchmarkRollWriterByTime benchmarks RollWriter by time. 552 func BenchmarkRollWriterByTime(b *testing.B) { 553 logDir := b.TempDir() 554 555 // init RollWriter. 556 writer, _ := NewRollWriter( 557 filepath.Join(logDir, "test.log"), 558 WithRotationTime(".%Y%m%d"), 559 ) 560 core := zapcore.NewCore( 561 zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()), 562 zapcore.AddSync(writer), 563 zapcore.DebugLevel, 564 ) 565 logger := zap.New( 566 core, 567 zap.AddCaller(), 568 ) 569 570 // warm up. 571 for i := 0; i < testTimes; i++ { 572 logger.Debug(fmt.Sprint("this is a test log: ", i)) 573 } 574 575 b.SetParallelism(testRoutines / runtime.GOMAXPROCS(0)) 576 b.ResetTimer() 577 b.RunParallel(func(pb *testing.PB) { 578 for pb.Next() { 579 logger.Debug("this is a test log") 580 } 581 }) 582 } 583 584 // BenchmarkAsyncRollWriterBySize benchmarks asynchronous RollWriter. 585 func BenchmarkAsyncRollWriterBySize(b *testing.B) { 586 logDir := b.TempDir() 587 588 // init RollWriter. 589 writer, _ := NewRollWriter( 590 filepath.Join(logDir, "test.log"), 591 ) 592 asyncWriter := NewAsyncRollWriter(writer) 593 core := zapcore.NewCore( 594 zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()), 595 asyncWriter, 596 zapcore.DebugLevel, 597 ) 598 logger := zap.New( 599 core, 600 zap.AddCaller(), 601 ) 602 603 // warm up. 604 for i := 0; i < testTimes; i++ { 605 logger.Debug(fmt.Sprint("this is a test log: ", i)) 606 } 607 608 b.SetParallelism(testRoutines / runtime.GOMAXPROCS(0)) 609 b.ResetTimer() 610 b.RunParallel(func(pb *testing.PB) { 611 for pb.Next() { 612 logger.Debug("this is a test log") 613 } 614 }) 615 } 616 617 // BenchmarkAsyncRollWriterByTime benchmarks asynchronous RollWriter by time. 618 func BenchmarkAsyncRollWriterByTime(b *testing.B) { 619 logDir := b.TempDir() 620 621 // init RollWriter. 622 writer, _ := NewRollWriter( 623 filepath.Join(logDir, "test.log"), 624 WithRotationTime(".%Y%m%d"), 625 ) 626 asyncWriter := NewAsyncRollWriter(writer) 627 core := zapcore.NewCore( 628 zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()), 629 asyncWriter, 630 zapcore.DebugLevel, 631 ) 632 logger := zap.New( 633 core, 634 zap.AddCaller(), 635 ) 636 637 // warm up. 638 for i := 0; i < testTimes; i++ { 639 logger.Debug(fmt.Sprint("this is a test log: ", i)) 640 } 641 642 b.SetParallelism(testRoutines / runtime.GOMAXPROCS(0)) 643 b.ResetTimer() 644 b.RunParallel(func(pb *testing.PB) { 645 for pb.Next() { 646 logger.Debug("this is a test log") 647 } 648 }) 649 } 650 651 func printLogFiles(logDir string) { 652 fmt.Println("================================================") 653 fmt.Printf("[%s]:\n", logDir) 654 655 entries, err := os.ReadDir(logDir) 656 if err != nil { 657 fmt.Println("get entries failed ", err) 658 } 659 for _, entry := range entries { 660 info, err := entry.Info() 661 if err != nil { 662 fmt.Println("get info failed ", err) 663 continue 664 } 665 fmt.Println("\t", info.Name(), info.Size()) 666 } 667 } 668 669 func execCommand(name string, args ...string) string { 670 fmt.Println(name, args) 671 cmd := exec.Command(name, args...) 672 output, err := cmd.Output() 673 if err != nil { 674 fmt.Printf("exec command failed, err: %v\n", err) 675 } 676 fmt.Println(string(output)) 677 678 return string(output) 679 } 680 681 func getLogBackups(logDir, prefix string) []os.FileInfo { 682 entries, err := os.ReadDir(logDir) 683 if err != nil { 684 return nil 685 } 686 687 var logFiles []os.FileInfo 688 for _, file := range entries { 689 if !strings.HasPrefix(file.Name(), prefix) { 690 continue 691 } 692 if info, err := file.Info(); err == nil { 693 logFiles = append(logFiles, info) 694 } 695 } 696 return logFiles 697 }