sneak 430bd76230
All checks were successful
check / check (push) Successful in 31s
check / check (pull_request) Successful in 29s
Emit slog attributes from every handler (closes #19)
The handlers took attributes and dropped them on the floor. Handle never
read record.Attrs, so the inline slog.Info("casting", "device", d) form
lost its fields; WithAttrs returned the receiver unchanged, so anything
attached to a derived logger vanished; and WithGroup did the same, so
grouping silently did nothing. The JSON and webhook handlers marshaled
the slog.Record value directly, which cannot work: a record keeps its
attributes in unexported fields, so encoding/json only ever saw Time,
Message, Level and PC.

The consequence was perverse. Converting log.Printf("[%s] Casting %s",
device, file) into structured attributes, as the Go styleguide asks,
left the output with strictly less information than before, and the
calling code reviewed as correct because it was correct.

A small shared attribute layer now holds the accumulated attributes and
open groups. It is copy-on-write, so two loggers derived from one parent
cannot leak attributes into each other, and it qualifies attributes by
the groups open at the time they were attached, per the slog.Handler
contract. Rendering follows each handler's format: the JSON and webhook
handlers emit attributes as object fields with groups as nested objects,
merging a group named twice rather than duplicating its key; the console
handler appends key=value pairs, groups flattened to dotted keys, with
key and value each quoted where leaving it bare would be ambiguous. The
key is quoted as one token, prefix included, so the "=" that separates
the pair is always the first one outside quotes: a key of "a=b" reads as
"a=b"=v rather than as a=b=v, which parses as the key "a" holding the
value "b=v". That is what slog.NewTextHandler does, and the console
rendering was compared against it key by key.

Values the caller logged go into the json payload by reference, because
rendering only reads them, which leaves the group merge as the one place
a value already in the payload is written to. It merges only into the
unexported groupMap type this package allocates for its own groups: a
caller's map[string]any is a different type and can never satisfy that
type assertion, so it is replaced rather than written into. The
invariant holds by construction - nothing reachable from the caller is
modified by logging it.

Values are resolved through slog.Value.Resolve, so LogValuer values are
reported as the value they stand for instead of as a struct, and errors
are reported as their message rather than as the empty object
encoding/json makes of them. Anything encoding/json cannot marshal falls
back to its slog string form rather than rendering as an empty object.

A duration is nanoseconds as a number in the json and webhook payloads,
matching slog.NewJSONHandler, so a consumer can compare and aggregate
the field without parsing it first. The console line keeps the readable
"3s" form, matching slog.NewTextHandler, because a person reads that
one.

The record's own fields keep the names they have always had - Time,
Level, Message, PC - and win a collision with an attribute key, so
existing consumers of the json output see no change beyond the added
fields. That, and a repeated key keeping its last value - except where
both are groups, which merge - are the two ways an attribute can go
missing from the json output; both are now written down in the README,
merge exception included, rather than left to be discovered.

All three handlers are covered, including WebhookHandler, which had the
same defect and is reached through the same MultiplexHandler.
2026-08-10 13:22:05 +00:00
2024-06-14 05:39:08 -07:00
2024-06-10 02:39:01 -07:00
2024-06-14 05:39:03 -07:00
fmt
2024-06-14 05:47:29 -07:00
2024-05-22 14:52:20 -07:00
2024-05-13 21:43:47 -07:00
2024-05-22 14:52:20 -07:00
2024-06-14 05:53:22 -07:00
2024-05-13 21:43:47 -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

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.

License

WTFPL

Description
sane logging defaults for go
Readme WTFPL 186 KiB
Languages
Go 89.6%
Dockerfile 4.6%
Makefile 3.8%
Shell 2%