Skip to content

Commit

Permalink
have a prefix even for tools to avoid the I for Info next to a verb l…
Browse files Browse the repository at this point in the history
…ooking off (#42)

* have a prefix even for tools to avoid the I for Info next to a verb looking like bad english also colorize better

* fix tests and change color output to be more verbose/human readable

* fix linter

* add coverage

* make log.Printf a special virtual level

* adding NoLevel as always logging Info

* Expose ColorLevelToStr so logc can use it

* avoid crash in json mode for printf yet don't reverse info to level 7 (nolevel)

* update screenshot

* fix the JSONStringLevelToLevel map and test boundary
  • Loading branch information
ldemailly authored Aug 2, 2023
1 parent 0f14c24 commit 1095950
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 31 deletions.
Binary file modified color.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions console_logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ var (
Colors.Red,
Colors.Purple,
Colors.BrightRed,
Colors.Green, // NoLevel log.Printf
}
// Used for color version of console logging.
LevelToText = []string{
"Debg",
"Verb",
"Info",
"Warn",
"Err!",
"Crit",
"Fatal",
"",
}
// Cached flag for whether to use color output or not.
Color = false
Expand Down Expand Up @@ -106,6 +118,7 @@ func SetColorMode() {
Colors.Red,
Colors.Purple,
Colors.BrightRed,
Colors.Green, // NoLevel log.Printf
}
}

Expand All @@ -130,3 +143,11 @@ func colorGID() string {
}
return Colors.Gray + fmt.Sprintf("[%d] ", goroutine.ID())
}

