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  }