A structured logging handler for Go, with a human-readable output format designed for development builds.
Run go get hermannm.dev/devlog
to add it to your project!
devlog.Handler
implements slog.Handler
, so it can handle
output for slog
's logging functions. It can be configured as follows:
logHandler := devlog.NewHandler(os.Stdout, nil)
slog.SetDefault(slog.New(logHandler))
Logging with slog
will now use this handler:
slog.Warn("no value found for 'PORT' in env, defaulting to 8000")
slog.Info("server started", slog.Int("port", 8000), slog.String("environment", "DEV"))
slog.Error(
"database query failed",
slog.Group("dbError", slog.Int("code", 60), slog.String("message", "UNKNOWN_TABLE")),
)
...giving the following output (using a gruvbox terminal color scheme):
This output is meant to be easily read by a developer working locally. However, you may want a more structured format for production, such as JSON, to make log analysis easier. You can get both by conditionally choosing the log handler for your application, e.g.:
var logHandler slog.Handler
switch os.Getenv("ENVIRONMENT") {
case "PROD":
logHandler = slog.NewJSONHandler(os.Stdout, nil)
case "DEV":
logHandler = devlog.NewHandler(os.Stdout, nil)
}
slog.SetDefault(slog.New(logHandler))
To complement devlog
's output handling, the
devlog/log
subpackage provides input handling. It is
a thin wrapper over the slog
package, with utility functions for log message formatting.
Example using devlog
and devlog/log
together:
import (
"errors"
"log/slog"
"os"
"hermannm.dev/devlog"
"hermannm.dev/devlog/log"
)
func main() {
logHandler := devlog.NewHandler(os.Stdout, nil)
slog.SetDefault(slog.New(logHandler))
user := map[string]any{"id": 2, "username": "hermannm"}
err := errors.New("username taken")
log.ErrorCause(err, "failed to create user", log.JSON("user", user))
}
This gives the following output:
- Jonathan Amsterdam for his fantastic
Guide to Writing
slog
Handlers