// Longer version when colorizing on console of the level text.
func ColorLevelToStr(lvl Level) string {
if lvl == NoLevel {
return Colors.DarkGray
}
return LevelToColor[lvl] + LevelToText[lvl] + Colors.DarkGray
}
2 changes: 1 addition & 1 deletion levelsDemo/levels.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func main() {
// Meat of the example: (some of these are reproducing fixed issues in `logc` json->console attributes detection)
log.Debugf("This is a debug message ending with backslash \\")
log.LogVf("This is a verbose message")
log.Printf("This an always printed, file:line omitted message")
log.Printf("This an always printed, file:line omitted message (and no level in console)")
log.Infof("This is an info message with no attributes but with \"quotes\"...")
log.S(log.Info, "This is multi line\n\tstructured info message with 3 attributes",
log.Str("attr1", "value1"), log.Attr("attr2", 42), log.Str("attr3", "\"quoted\nvalue\""))
Expand Down
58 changes: 40 additions & 18 deletions logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const (
Error
Critical
Fatal
NoLevel
)

//nolint:revive // we keep "Config" for the variable itself.
Expand Down Expand Up @@ -109,6 +110,7 @@ var (
"\"err\"",
"\"crit\"",
"\"fatal\"",
"\"info\"", // For Printf / NoLevel JSON output
}
// Reverse mapping of level string used in JSON to Level. Used by https://github.com/fortio/logc
// to interpret and colorize pre existing JSON logs.
Expand All @@ -120,7 +122,7 @@ var (
// Needs to be called before flag.Parse(). Caller could also use log.Printf instead of changing this
// if not wanting to use levels. Also makes log.Fatalf just exit instead of panic.
func SetDefaultsForClientTools() {
Config.LogPrefix = ""
Config.LogPrefix = "> "
Config.LogFileAndLine = false
Config.FatalPanics = false
Config.ConsoleColor = true
Expand Down Expand Up @@ -159,13 +161,13 @@ func (l *JSONEntry) Time() time.Time {
func init() {
setLevel(Info) // starting value
levelToStrM = make(map[string]Level, 2*len(LevelToStrA))
JSONStringLevelToLevel = make(map[string]Level, len(LevelToJSON))
JSONStringLevelToLevel = make(map[string]Level, len(LevelToJSON)-1) // -1 to not reverse info to NoLevel
for l, name := range LevelToStrA {
// Allow both -loglevel Verbose and -loglevel verbose ...
levelToStrM[name] = Level(l)
levelToStrM[strings.ToLower(name)] = Level(l)
}
for l, name := range LevelToJSON {
for l, name := range LevelToJSON[0 : Fatal+1] { // Skip NoLevel
// strip the quotes around
JSONStringLevelToLevel[name[1:len(name)-1]] = Level(l)
}
Expand Down Expand Up @@ -353,36 +355,46 @@ func logPrintf(lvl Level, format string, rest ...interface{}) {
}

func logUnconditionalf(logFileAndLine bool, lvl Level, format string, rest ...interface{}) {
prefix := Config.LogPrefix
if prefix == "" {
prefix = " "
}
lvl1Char := ""
if lvl == NoLevel {
prefix = ""
} else {
lvl1Char = LevelToStrA[lvl][0:1]
}
if logFileAndLine { //nolint:nestif
_, file, line, _ := runtime.Caller(3)
file = file[strings.LastIndex(file, "/")+1:]
if Color {
jsonWrite(fmt.Sprintf("%s%s%s%s %s:%d%s%s%s\n",
colorTimestamp(), colorGID(), LevelToColor[lvl], LevelToStrA[lvl][0:1],
file, line, Config.LogPrefix, fmt.Sprintf(format, rest...), Colors.Reset))
jsonWrite(fmt.Sprintf("%s%s%s %s:%d%s%s%s%s\n",
colorTimestamp(), colorGID(), ColorLevelToStr(lvl),
file, line, prefix, LevelToColor[lvl], fmt.Sprintf(format, rest...), Colors.Reset))
} else if Config.JSON {
jsonWrite(fmt.Sprintf("{%s\"level\":%s,%s\"file\":%q,\"line\":%d,\"msg\":%q}\n",
jsonTimestamp(), LevelToJSON[lvl], jsonGID(), file, line, fmt.Sprintf(format, rest...)))
} else {
log.Print(LevelToStrA[lvl][0:1], " ", file, ":", line, Config.LogPrefix, fmt.Sprintf(format, rest...))
log.Print(lvl1Char, " ", file, ":", line, prefix, fmt.Sprintf(format, rest...))
}
} else {
if Color {
jsonWrite(fmt.Sprintf("%s%s%s%s %s%s%s\n",
colorTimestamp(), colorGID(), LevelToColor[lvl], LevelToStrA[lvl][0:1], Config.LogPrefix,
jsonWrite(fmt.Sprintf("%s%s%s%s%s%s%s\n",
colorTimestamp(), colorGID(), ColorLevelToStr(lvl), prefix, LevelToColor[lvl],
fmt.Sprintf(format, rest...), Colors.Reset))
} else if Config.JSON {
jsonWrite(fmt.Sprintf("{%s\"level\":%s,%s\"msg\":%q}\n",
jsonTimestamp(), LevelToJSON[lvl], jsonGID(), fmt.Sprintf(format, rest...)))
} else {
log.Print(LevelToStrA[lvl][0:1], " ", Config.LogPrefix, fmt.Sprintf(format, rest...))
log.Print(lvl1Char, prefix, fmt.Sprintf(format, rest...))
}
}
}

// Printf forwards to the underlying go logger to print (with only timestamp prefixing).
func Printf(format string, rest ...interface{}) {
logUnconditionalf(false, Info, format, rest...)
logUnconditionalf(false, NoLevel, format, rest...)
}

// SetOutput sets the output to a different writer (forwards to system logger).
Expand Down Expand Up @@ -539,28 +551,38 @@ func S(lvl Level, msg string, attrs ...KeyVal) {
buf.WriteString(fmt.Sprintf(format, attr.Key, attr.Value.String()))
}
// TODO share code with log.logUnconditionalf yet without extra locks or allocations/buffers?
prefix := Config.LogPrefix
if prefix == "" {
prefix = " "
}
lvl1Char := ""
if lvl == NoLevel {
prefix = ""
} else {
lvl1Char = LevelToStrA[lvl][0:1]
}
if Config.LogFileAndLine { //nolint:nestif
_, file, line, _ := runtime.Caller(1)
file = file[strings.LastIndex(file, "/")+1:]
if Color {
jsonWrite(fmt.Sprintf("%s%s%s%s %s:%d%s%s%s%s\n",
colorTimestamp(), colorGID(), LevelToColor[lvl], LevelToStrA[lvl][0:1],
file, line, Config.LogPrefix, msg, buf.String(), Colors.Reset))
jsonWrite(fmt.Sprintf("%s%s%s %s:%d%s%s%s%s%s\n",
colorTimestamp(), colorGID(), ColorLevelToStr(lvl),
file, line, prefix, LevelToColor[lvl], msg, buf.String(), Colors.Reset))
} else if Config.JSON {
jsonWrite(fmt.Sprintf("{%s\"level\":%s,%s\"file\":%q,\"line\":%d,\"msg\":%q%s}\n",
jsonTimestamp(), LevelToJSON[lvl], jsonGID(), file, line, msg, buf.String()))
} else {
log.Print(LevelToStrA[lvl][0:1], " ", file, ":", line, Config.LogPrefix, msg, buf.String())
log.Print(lvl1Char, " ", file, ":", line, prefix, msg, buf.String())
}
} else {
if Color {
jsonWrite(fmt.Sprintf("%s%s%s%s %s%s%s%s\n",
colorTimestamp(), colorGID(), LevelToColor[lvl], LevelToStrA[lvl][0:1], Config.LogPrefix, msg, buf.String(), Colors.Reset))
jsonWrite(fmt.Sprintf("%s%s%s%s%s%s%s%s\n",
colorTimestamp(), colorGID(), ColorLevelToStr(lvl), prefix, LevelToColor[lvl], msg, buf.String(), Colors.Reset))
} else if Config.JSON {
jsonWrite(fmt.Sprintf("{%s\"level\":%s,\"msg\":%q%s}\n",
jsonTimestamp(), LevelToJSON[lvl], msg, buf.String()))
} else {
log.Print(LevelToStrA[lvl][0:1], " ", Config.LogPrefix, msg, buf.String())
log.Print(lvl1Char, prefix, msg, buf.String())
}
}
}
57 changes: 45 additions & 12 deletions logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func TestLoggerFilenameLine(t *testing.T) {
expected := "D logger_test.go:51-prefix-test\n" +
"E logger_test.go:53-prefix-SetLogLevel called with level -1 lower than Debug!\n" +
"I logger_test.go:54-prefix-Log level is now 3 Warning (was 0 Debug)\n" +
"I -prefix-Should show despite being Info - unconditional Printf without line/file\n"
"Should show despite being Info - unconditional Printf without line/file\n"
if actual != expected {
t.Errorf("unexpected:\n%s\nvs:\n%s\n", actual, expected)
}
Expand Down Expand Up @@ -111,7 +111,7 @@ func Test_LogS_JSON_no_json_with_filename(t *testing.T) {
_ = w.Flush()
actual := b.String()
expected := "W logger_test.go:109-bar-This will show, key1=\"value 1\", key2=\"42\"\n" +
"I -bar-This will show too\n"
"This will show too\n"
if actual != expected {
t.Errorf("got %q expected %q", actual, expected)
}
Expand All @@ -128,27 +128,28 @@ func TestColorMode(t *testing.T) {
Config = DefaultConfig()
Config.ForceColor = true
Config.NoTimestamp = true
Config.LogPrefix = "" // test it'll be at least one space
SetLogLevelQuiet(Info)
var b bytes.Buffer
w := bufio.NewWriter(&b)
SetOutput(w) // will call SetColorMode()
if !Color {
t.Errorf("expected to be in color mode after ForceColor=true and SetColorMode()")
}
S(Warning, "With file and line", Str("attr", "value with space")) // line 138
Infof("info with file and line = %v", Config.LogFileAndLine) // line 139
S(Warning, "With file and line", Str("attr", "value with space")) // line 139
Infof("info with file and line = %v", Config.LogFileAndLine) // line 140
Config.LogFileAndLine = false
Config.GoroutineID = false
S(Warning, "Without file and line", Str("attr", "value with space"))
Infof("info with file and line = %v", Config.LogFileAndLine)
_ = w.Flush()
actual := b.String()
grID := fmt.Sprintf("[%d] ", goroutine.ID())
expected := "\x1b[37m" + grID +
"\x1b[33mW logger_test.go:138> With file and line\x1b[0m, \x1b[34mattr\x1b[0m=\x1b[33m\"value with space\"\x1b[0m\n" +
"\x1b[37m" + grID + "\x1b[32mI logger_test.go:139> info with file and line = true\x1b[0m\n" +
"\x1b[33mW > Without file and line\x1b[0m, \x1b[34mattr\x1b[0m=\x1b[33m\"value with space\"\x1b[0m\n" +
"\x1b[32mI > info with file and line = false\x1b[0m\n"
expected := "\x1b[37m" + grID + "\x1b[33mWarn\x1b[90m logger_test.go:139 " +
"\x1b[33mWith file and line\x1b[0m, \x1b[34mattr\x1b[0m=\x1b[33m\"value with space\"\x1b[0m\n" +
"\x1b[37m" + grID + "\x1b[32mInfo\x1b[90m logger_test.go:140 \x1b[32minfo with file and line = true\x1b[0m\n" +
"\x1b[33mWarn\x1b[90m \x1b[33mWithout file and line\x1b[0m, \x1b[34mattr\x1b[0m=\x1b[33m\"value with space\"\x1b[0m\n" +
"\x1b[32mInfo\x1b[90m \x1b[32minfo with file and line = false\x1b[0m\n"
if actual != expected {
t.Errorf("got:\n%q\nexpected:\n%q", actual, expected)
}
Expand Down Expand Up @@ -258,7 +259,7 @@ func TestLogger1(t *testing.T) {
Critf("testing crit %d", i) // should show
expected += "C testing crit 7\n"
Printf("Printf should always show n=%d", 8)
expected += "I Printf should always show n=8\n"
expected += "Printf should always show n=8\n"
r := FErrf("FErrf should always show but not exit, n=%d", 9)
expected += "F FErrf should always show but not exit, n=9\n"
if r != 1 {
Expand All @@ -277,7 +278,7 @@ func TestLoggerJSON(t *testing.T) {
w := bufio.NewWriter(&b)
SetLogLevel(LevelByName("Verbose"))
Config.LogFileAndLine = true
Config.LogPrefix = "no used"
Config.LogPrefix = "not used"
Config.JSON = true
Config.NoTimestamp = false
SetOutput(w)
Expand Down Expand Up @@ -420,9 +421,11 @@ func Test_LogS_JSON_no_json_no_file(t *testing.T) {
// Start of the actual test
S(Verbose, "This won't show")
S(Warning, "This will show", Str("key1", "value 1"), Attr("key2", 42))
S(NoLevel, "This NoLevel will show despite logically info level")
_ = w.Flush()
actual := b.String()
expected := "W -foo-This will show, key1=\"value 1\", key2=\"42\"\n"
expected := "W-foo-This will show, key1=\"value 1\", key2=\"42\"\n" +
"This NoLevel will show despite logically info level\n"
if actual != expected {
t.Errorf("got %q expected %q", actual, expected)
}
Expand Down Expand Up @@ -670,6 +673,36 @@ func TestJSONLevelReverse(t *testing.T) {
if lvl != Warning {
t.Errorf("unexpected level %d", lvl)
}
lvl = JSONStringLevelToLevel["info"] // Should be info and not NoLevel (7)
if lvl != Info {
t.Errorf("unexpected level %d", lvl)
}
lvl = JSONStringLevelToLevel["fatal"] // Should be info and not NoLevel (7)
if lvl != Fatal {
t.Errorf("unexpected level %d", lvl)
}
}

func TestNoLevel(t *testing.T) {
Config.ForceColor = true
SetColorMode()
color := ColorLevelToStr(NoLevel)
if color != ANSIColors.DarkGray {
t.Errorf("unexpected color %q", color)
}
Config.ForceColor = false
Config.JSON = true
Config.ConsoleColor = false
Config.NoTimestamp = true
Config.GoroutineID = false
var buf bytes.Buffer
SetOutput(&buf)
Printf("test")
actual := buf.String()
expected := `{"level":"info","msg":"test"}` + "\n"
if actual != expected {
t.Errorf("unexpected:\n%s\nvs:\n%s\n", actual, expected)
}
}

// io.Discard but specially known to by logger optimizations for instance.
Expand Down

0 comments on commit 1095950

Please sign in to comment.