Go library that provides utilities for structured logging, building on the standard
log/slog
package. It provides two independent packages:
devlog
implements aslog.Handler
with a human-readable output format, designed for local development and CLI toolsdevlog/log
is a thin wrapper over the logging API oflog/slog
, providing:- Utility functions for log message formatting (
log.Infof
,log.Errorf
, etc.) - Error-aware logging functions, which structure errors to be formatted consistently as log attributes
log.AddContextAttrs
, a function for adding log attributes to acontext.Context
, applying the attributes to all logs made in that context
- Utility functions for log message formatting (
Run go get hermannm.dev/devlog
to add it to your project!
Docs: pkg.go.dev/hermannm.dev/devlog
Contents:
devlog.Handler
implements slog.Handler
, so it can handle
output for slog
's logging functions. It can be configured as follows:
import (
"log/slog"
"hermannm.dev/devlog"
)
func main() {
logHandler := devlog.NewHandler(os.Stdout, nil)
slog.SetDefault(slog.New(logHandler))
}
Logging with slog
will now use this handler. So the following log:
slog.Info("Server started", "port", 8000, "environment", "DEV")
...will give the following output (using a gruvbox terminal color scheme):
Structs, slices and other non-primitive types are encoded as pretty-formatted JSON, so this example:
type Event struct {
ID int `json:"id"`
Type string `json:"type"`
}
event := Event{ID: 1000, Type: "ORDER_UPDATED"}
slog.Error("Failed to process event", "event", event)
...gives this output:
devlog
's output is meant to be easily read by a developer working locally. However, you may want a
more structured format for production systems, to make log analysis easier. You can get both by
conditionally choosing the log handler for your application, like this:
var logHandler slog.Handler
switch os.Getenv("ENVIRONMENT") {
case "LOCAL", "TEST":
// Pretty-formatted logs for local development and tests
logHandler = devlog.NewHandler(os.Stdout, nil)
default:
// Structured JSON logs for deployed environments
logHandler = slog.NewJSONHandler(os.Stdout, nil)
}
slog.SetDefault(slog.New(logHandler))
Unlike log/slog
, devlog/log
provides logging functions that take an error
. When an error is
passed to such a logging function, it is attached to the log as a cause
attribute, so errors are
structured consistently between logs.
import (
"context"
"errors"
"hermannm.dev/devlog/log"
)
func example(ctx context.Context) {
err := errors.New("database insert failed")
log.Error(ctx, err, "Failed to store event")
}
This gives the following output (using the devlog
output handler):
The package also provides log.AddContextAttrs
, a function for adding log attributes to a
context.Context
. These attributes are added to all logs where the context is passed, so this
example:
func processEvent(ctx context.Context, event Event) {
ctx = log.AddContextAttrs(ctx, "eventId", event.ID)
log.Debug(ctx, "Started processing event")
// ...
log.Debug(ctx, "Finished processing event")
}
...gives this output:
This can help you trace connected logs in your system (especially when using a more structured JSON
output in production, allowing you to filter on all logs with a specific eventId
).
In order to encourage propagating context attributes, all log functions in this package take a
context.Context
. If you're in a function without a context parameter, you may pass a nil
context. But ideally, you should pass a context wherever you do logging, in order to propagate
context attributes.
- Run tests and linter (
golangci-lint
):go test ./... && golangci-lint run
- Add an entry to
CHANGELOG.md
(with the current date)- Remember to update the link section, and bump the version for the
[Unreleased]
link
- Remember to update the link section, and bump the version for the
- Create commit and tag for the release (update
TAG
variable in below command):TAG=vX.Y.Z && git commit -m "Release ${TAG}" && git tag -a "${TAG}" -m "Release ${TAG}" && git log --oneline -2
- Push the commit and tag:
git push && git push --tags
- Our release workflow will then create a GitHub release with the pushed tag's changelog entry
- Jonathan Amsterdam for his fantastic
Guide to Writing
slog
Handlers