fortio.org/log@v1.12.2/logger_test.go (about) 1 // Copyright 2017-2023 Fortio 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 // import "fortio.org/fortio/log" 16 17 import ( 18 "bufio" 19 "bytes" 20 "encoding/json" 21 "errors" 22 "flag" 23 "fmt" 24 "log" 25 "os" 26 "os/exec" 27 "strconv" 28 "strings" 29 "sync" 30 "testing" 31 "time" 32 33 "fortio.org/log/goroutine" 34 ) 35 36 const thisFilename = "logger_test.go" 37 38 // leave this test first/where it is as it relies on line number not changing. 39 func TestLoggerFilenameLine(t *testing.T) { 40 SetLogLevel(Debug) // make sure it's already debug when we capture 41 on := true 42 Config.LogFileAndLine = on 43 Config.LogPrefix = "-prefix-" 44 Config.JSON = false 45 var b bytes.Buffer 46 w := bufio.NewWriter(&b) 47 SetOutput(w) 48 SetFlags(0) 49 SetLogLevel(Debug) 50 if LogDebug() { 51 Debugf("test") // line 51 52 } 53 SetLogLevel(-1) // line 53 54 SetLogLevel(Warning) // line 54 55 Infof("should not show (info level)") 56 Printf("Should show despite being Info - unconditional Printf without line/file") 57 w.Flush() 58 actual := b.String() 59 expected := "[D] logger_test.go:51-prefix-test\n" + 60 "[E] logger_test.go:53-prefix-SetLogLevel called with level -1 lower than Debug!\n" + 61 "[I] logger_test.go:54-prefix-Log level is now 3 Warning (was 0 Debug)\n" + 62 "Should show despite being Info - unconditional Printf without line/file\n" 63 if actual != expected { 64 t.Errorf("unexpected:\n%s\nvs:\n%s\n", actual, expected) 65 } 66 } 67 68 // leave this test second/where it is as it relies on line number not changing. 69 func TestLoggerFilenameLineJSON(t *testing.T) { 70 SetLogLevel(Debug) // make sure it's already debug when we capture 71 on := true 72 Config.LogFileAndLine = on 73 Config.LogPrefix = "-not used-" 74 Config.JSON = true 75 Config.NoTimestamp = true 76 var b bytes.Buffer 77 w := bufio.NewWriter(&b) 78 SetOutput(w) 79 SetLogLevel(Debug) 80 if LogDebug() { 81 Debugf("a test") // line 81 82 } 83 w.Flush() 84 actual := b.String() 85 grID := goroutine.ID() 86 if grID <= 0 { 87 t.Errorf("unexpected goroutine id %d", grID) 88 } 89 expected := `{"level":"dbug","r":` + strconv.FormatInt(grID, 10) + 90 `,"file":"` + thisFilename + `","line":81,"msg":"a test"}` + "\n" 91 if actual != expected { 92 t.Errorf("unexpected:\n%s\nvs:\n%s\n", actual, expected) 93 } 94 } 95 96 func Test_LogS_JSON_no_json_with_filename(t *testing.T) { 97 // Setup 98 var b bytes.Buffer 99 w := bufio.NewWriter(&b) 100 SetLogLevel(LevelByName("Warning")) 101 Config.LogFileAndLine = true 102 Config.JSON = false 103 Config.NoTimestamp = false 104 Config.LogPrefix = "-bar-" 105 log.SetFlags(0) 106 SetOutput(w) 107 // Start of the actual test 108 S(Verbose, "This won't show") 109 S(Warning, "This will show", Str("key1", "value 1"), Attr("key2", 42)) // line 109 110 Printf("This will show too") // no filename/line and shows despite level 111 _ = w.Flush() 112 actual := b.String() 113 expected := "[W] logger_test.go:109-bar-This will show, key1=\"value 1\", key2=42\n" + 114 "This will show too\n" 115 if actual != expected { 116 t.Errorf("got %q expected %q", actual, expected) 117 } 118 } 119 120 func TestColorMode(t *testing.T) { 121 if ConsoleLogging() { 122 t.Errorf("expected not to be console logging") 123 } 124 if Color { 125 t.Errorf("expected to not be in color mode initially") 126 } 127 // Setup 128 Config = DefaultConfig() 129 Config.ForceColor = true 130 Config.NoTimestamp = true 131 Config.LogPrefix = "" // test it'll be at least one space 132 SetLogLevelQuiet(Info) 133 var b bytes.Buffer 134 w := bufio.NewWriter(&b) 135 SetOutput(w) // will call SetColorMode() 136 if !Color { 137 t.Errorf("expected to be in color mode after ForceColor=true and SetColorMode()") 138 } 139 S(Warning, "With file and line", String("attr", "value with space")) // line 139 140 Infof("info with file and line = %v", Config.LogFileAndLine) // line 140 141 Config.LogFileAndLine = false 142 Config.GoroutineID = false 143 S(Warning, "Without file and line", Str("attr", "value with space")) 144 Infof("info with file and line = %v", Config.LogFileAndLine) 145 _ = w.Flush() 146 actual := b.String() 147 grID := fmt.Sprintf("r%d ", goroutine.ID()) 148 expected := "\x1b[37m" + grID + "\x1b[90m[\x1b[33mWRN\x1b[90m] logger_test.go:139 " + 149 "\x1b[33mWith file and line\x1b[0m, \x1b[34mattr\x1b[0m=\x1b[33m\"value with space\"\x1b[0m\n" + 150 "\x1b[37m" + grID + "\x1b[90m[\x1b[32mINF\x1b[90m] logger_test.go:140 \x1b[32minfo with file and line = true\x1b[0m\n" + 151 "\x1b[90m[\x1b[33mWRN\x1b[90m] \x1b[33mWithout file and line\x1b[0m, \x1b[34mattr\x1b[0m=\x1b[33m\"value with space\"\x1b[0m\n" + 152 "\x1b[90m[\x1b[32mINF\x1b[90m] \x1b[32minfo with file and line = false\x1b[0m\n" 153 if actual != expected { 154 t.Errorf("got:\n%s\nexpected:\n%s", actual, expected) 155 } 156 // See color timestamp 157 Config.NoTimestamp = false 158 cs := colorTimestamp() 159 if cs == "" { 160 t.Errorf("got empty color timestamp") 161 } 162 if Colors.Green == "" { 163 t.Errorf("expected to have green color not empty when in color mode") 164 } 165 prevGreen := Colors.Green 166 // turn off color mode 167 Config.ForceColor = false 168 SetColorMode() 169 if Color { 170 t.Errorf("expected to not be in color mode after SetColorMode() and forcecolor false") 171 } 172 if Colors.Green != "" { 173 t.Errorf("expected to have green color empty when not color mode, got %q", Colors.Green) 174 } 175 if LevelToColor[Info] != "" { 176 t.Errorf("expected LevelToColor to be empty when not color mode, got %q", LevelToColor[Info]) 177 } 178 // Show one can mutate/change/tweak colors 179 customColor := "foo" 180 ANSIColors.Green = customColor 181 Config.ForceColor = true 182 SetColorMode() 183 if Colors.Green != customColor { 184 t.Errorf("expected to have color customized, got %q", Colors.Green) 185 } 186 if LevelToColor[Info] != customColor { 187 t.Errorf("expected LevelToColor to the custom foo, got %q", LevelToColor[Info]) 188 } 189 // put it back to real green for other tests 190 ANSIColors.Green = prevGreen 191 // Reset for other/further tests 192 Config.ForceColor = false 193 SetColorMode() 194 } 195 196 func TestSetLevel(t *testing.T) { 197 _ = SetLogLevel(Info) 198 err := SetLogLevelStr("debug") 199 if err != nil { 200 t.Errorf("unexpected error for valid level %v", err) 201 } 202 prev := SetLogLevel(Info) 203 if prev != Debug { 204 t.Errorf("unexpected level after setting debug %v", prev) 205 } 206 err = SetLogLevelStr("bogus") 207 if err == nil { 208 t.Errorf("Didn't get an error setting bogus level") 209 } 210 } 211 212 func TestLogger1(t *testing.T) { 213 // Setup 214 var b bytes.Buffer 215 w := bufio.NewWriter(&b) 216 SetLogLevel(Info) // reset from other tests 217 Config.LogFileAndLine = false 218 Config.LogPrefix = "" 219 Config.JSON = false 220 SetOutput(w) 221 log.SetFlags(0) 222 // Start of the actual test 223 SetLogLevel(LevelByName("Verbose")) 224 expected := "[I] Log level is now 1 Verbose (was 2 Info)\n" 225 i := 0 226 if LogVerbose() { 227 LogVf("test Va %d", i) // Should show 228 } 229 i++ 230 expected += "[V] test Va 0\n" 231 Warnf("test Wa %d", i) // Should show 232 i++ 233 expected += "[W] test Wa 1\n" 234 Logger().Printf("test Logger().Printf %d", i) 235 i++ 236 expected += "[I] test Logger().Printf 2\n" 237 SetLogLevelQuiet(Debug) // no additional logging about level change 238 prevLevel := SetLogLevel(LevelByName("error")) // works with lowercase too 239 expected += "[I] Log level is now 4 Error (was 0 Debug)\n" 240 LogVf("test Vb %d", i) // Should not show 241 Infof("test info when level is error %d", i) // Should not show 242 i++ 243 Warnf("test Wb %d", i) // Should not show 244 i++ 245 Errf("test E %d", i) // Should show 246 i++ 247 expected += "[E] test E 5\n" 248 // test the rest of the api 249 Logf(LevelByName("Critical"), "test %d level str %s, cur %s", i, prevLevel.String(), GetLogLevel().String()) 250 expected += "[C] test 6 level str Debug, cur Error\n" 251 i++ 252 SetLogLevel(Debug) // should be fine and invisible change 253 SetLogLevel(Debug - 1) 254 expected += "[E] SetLogLevel called with level -1 lower than Debug!\n" 255 SetLogLevel(Fatal) // Hiding critical level is not allowed 256 expected += "[E] SetLogLevel called with level 6 higher than Critical!\n" 257 SetLogLevel(Critical) // should be fine 258 expected += "[I] Log level is now 5 Critical (was 0 Debug)\n" 259 Critf("testing crit %d", i) // should show 260 expected += "[C] testing crit 7\n" 261 Printf("Printf should always show n=%d", 8) 262 expected += "Printf should always show n=8\n" 263 r := FErrf("FErrf should always show but not exit, n=%d", 9) 264 expected += "[F] FErrf should always show but not exit, n=9\n" 265 if r != 1 { 266 t.Errorf("FErrf returned %d instead of 1", r) 267 } 268 _ = w.Flush() 269 actual := b.String() 270 if actual != expected { 271 t.Errorf("unexpected:\n%s\nvs:\n%s\n", actual, expected) 272 } 273 } 274 275 func TestLoggerJSON(t *testing.T) { 276 // Setup 277 var b bytes.Buffer 278 w := bufio.NewWriter(&b) 279 SetLogLevel(LevelByName("Verbose")) 280 Config.LogFileAndLine = true 281 Config.LogPrefix = "not used" 282 Config.JSON = true 283 Config.NoTimestamp = false 284 SetOutput(w) 285 // Start of the actual test 286 now := time.Now() 287 if LogVerbose() { 288 LogVf("Test Verbose %d", 0) // Should show 289 } 290 _ = w.Flush() 291 actual := b.String() 292 e := JSONEntry{} 293 err := json.Unmarshal([]byte(actual), &e) 294 t.Logf("got: %s -> %#v", actual, e) 295 if err != nil { 296 t.Errorf("unexpected JSON deserialization error %v for %q", err, actual) 297 } 298 if e.Level != "trace" { 299 t.Errorf("unexpected level %s", e.Level) 300 } 301 if e.Msg != "Test Verbose 0" { 302 t.Errorf("unexpected body %s", e.Msg) 303 } 304 if e.File != thisFilename { 305 t.Errorf("unexpected file %q", e.File) 306 } 307 if e.Line < 270 || e.Line > 310 { 308 t.Errorf("unexpected line %d", e.Line) 309 } 310 ts := e.Time() 311 now = microsecondResolution(now) // truncates so can't be after ts 312 if now.After(ts) { 313 t.Errorf("unexpected time %v is after %v", now, ts) 314 } 315 if ts.Sub(now) > 100*time.Millisecond { 316 t.Errorf("unexpected time %v is > 1sec after %v", ts, now) 317 } 318 } 319 320 func Test_LogS_JSON(t *testing.T) { 321 // Setup 322 var b bytes.Buffer 323 w := bufio.NewWriter(&b) 324 SetLogLevel(LevelByName("Verbose")) 325 Config.LogFileAndLine = true 326 Config.JSON = true 327 Config.NoTimestamp = false 328 SetOutput(w) 329 // Start of the actual test 330 now := time.Now() 331 value2 := 42 332 value3 := 3.14 333 S(Verbose, "Test Verbose", Str("key1", "value 1"), Int("key2", value2), Float64("key3", value3)) 334 _ = w.Flush() 335 actual := b.String() 336 e := JSONEntry{} 337 err := json.Unmarshal([]byte(actual), &e) 338 t.Logf("got: %s -> %#v", actual, e) 339 if err != nil { 340 t.Errorf("unexpected JSON deserialization error %v for %q", err, actual) 341 } 342 if e.Level != "trace" { 343 t.Errorf("unexpected level %s", e.Level) 344 } 345 if e.Msg != "Test Verbose" { 346 t.Errorf("unexpected body %s", e.Msg) 347 } 348 if e.File != thisFilename { 349 t.Errorf("unexpected file %q", e.File) 350 } 351 if e.Line < 270 || e.Line > 340 { 352 t.Errorf("unexpected line %d", e.Line) 353 } 354 ts := e.Time() 355 now = microsecondResolution(now) // truncates so can't be after ts 356 if now.After(ts) { 357 t.Errorf("unexpected time %v is after %v", now, ts) 358 } 359 if ts.Sub(now) > 100*time.Millisecond { 360 t.Errorf("unexpected time %v is > 1sec after %v", ts, now) 361 } 362 // check extra attributes 363 var tmp map[string]interface{} 364 err = json.Unmarshal([]byte(actual), &tmp) 365 if err != nil { 366 t.Errorf("unexpected JSON deserialization 2 error %v for %q", err, actual) 367 } 368 if tmp["key1"] != "value 1" { 369 t.Errorf("unexpected key1 %v", tmp["key1"]) 370 } 371 if tmp["key2"] != float64(42) { 372 t.Errorf("unexpected key2 %v", tmp["key2"]) 373 } 374 if tmp["key3"] != 3.14 { // comparing floats with == is dicey but... this passes... 375 t.Errorf("unexpected key3 %v", tmp["key3"]) 376 } 377 if tmp["file"] != thisFilename { 378 t.Errorf("unexpected file %v", tmp["file"]) 379 } 380 } 381 382 func Test_LogS_JSON_no_file(t *testing.T) { 383 // Setup 384 var b bytes.Buffer 385 w := bufio.NewWriter(&b) 386 SetLogLevel(LevelByName("Warning")) 387 Config.LogFileAndLine = false 388 Config.JSON = true 389 Config.NoTimestamp = false 390 SetOutput(w) 391 // Start of the actual test 392 S(Verbose, "This won't show") 393 S(Warning, "This will show", Attr("key1", "value 1")) 394 _ = w.Flush() 395 actual := b.String() 396 var tmp map[string]interface{} 397 err := json.Unmarshal([]byte(actual), &tmp) 398 if err != nil { 399 t.Errorf("unexpected JSON deserialization error %v for %q", err, actual) 400 } 401 if tmp["key1"] != "value 1" { 402 t.Errorf("unexpected key1 %v", tmp["key1"]) 403 } 404 if tmp["file"] != nil { 405 t.Errorf("unexpected file %v", tmp["file"]) 406 } 407 } 408 409 func Test_LogS_JSON_no_json_no_file(t *testing.T) { 410 // Setup 411 var b bytes.Buffer 412 w := bufio.NewWriter(&b) 413 SetLogLevel(LevelByName("Warning")) 414 Config.LogFileAndLine = false 415 Config.JSON = false 416 Config.NoTimestamp = false 417 Config.LogPrefix = "-foo-" 418 log.SetFlags(0) 419 SetOutput(w) 420 // Start of the actual test 421 S(Verbose, "This won't show") 422 S(Warning, "This will show", Str("key1", "value 1"), Attr("key2", 42)) 423 S(NoLevel, "This NoLevel will show despite logically info level") 424 _ = w.Flush() 425 actual := b.String() 426 expected := "[W]-foo-This will show, key1=\"value 1\", key2=42\n" + 427 "This NoLevel will show despite logically info level\n" 428 if actual != expected { 429 t.Errorf("---got:---\n%s\n---expected:---\n%s\n", actual, expected) 430 } 431 } 432 433 func TestLoggerJSONNoTimestampNoFilename(t *testing.T) { 434 // Setup 435 var b bytes.Buffer 436 w := bufio.NewWriter(&b) 437 SetLogLevel(LevelByName("Verbose")) 438 Config.LogFileAndLine = false 439 Config.LogPrefix = "no used" 440 Config.JSON = true 441 Config.NoTimestamp = true 442 SetOutput(w) 443 // Start of the actual test 444 Critf("Test Critf") 445 _ = w.Flush() 446 actual := b.String() 447 e := JSONEntry{} 448 err := json.Unmarshal([]byte(actual), &e) 449 t.Logf("got: %s -> %#v", actual, e) 450 if err != nil { 451 t.Errorf("unexpected JSON deserialization error %v for %q", err, actual) 452 } 453 if e.Level != "crit" { 454 t.Errorf("unexpected level %s", e.Level) 455 } 456 if e.Msg != "Test Critf" { 457 t.Errorf("unexpected body %s", e.Msg) 458 } 459 if e.File != "" { 460 t.Errorf("unexpected file %q", e.File) 461 } 462 if e.Line != 0 { 463 t.Errorf("unexpected line %d", e.Line) 464 } 465 if e.TS != 0 { 466 t.Errorf("unexpected time should be absent, got %v %v", e.TS, e.Time()) 467 } 468 } 469 470 func TestLoggerSimpleJSON(t *testing.T) { 471 // Setup 472 var b bytes.Buffer 473 w := bufio.NewWriter(&b) 474 SetLogLevel(LevelByName("Verbose")) 475 Config.LogFileAndLine = false 476 Config.LogPrefix = "no used" 477 Config.JSON = true 478 Config.NoTimestamp = false 479 SetOutput(w) 480 // Start of the actual test 481 w.WriteString("[") 482 Critf("Test Critf2") 483 w.WriteString(",") 484 S(Critical, "Test Critf3") 485 w.WriteString("]") 486 _ = w.Flush() 487 actual := b.String() 488 e := []JSONEntry{} 489 err := json.Unmarshal([]byte(actual), &e) 490 t.Logf("got: %s -> %#v", actual, e) 491 if err != nil { 492 t.Errorf("unexpected JSON deserialization error %v for %q", err, actual) 493 } 494 if len(e) != 2 { 495 t.Errorf("unexpected number of entries %d", len(e)) 496 } 497 for i := 0; i < 2; i++ { 498 e := e[i] 499 if e.Level != "crit" { 500 t.Errorf("unexpected level %s", e.Level) 501 } 502 exp := fmt.Sprintf("Test Critf%d", i+2) 503 if e.Msg != exp { 504 t.Errorf("unexpected body %s", e.Msg) 505 } 506 if e.File != "" { 507 t.Errorf("unexpected file %q", e.File) 508 } 509 if e.Line != 0 { 510 t.Errorf("unexpected line %d", e.Line) 511 } 512 if e.TS == 0 { 513 t.Errorf("unexpected 0 time should have been present") 514 } 515 } 516 } 517 518 // Test that TimeToTs and Time() are inverse of one another. 519 func TestTimeToTs(t *testing.T) { 520 var prev float64 521 // tight loop to get different times, at highest resolution 522 for i := 0; i < 100000; i++ { 523 now := time.Now() 524 // now = now.Add(69 * time.Nanosecond) 525 usecTSstr := timeToTStr(now) 526 usecTS, _ := strconv.ParseFloat(usecTSstr, 64) 527 if i != 0 && usecTS < prev { 528 t.Fatalf("clock went back in time at iter %d %v vs %v", i, usecTS, prev) 529 } 530 prev = usecTS 531 e := JSONEntry{TS: usecTS} 532 inv := e.Time() 533 // Round to microsecond because that's the resolution of the timestamp 534 // (note that on a mac for instance, there is no nanosecond resolution anyway) 535 if !microsecondResolution(now).Equal(inv) { 536 t.Fatalf("[at %d] unexpected time %v -> %v != %v (%v %v)", i, now, microsecondResolution(now), inv, usecTS, usecTSstr) 537 } 538 } 539 } 540 541 func microsecondResolution(t time.Time) time.Time { 542 // Truncate and not Round because that's what UnixMicro does (indirectly). 543 return t.Truncate(1 * time.Microsecond) 544 } 545 546 // concurrency test, make sure json aren't mixed up. 547 func TestLoggerJSONConcurrency(t *testing.T) { 548 // Setup 549 var b bytes.Buffer 550 w := bufio.NewWriter(&b) 551 SetLogLevel(LevelByName("Verbose")) 552 Config.LogFileAndLine = true 553 Config.NoTimestamp = true 554 Config.JSON = true 555 SetOutput(w) 556 // Start of the actual test 557 var wg sync.WaitGroup 558 wg.Add(10) 559 for i := 0; i < 10; i++ { 560 go func(i int) { 561 for j := 0; j < 100; j++ { 562 Infof("Test from %d: %d", i, j) 563 } 564 wg.Done() 565 }(i) 566 } 567 wg.Wait() 568 _ = w.Flush() 569 actual := b.String() 570 t.Logf("got: %s", actual) 571 // Check it all deserializes to JSON correctly and we get the expected number of lines 572 count := 0 573 for _, line := range strings.Split(actual, "\n") { 574 if count == 1000 && line == "" { 575 // last line is empty 576 continue 577 } 578 count++ 579 e := JSONEntry{} 580 err := json.Unmarshal([]byte(line), &e) 581 if err != nil { 582 t.Errorf("unexpected JSON deserialization error on line %d %v for %q", count, err, line) 583 } 584 } 585 if count != 1000 { 586 t.Errorf("unexpected number of lines %d", count) 587 } 588 } 589 590 func TestLogFatal(t *testing.T) { 591 defer func() { 592 if r := recover(); r == nil { 593 t.Errorf("expected a panic from log.Fatalf, didn't get one") 594 } 595 }() 596 Fatalf("test of log fatal") 597 } 598 599 func TestLoggerFatalCliMode(t *testing.T) { 600 SetDefaultsForClientTools() 601 if os.Getenv("DO_LOG_FATALF") == "1" { 602 Fatalf("test") 603 Errf("should have exited / this shouldn't have been reached") 604 return // will cause exit status 0 if reached and thus fail the test 605 } 606 // unfortunately, even if passing -test.coverprofile it doesn't get counted 607 cmd := exec.Command(os.Args[0], "-test.run=TestLoggerFatalCliMode") 608 cmd.Stderr = os.Stderr 609 cmd.Stdout = os.Stdout 610 cmd.Env = append(os.Environ(), "DO_LOG_FATALF=1") 611 err := cmd.Run() 612 var e *exec.ExitError 613 if ok := errors.As(err, &e); ok && e.ExitCode() == 1 { 614 Printf("Got expected exit status 1") 615 return 616 } 617 t.Fatalf("process ran with err %v, want exit status 1", err) 618 } 619 620 func TestLoggerFatalExitOverride(t *testing.T) { 621 SetDefaultsForClientTools() 622 exitCalled := false 623 Config.FatalExit = func(_ int) { 624 exitCalled = true 625 } 626 Fatalf("testing log.Fatalf exit case") 627 if !exitCalled { 628 t.Error("expected exit function override not called") 629 } 630 } 631 632 func TestMultipleFlags(t *testing.T) { 633 SetLogLevelQuiet(Verbose) 634 // use x... so it's sorted after the standard loglevel for package level 635 // print default tests were all 3 flags are present. 636 LoggerStaticFlagSetup("xllvl1", "xllvl2") 637 f := flag.Lookup("loglevel") 638 if f != nil { 639 t.Error("expected default loglevel to not be registered") 640 } 641 f = flag.Lookup("xllvl1") 642 if f.Value.String() != "Verbose" { 643 t.Errorf("expected flag default value to be Verbose, got %s", f.Value.String()) 644 } 645 if err := f.Value.Set(" iNFo\n"); err != nil { 646 t.Errorf("expected flag to be settable, got %v", err) 647 } 648 f2 := flag.Lookup("xllvl2") 649 if f2.Value.String() != "Info" { 650 t.Errorf("expected linked flag value to be Info, got %s", f2.Value.String()) 651 } 652 if GetLogLevel() != Info { 653 t.Errorf("expected log level to be Info, got %s", GetLogLevel().String()) 654 } 655 if err := f2.Value.Set("debug"); err != nil { 656 t.Errorf("expected flag2 to be settable, got %v", err) 657 } 658 if GetLogLevel() != Debug { 659 t.Errorf("expected log level to be Debug, got %s", GetLogLevel().String()) 660 } 661 } 662 663 func TestStaticFlagDefault(t *testing.T) { 664 SetLogLevelQuiet(Warning) 665 LoggerStaticFlagSetup() 666 var b bytes.Buffer 667 flag.CommandLine.SetOutput(&b) 668 flag.CommandLine.PrintDefaults() 669 s := b.String() 670 expected := " -loglevel level\n" + 671 " \tlog level, one of [Debug Verbose Info Warning Error Critical Fatal] " + 672 "(default Warning)\n" 673 if !strings.HasPrefix(s, expected) { 674 t.Errorf("expected flag output to start with %q, got %q", expected, s) 675 } 676 f := flag.Lookup("loglevel") 677 if f == nil { 678 t.Fatal("expected flag to be registered") 679 } 680 if f.Value.String() != "Warning" { 681 t.Errorf("expected flag default value to be Warning, got %s", f.Value.String()) 682 } 683 if err := f.Value.Set("badlevel"); err == nil { 684 t.Error("expected error passing a bad level value, didn't get one") 685 } 686 if err := f.Value.Set(" iNFo\n"); err != nil { 687 t.Errorf("expected flag to be settable, got %v", err) 688 } 689 if GetLogLevel() != Info { 690 t.Errorf("expected log level to be Info, got %s", GetLogLevel().String()) 691 } 692 } 693 694 func TestTimeToTS(t *testing.T) { 695 // test a few times and expected string 696 for _, tst := range []struct { 697 sec int64 698 nano int64 699 result string 700 }{ 701 {1688763601, 42000, "1688763601.000042"}, // 42 usec after the seconds part, checking for leading zeroes 702 {1688763601, 199999999, "1688763601.199999"}, // nanosec are truncated away not rounded (see note in TimeToTS) 703 {1688763601, 200000999, "1688763601.200000"}, // boundary 704 {1689983019, 142600000, "1689983019.142600"}, // trailing zeroes 705 } { 706 tm := time.Unix(tst.sec, tst.nano) 707 ts := timeToTStr(tm) 708 if ts != tst.result { 709 t.Errorf("unexpected ts for %d, %d -> %q instead of %q (%v)", tst.sec, tst.nano, ts, tst.result, tm) 710 } 711 } 712 } 713 714 func TestJSONLevelReverse(t *testing.T) { 715 str := LevelToJSON[Warning] 716 if str != `"warn"` { 717 t.Errorf("unexpected JSON level string %q (should have quotes)", str) 718 } 719 lvl := JSONStringLevelToLevel["warn"] 720 if lvl != Warning { 721 t.Errorf("unexpected level %d", lvl) 722 } 723 lvl = JSONStringLevelToLevel["info"] // Should be info and not NoLevel (7) 724 if lvl != Info { 725 t.Errorf("unexpected level %d", lvl) 726 } 727 lvl = JSONStringLevelToLevel["fatal"] // Should be info and not NoLevel (7) 728 if lvl != Fatal { 729 t.Errorf("unexpected level %d", lvl) 730 } 731 } 732 733 func TestNoLevel(t *testing.T) { 734 Config.ForceColor = true 735 SetColorMode() 736 color := ColorLevelToStr(NoLevel) 737 if color != ANSIColors.DarkGray { 738 t.Errorf("unexpected color %q", color) 739 } 740 Config.ForceColor = false 741 Config.JSON = true 742 Config.ConsoleColor = false 743 Config.NoTimestamp = true 744 Config.GoroutineID = false 745 var buf bytes.Buffer 746 SetOutput(&buf) 747 Printf("test") 748 actual := buf.String() 749 expected := `{"level":"info","msg":"test"}` + "\n" 750 if actual != expected { 751 t.Errorf("unexpected:\n%s\nvs:\n%s\n", actual, expected) 752 } 753 } 754 755 type customError struct { 756 Msg string 757 Code int 758 } 759 760 type customErrorAlias customError 761 762 func (e customError) Error() string { 763 return fmt.Sprintf("custom error %s (code %d)", e.Msg, e.Code) 764 } 765 766 func (e customError) MarshalJSON() ([]byte, error) { 767 return json.Marshal(customErrorAlias(e)) 768 } 769 770 func TestSerializationOfError(t *testing.T) { 771 var err error 772 kv := Any("err", err) 773 kvStr := kv.StringValue() 774 expected := `null` 775 if kvStr != expected { 776 t.Errorf("unexpected:\n%s\nvs:\n%s\n", kvStr, expected) 777 } 778 err = errors.New("test error") 779 Errf("Error on purpose: %v", err) 780 S(Error, "Error on purpose", Any("err", err)) 781 kv = Any("err", err) 782 kvStr = kv.StringValue() 783 expected = `"test error"` 784 if kvStr != expected { 785 t.Errorf("unexpected:\n%s\nvs:\n%s\n", kvStr, expected) 786 } 787 err = customError{Msg: "custom error", Code: 42} 788 kv = Any("err", err) 789 kvStr = kv.StringValue() 790 expected = `{"Msg":"custom error","Code":42}` 791 if kvStr != expected { 792 t.Errorf("unexpected:\n%s\nvs:\n%s\n", kvStr, expected) 793 } 794 } 795 796 func TestToJSON_MarshalError(t *testing.T) { 797 badValue := make(chan int) 798 799 expected := fmt.Sprintf("\"ERR marshaling %v: %v\"", badValue, "json: unsupported type: chan int") 800 actual := toJSON(badValue) 801 802 if actual != expected { 803 t.Errorf("Expected %q, got %q", expected, actual) 804 } 805 } 806 807 func TestEnvHelp(t *testing.T) { 808 SetDefaultsForClientTools() 809 Config.NoTimestamp = false 810 // Setup 811 var b bytes.Buffer 812 w := bufio.NewWriter(&b) 813 EnvHelp(w) 814 w.Flush() 815 actual := b.String() 816 expected := `# Logger environment variables: 817 LOGGER_LOG_PREFIX=' ' 818 LOGGER_LOG_FILE_AND_LINE=false 819 LOGGER_FATAL_PANICS=false 820 LOGGER_JSON=false 821 LOGGER_NO_TIMESTAMP=false 822 LOGGER_CONSOLE_COLOR=true 823 LOGGER_FORCE_COLOR=false 824 LOGGER_GOROUTINE_ID=false 825 LOGGER_COMBINE_REQUEST_AND_RESPONSE=false 826 LOGGER_LEVEL='Info' 827 ` 828 if actual != expected { 829 t.Errorf("unexpected:\n%s\nvs:\n%s\n", actual, expected) 830 } 831 } 832 833 func TestConfigFromEnvError(t *testing.T) { 834 t.Setenv("LOGGER_LEVEL", "foo") 835 var buf bytes.Buffer 836 SetOutput(&buf) 837 configFromEnv() 838 actual := buf.String() 839 expected := "Invalid log level from environment" 840 if !strings.Contains(actual, expected) { 841 t.Errorf("unexpected:\n%s\nvs:\n%s\n", actual, expected) 842 } 843 } 844 845 func TestConfigFromEnvOk(t *testing.T) { 846 t.Setenv("LOGGER_LEVEL", "verbose") 847 var buf bytes.Buffer 848 SetOutput(&buf) 849 configFromEnv() 850 actual := buf.String() 851 expected := "Log level set from environment LOGGER_LEVEL to Verbose" 852 if !strings.Contains(actual, expected) { 853 t.Errorf("unexpected:\n%s\nvs:\n%s\n", actual, expected) 854 } 855 } 856 857 // io.Discard but specially known to by logger optimizations for instance. 858 type discard struct{} 859 860 func (discard) Write(p []byte) (int, error) { 861 return len(p), nil 862 } 863 864 func (discard) WriteString(s string) (int, error) { 865 return len(s), nil 866 } 867 868 var Discard = discard{} 869 870 func BenchmarkLogDirect1(b *testing.B) { 871 setLevel(Error) 872 for n := 0; n < b.N; n++ { 873 Debugf("foo bar %d", n) 874 } 875 } 876 877 func BenchmarkLogDirect2(b *testing.B) { 878 setLevel(Error) 879 for n := 0; n < b.N; n++ { 880 Logf(Debug, "foo bar %d", n) 881 } 882 } 883 884 func BenchmarkMultipleStrNoLog(b *testing.B) { 885 setLevel(Error) 886 for n := 0; n < b.N; n++ { 887 S(Debug, "foo bar", Str("a", "aval"), Str("b", "bval"), Str("c", "cval"), Str("d", "dval")) 888 } 889 } 890 891 func BenchmarkLogSnologNotOptimized1(b *testing.B) { 892 for n := 0; n < b.N; n++ { 893 // Avoid optimization for n < 256 that skews memory number (and combined with truncation gives 0 instead of 1) 894 // https://github.com/golang/go/blob/go1.21.0/src/runtime/iface.go#L493 895 S(Debug, "foo bar", Attr("n1", 12345+n)) 896 } 897 } 898 899 func BenchmarkLogSnologNotOptimized4(b *testing.B) { 900 for n := 0; n < b.N; n++ { 901 v := n + 12345 902 S(Debug, "foo bar", Attr("n1", v), Attr("n2", v+1), Attr("n3", v+2), Attr("n4", v+3)) 903 } 904 } 905 906 func BenchmarkLogSnologOptimized(b *testing.B) { 907 setLevel(Error) 908 v := ValueType[int]{0} 909 aa := KeyVal{Key: "n", Value: &v} 910 ba := Str("b", "bval") 911 for n := 0; n < b.N; n++ { 912 v.Val = n + 1235 913 S(Debug, "foo bar", aa, ba) 914 } 915 } 916 917 func BenchmarkLogS_NotOptimized(b *testing.B) { 918 setLevel(Info) 919 Config.JSON = true 920 Config.LogFileAndLine = false 921 Config.ConsoleColor = false 922 Config.ForceColor = false 923 SetOutput(Discard) 924 for n := 0; n < b.N; n++ { 925 S(Info, "foo bar", Attr("n", n)) 926 } 927 } 928 929 func BenchmarkLog_Optimized(b *testing.B) { 930 setLevel(Info) 931 Config.JSON = true 932 Config.LogFileAndLine = false 933 Config.ConsoleColor = false 934 Config.ForceColor = false 935 SetOutput(Discard) 936 v := ValueType[int]{0} 937 a := KeyVal{Key: "n", Value: &v} 938 for n := 0; n < b.N; n++ { 939 v.Val = n 940 S(Info, "foo bar", a) 941 } 942 } 943 944 func BenchmarkLogOldStyle(b *testing.B) { 945 setLevel(Info) 946 Config.JSON = false 947 Config.LogFileAndLine = false 948 Config.ConsoleColor = false 949 Config.ForceColor = false 950 SetColorMode() 951 SetOutput(Discard) 952 for n := 0; n < b.N; n++ { 953 S(Info, "foo bar", Attr("n", n)) 954 } 955 }