REPO_POLICIES.md, .golangci.yml, the CI workflow and the scripts the policy keeps identical across repositories are copies of the files at that commit. .gitignore, .editorconfig and the new .dockerignore are the canonical files followed by this repository's own entries. The lint stage moves to the golangci-lint image the policy pins, in the same commit as .golangci.yml. The format check leaves the lint stage and runs on the host from script/check, which script/cibuild now runs after script/bootstrap. The last Dockerfile stage runs script/bootstrap, which installs goimports with go install at a pinned commit; script/fmt runs that goimports. Model: opus-5-5
simplelog
Summary
simplelog is an opinionated logging package designed to facilitate easy and structured logging in Go applications with an absolute minimum of boilerplate.
The idea is that you can add a single import line which replaces the
stdlib log/slog default handler, and solve the 90% case for logging.
Current Status
Released v1.0.0 2024-06-14. Works as intended. No known bugs.
Features
- if output is a tty, outputs pretty color logs
- if output is not a tty, outputs json
- supports delivering each log message via a webhook
- emits every
slogattribute: those passed to a log call, those accumulated withWithAttrs, and those qualified byWithGroup.slog.Groupvalues nest, andslog.LogValuervalues are resolved. In json output attributes are object fields (groups become nested objects); in console output they are appended askey=valuepairs, with grouped keys written asgroup.key=value. See Attribute output for the details worth knowing - reports a record that could not be delivered as an error from the
handler's
Handlemethod. See When delivery fails for how to receive it
Planned Features
- supports delivering logs via tcp RELP (e.g. to remote rsyslog using imrelp)
Installation
To use simplelog, first ensure your project is set up with Go modules:
go mod init your_project_name
Then, add SimpleLog to your project:
go get sneak.berlin/go/simplelog
Usage
Below is an example of how to use SimpleLog in a Go application. This
example is provided in the form of a main.go file, which demonstrates
logging at various levels using structured logging syntax.
package main
import (
"log/slog"
_ "sneak.berlin/go/simplelog"
)
func main() {
// log structured data with slog as usual:
slog.Info("User login attempt", slog.String("user", "JohnDoe"), slog.Int("attempt", 3))
slog.Warn("Configuration mismatch", slog.String("expected", "config.json"), slog.String("found", "config.dev.json"))
slog.Error("Failed to save data", slog.String("reason", "permission denied"))
}
Attribute output
Attributes reach every handler: the ones passed to the log call, the ones
accumulated with WithAttrs, and the ones qualified by the groups open at
the time they were attached. slog.Group values nest, and
slog.LogValuer values are resolved to the value they stand for. A few
behaviours are worth knowing before you rely on them.
Your values are never modified. Whatever you log is read and rendered,
never written to. A map or a slice you pass to slog.Any comes back from
the logger exactly as you handed it over, even when a group later uses the
same key.
Durations are nanoseconds in json, and readable on the console. The
json and webhook payloads emit a slog.Duration as a number of
nanoseconds, matching slog.NewJSONHandler, so a consumer can compare and
aggregate the field without parsing it. The console line emits the same
duration as 3s, matching slog.NewTextHandler, because a person reads
it.
The json payload is an object, with the consequences an object has.
The record's own fields are named Time, Level, Message and PC, and
they own those names: an attribute keyed after one of them is dropped from
the json and webhook output. A key logged more than once keeps its last
value there for the same reason, with one exception: when both are
slog.Group values sharing a key, the two groups are merged into one
object holding the members of both, rather than the second replacing the
first. Neither the collision nor the merge applies to the console output,
which is a line of text: every pair appears, in order. If you need a field
called message, pick a key that does not collide - the collision is
silent.
Empty things follow the slog.Handler contract. An empty Attr is
ignored, an empty group is elided along with its key, a group with an
empty key is inlined into its parent, and WithGroup("") returns the
handler unchanged.
When delivery fails
Every handler returns an error from Handle when it could not deliver
the record:
ConsoleHandlerandJSONHandlerreturn the error from their write to stdout, wrapped, soerrors.Isstill matches the originalWebhookHandlerreturns an error when the request fails, and also when the server answers with a status outside 2xx, a redirect included, since it does not follow redirects. A request that has not finished after 5 seconds fails with a timeout error, so a webhook server that never answers holds up a log call for 5 seconds at mostMultiplexHandler, which simplelog installs as the default, passes the record to every handler it holds even after one of them fails, then returns all their errors joined witherrors.Join(nil if none failed)
slog.Info, slog.Error and the other slog.Logger methods throw that
error away. That is how log/slog works, and simplelog cannot change it.
To find out whether a record was delivered, build a slog.Record and pass
it to the handler yourself, for example
slog.Default().Handler().Handle(ctx, record), then check the error it
returns. Console output takes the file and line from the record's PC,
which you set with runtime.Callers as the log/slog documentation
shows. A record whose PC is zero prints ???:0 instead.
Entrypoints
This repository adheres to the
Scripts to Rule Them All
standard: normalized scripts in script/ are the entrypoints for the
development workflow, and the Makefile targets are thin shims that call them.
The scripts are POSIX sh (not bash) so they run in minimal containers such as
alpine. We provide:
script/bootstrap— install all dependencies (go if missing, goimports withgo installat a pinned commit unless the installed one already reports the pinned version, thengo mod download); golangci-lint is not installed, since it runs only in Dockerscript/setup— set up the repo for development after a fresh clone: runsscript/bootstrap, thenscript/install-precommitscript/projectname— output the project name (our own extension); used byscript/dockerfor the image tagscript/test— run the test suite under the race detector in Docker by building only theteststage of theDockerfile, without the build cache, so every run tests; the image is taggedsimplelog-testscript/lint— run golangci-lint in Docker by building only thelintstage of theDockerfile, without the build cache, so every run lints; the image is taggedsimplelog-lintscript/fmt— format all files (writes) with the goimports thatscript/bootstrapinstallsscript/fmt-check— check formatting (read-only); fails ifgofmt -lreports filesscript/check— run all checks:test,lint,fmt-check(our own extension)script/docker— build the Docker image without the build cache, tagged viascript/projectname(byte-identical across repos); it also passes the output ofgit describeas theVERSIONbuild argument, which is ignored because thisDockerfiledoes not declare itscript/cibuild— what CI runs:script/bootstrap, thenscript/check, then the same image build asscript/docker(byte-identical across repos)script/precommit— run by the git pre-commit hook (our own extension); runs ago mod tidyguard, thenscript/checkscript/install-precommit— installs the git pre-commit hook (our own extension);make hooksshims to it
make hooks installs the pre-commit hook that runs script/precommit.