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.

Gwendolyn James @gwynforthewyn