feat(logger): add custom console formatter for JSON and multiline strings

This commit is contained in:
afjcjsbx 2026-03-12 18:58:24 +01:00
parent d18a319b0c
commit a01af36af4

View file

@ -5,6 +5,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv"
"strings" "strings"
"sync" "sync"
@ -45,6 +46,9 @@ func init() {
consoleWriter := zerolog.ConsoleWriter{ consoleWriter := zerolog.ConsoleWriter{
Out: os.Stdout, Out: os.Stdout,
TimeFormat: "15:04:05", // TODO: make it configurable??? TimeFormat: "15:04:05", // TODO: make it configurable???
// Custom formatter to handle multiline strings and JSON objects
FormatFieldValue: formatFieldValue,
} }
logger = zerolog.New(consoleWriter).With().Timestamp().Logger() logger = zerolog.New(consoleWriter).With().Timestamp().Logger()
@ -52,6 +56,37 @@ func init() {
}) })
} }
func formatFieldValue(i any) string {
var s string
switch val := i.(type) {
case string:
s = val
case []byte:
s = string(val)
default:
return fmt.Sprintf("%v", i)
}
if unquoted, err := strconv.Unquote(s); err == nil {
s = unquoted
}
if strings.Contains(s, "\n") {
return fmt.Sprintf("\n%s", s)
}
if strings.Contains(s, " ") {
if (strings.HasPrefix(s, "{") && strings.HasSuffix(s, "}")) ||
(strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]")) {
return s
}
return fmt.Sprintf("%q", s)
}
return s
}
func SetLevel(level LogLevel) { func SetLevel(level LogLevel) {
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
@ -162,10 +197,7 @@ func logMessage(level LogLevel, component string, message string, fields map[str
event.Str("caller", fmt.Sprintf("<none> %s:%d (%s)", callerFile, callerLine, callerFunc)) event.Str("caller", fmt.Sprintf("<none> %s:%d (%s)", callerFile, callerLine, callerFunc))
} }
for k, v := range fields { appendFields(event, fields)
event.Interface(k, v)
}
event.Msg(message) event.Msg(message)
// Also log to file if enabled // Also log to file if enabled
@ -175,9 +207,8 @@ func logMessage(level LogLevel, component string, message string, fields map[str
if component != "" { if component != "" {
fileEvent.Str("component", component) fileEvent.Str("component", component)
} }
for k, v := range fields {
fileEvent.Interface(k, v) appendFields(event, fields)
}
fileEvent.Msg(message) fileEvent.Msg(message)
} }
@ -186,6 +217,26 @@ func logMessage(level LogLevel, component string, message string, fields map[str
} }
} }
func appendFields(event *zerolog.Event, fields map[string]any) {
for k, v := range fields {
// Type switch to avoid double JSON serialization of strings
switch val := v.(type) {
case string:
event.Str(k, val)
case int:
event.Int(k, val)
case int64:
event.Int64(k, val)
case float64:
event.Float64(k, val)
case bool:
event.Bool(k, val)
default:
event.Interface(k, v) // Fallback for struct, slice and maps
}
}
}
func Debug(message string) { func Debug(message string) {
logMessage(DEBUG, "", message, nil) logMessage(DEBUG, "", message, nil)
} }