How Does Charm Logger Format Logs?
I woke up today wondering how charm’s logger library (https://github.com/charmbracelet/log) actually handles log levels. I know they have some levels predefined in an enum, but what’re they doing with that? This is relevant to #golang #opensource #programming #readingcode
logger.Log is the natural entrypoint, as it’s the actual code doing the logging https://github.com/charmbracelet/log/blob/1e6353e3ca793f1177148e09f990ef220e19b037/logger.go#L56
It dispatches to logger.handle, which has this curious setup:
func (l *Logger) handle(level Level, ts time.Time, frames []runtime.Frame, msg interface{}, keyvals ...interface{}) {
var kvs []interface{}
if l.reportTimestamp && !ts.IsZero() {
kvs = append(kvs, TimestampKey, ts)
}
if level != noLevel {
kvs = append(kvs, LevelKey, level)
}
I think this means that no matter your log level, whether the log will print or not, the logger always constructs a log line for you, unless you match the noLevel?
As a note, noLevel is private, but is defined as math.MaxInt32
. This probably means the most efficient way to generate no logs is to declare your own equivalent noLog
level as a default, but I can come back to that in a few paragraphs.
So how do the logs get written? Scanning to the end of the function, it’s this straightforwards l.b.WriteTo(l.w)
. l is a pointer to the logger struct, b is a bytes.buffer inside that logger, and it’s written out to whatever destination is stored in an io.writer called w
.
So if we can understand how b
get populated, we’ve cracked how these logs are written. The answer is in this switch statement, though it’s indirect:
switch l.formatter {
case LogfmtFormatter:
l.logfmtFormatter(kvs...)
case JSONFormatter:
l.jsonFormatter(kvs...)
default:
l.textFormatter(kvs...)
Each of these formatters accepts responsibility for populating the buffer with an appropriately formatted log output. The logfmtFormatter, for example, begins with e := logfmt.NewEncoder(&l.b)
, accepting a reference to the buffer.
So, that’s the story. Logs are appended to a key value store, which is handed to a formatter that writes formatted output to a buffer on the logger, which is then written.
I’ll come back to the question of figuring out if the best performing no-opt logger is setting hte logLevel to math.MaxInt32
in my next post about this.