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.)
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.)
go get github.com/telemachus/humane
// 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")
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
andslog.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 usesslog.TimeKey
as its key value by default.AddSource bool
: This option defaults to false. If you set it to true, then an Attr containingsource=/path/to/source:line
will be added to each record. If a source Attr is present, it usesslog.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))
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.
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.)