clawbot 9710d3f05f
check / check (push) Successful in 30s
check / check (pull_request) Successful in 22s
Take the console location from the record (closes #36)
ConsoleHandler found the file and line it prints by walking a fixed
number of stack frames up from itself, which only fit a record logged
through a slog.Logger and delivered through MultiplexHandler. It now
reads them from the record's PC, the call site slog stores, so a
ConsoleHandler used with slog.New and a record passed to Handle
directly print the right location too. A record whose PC is zero
prints ???:0 as before. The README drops its warning about the wrong
location and says how to set PC instead.

Model: opus-5-5
2026-10-06 12:02:43 +00:00
2024-06-14 05:39:03 -07:00
2024-05-22 14:52:20 -07:00
2024-05-13 21:43:47 -07:00
2024-06-14 05:53:22 -07:00
2024-05-13 21:43:47 -07:00

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 slog attribute: those passed to a log call, those accumulated with WithAttrs, and those qualified by WithGroup. slog.Group values nest, and slog.LogValuer values are resolved. In json output attributes are object fields (groups become nested objects); in console output they are appended as key=value pairs, with grouped keys written as group.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 Handle method. 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:

  • ConsoleHandler and JSONHandler return the error from their write to stdout, wrapped, so errors.Is still matches the original
  • WebhookHandler returns 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
  • MultiplexHandler, 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 with errors.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, then go mod download); golangci-lint is not installed, since it runs only in Docker
  • script/setup — set up the repo for development after a fresh clone: runs script/bootstrap, then script/install-precommit
  • script/projectname — output the project name (our own extension); used by script/docker for the image tag
  • script/test — run the test suite under the race detector in Docker by building only the test stage of the Dockerfile, without the build cache, so every run tests; the image is tagged simplelog-test
  • script/lint — run golangci-lint in Docker by building only the lint stage of the Dockerfile (which also runs the format check), without the build cache, so every run lints; the image is tagged simplelog-lint
  • script/fmt — format all files with goimports (writes)
  • script/fmt-check — check formatting (read-only); fails if gofmt -l reports files
  • script/check — run all checks: test, lint, fmt-check (our own extension)
  • script/docker — build the Docker image without the build cache, tagged via script/projectname (byte-identical across repos); it also passes the output of git describe as the VERSION build argument, which is ignored because this Dockerfile does not declare it
  • script/cibuild — cd to the repo root and build the Docker image without the build cache, tagged via script/projectname (what CI runs; the image build runs the checks)
  • script/precommit — run by the git pre-commit hook (our own extension); runs a go mod tidy guard, then script/check
  • script/install-precommit — installs the git pre-commit hook (our own extension); make hooks shims to it

make hooks installs the pre-commit hook that runs script/precommit.

License

WTFPL

S
Description
sane logging defaults for go
Readme WTFPL
262 KiB
2026-10-02 01:59:50 +02:00
Languages
Go 97.2%
Dockerfile 1.3%
Makefile 1%
Shell 0.5%