Format the existing Markdown with prettier
check / check (push) Failing after 4s

The one-time reformat of README.md and TODO.md, produced by make fmt
with nothing else in it. REPO_POLICIES.md already matched prettier's
output and is unchanged.

Model: opus-5-5
This commit is contained in:
2026-10-06 14:04:18 +00:00
parent d827a7e65c
commit 815b57fa0d
2 changed files with 149 additions and 157 deletions
+76 -83
View File
@@ -3,35 +3,33 @@
## Summary
simplelog is an opinionated logging package designed to facilitate easy and
structured logging in Go applications with an absolute minimum of
boilerplate.
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.
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.
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](#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](#when-delivery-fails) for how to receive it
- 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](#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](#when-delivery-fails) for how to
receive it
## Planned Features
- supports delivering logs via tcp RELP (e.g. to remote rsyslog using imrelp)
- supports delivering logs via tcp RELP (e.g. to remote rsyslog using imrelp)
## Installation
@@ -49,9 +47,9 @@ 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.
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.
```go
package main
@@ -73,63 +71,58 @@ func main() {
## 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.
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.
**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.
**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.
**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.
**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:
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. 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 most
- `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)
- `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. 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 most
- `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
`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
@@ -154,23 +147,23 @@ alpine. We provide:
- `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`, without the build cache, so every run lints; the
image is tagged `simplelog-lint`
- `script/lint` — run golangci-lint in Docker by building only the `lint` stage
of the `Dockerfile`, without the build cache, so every run lints; the image is
tagged `simplelog-lint`
- `script/fmt` — format all files (writes): Go with the goimports that
`script/bootstrap` installs, then every `*.md` file with `prettier`
- `script/fmt-check` — check formatting (read-only); fails if `gofmt -l`
reports files, or if `prettier` would change any `*.md` file
- `script/fmt-check` — check formatting (read-only); fails if `gofmt -l` reports
files, or if `prettier` would change any `*.md` file
- `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` — what CI runs: `script/bootstrap`, then `script/check`,
then the same image build as `script/docker` (byte-identical across repos)
- `script/precommit` — run by the git pre-commit hook (our own extension);
runs a `go mod tidy` guard, then `script/check`
- `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` — what CI runs: `script/bootstrap`, then `script/check`, then
the same image build as `script/docker` (byte-identical across repos)
- `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