A structured log 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", "port", 8000, "environment", "DEV")
slog.Error(
"Database query failed",
slog.Group("dbError", "code", 60, "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"
"os"
"hermannm.dev/devlog"
"hermannm.dev/devlog/log"
)
func main() {
// Shorthand for calling NewHandler with slog.SetDefault
devlog.InitDefaultLogHandler(os.Stdout, nil)
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
slogHandlers

