Skip to content

telemachus/humane

Repository files navigation

humane: a human-friendly (but still largely structured) slog.Handler

humane provides a slog.Handler for a human-friendly version of logfmt. The idea for this format comes from Brandur Leach's original post about logfmt. See the section Human logfmt and best practices for details. (To be very clear, Brandur Leach wrote that section in 2016, and he has nothing to do with this project. Any bad ideas are entirely my fault, not his.)

The Format

Briefly, the format is as follows.

LEVEL | Message text | [foo=bar...] time="2023-04-02T10:50.09 EDT"

The level and message Attrs appear as is without key=value structure or quoting. Then the rest of the Attrs appear as key=value pairs. A time Attr will be added by default to the third section. (See below for how to change the format of this Attr or omit it entirely.) The three sections of the log line are separated by a pipe character (|). The pipes should make it easy to parse out the sections of the message with (e.g.) cut or awk, but no attempt is made to check for that character anywhere else in the log. Thus, if pipes appear elsewhere, all bets are off. (This seems like a reasonable trade-off to me since the format is meant for humans to scan rather than for other programs to parse. If you want something fully structured, you should probably use JSON or another format.)

Installation

go get github.com/telemachus/humane

Usage

// Create a logger with default options.  See below for more on available
// options.
logger := slog.New(humane.NewHandler(os.Stdout, nil))
logger.Info("My informative message", "foo", "bar", "bizz", "buzz")
logger.Error("Ooops", slog.Any("error", err))
// Output:
//  INFO | My informative message | foo=bar bizz=buzz time="2023-04-02T10:50.09 EDT"
// ERROR | Ooops | error="error message" time="2023-04-02T10:50.09 EDT"

// You can also set options.  Again, see the next section for more details.
opts := &humane.Options{
    Level: slog.LevelError,
    TimeFormat: time.RFC3339
}
logger := slog.New(humane.NewHandler(os.Stderr, opts))
logger.Info("This message will not be written")

Options

  • Level slog.Leveler: Level defaults to slog.Info. You can use a slog.Level to change the default. If you want something more complex, you can also implement a slog.Leveler.
  • ReplaceAttr func(groups []string, a slog.Attr): As in slog itself, this function is applied to each Attr in a given Record during handling. This allows you to, e.g., omit or edit Attrs in creative ways. See slog's documentation and tests for further examples. Note that the ReplaceAttr function is not applied to the level or message Attrs since they receive specific formatting by this handler. (However, I am open to reconsidering that. Please open an issue to discuss it.) In order to make the time and source Attrs easier to test for, they use constants defined by slog for their keys: slog.TimeKey and slog.SourceKey.
  • TimeFormat string: The time format defaults to "2006-01-02T03:04.05 MST". You can use this option to set some other time format. (You can also tweak the time format via a ReplaceAttr function, but setting this option is easier for simple format changes.) The time Attr uses slog.TimeKey as its key value by default.
  • AddSource bool: This option defaults to false. If you set it to true, then an Attr containing source=/path/to/source:line will be added to each record. If a source Attr is present, it uses slog.SourceKey as its default key value.

A common need (e.g., for testing) is to remove the time Attr altogether. Here's a simple way to do that.

func removeTime(_ []string, a slog.Attr) slog.Attr {
    if a.Key == slog.TimeKey {
        return slog.Attr{}
    }
    return a
}
opts := &humane.Options{ReplaceAttr: removeTime}
logger := slog.New(humane.NewHandler(os.Stdout, opts))

Bugs and Limitations

I'm not aware of any bugs yet, but I'm sure there in here. Please let me know if you find any.

One limitation concerns the source Attr. If you use the logger in a helper function or a wrapper, then the source information will likely be wrong. See slog's documentation for a discussion and workaround.

Acknowledgments

I'm using quite a lot of code from slog itself as well as from the slog extras repository. The guide to writing slog handlers was also very useful. Thanks to Jonathan Amsterdam for for all three of these. I've also taken ideas and code from sources on Go's wiki as well as several blog posts about slog. See below for a list of resources. (Note that some of the resources are more or less out of date since slog and its API have changed over time.)