check / check (push) Failing after 5s
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. script/fmt runs goimports with go run at a pinned commit. Model: opus-5-5
177 lines
7.4 KiB
Markdown
177 lines
7.4 KiB
Markdown
# 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](#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)
|
|
|
|
## Installation
|
|
|
|
To use simplelog, first ensure your project is set up with Go modules:
|
|
|
|
```bash
|
|
go mod init your_project_name
|
|
```
|
|
|
|
Then, add SimpleLog to your project:
|
|
|
|
```bash
|
|
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.
|
|
|
|
```go
|
|
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](https://github.com/github/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`, without the build cache, so every run lints; the
|
|
image is tagged `simplelog-lint`
|
|
- `script/fmt` — format all files with goimports (writes); goimports runs
|
|
with `go run` at a pinned commit, so nothing needs to install it
|
|
- `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` — 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
|
|
|
|
`make hooks` installs the pre-commit hook that runs `script/precommit`.
|
|
|
|
## License
|
|
|
|
[WTFPL](./LICENSE)
|