diff --git a/.slog.example.yml b/.slog.example.yml index d01a481..eecd888 100644 --- a/.slog.example.yml +++ b/.slog.example.yml @@ -13,7 +13,7 @@ imports: # default: [] levels: - info: 0 - - alert: 1 + - alert: 12 # the list of keys to generate constants for. # default: [] diff --git a/README.md b/README.md index 9a40f31..3c14a77 100644 --- a/README.md +++ b/README.md @@ -5,39 +5,179 @@ [](https://goreportcard.com/report/go-simpler.org/sloggen) [](https://codecov.io/gh/go-simpler/sloggen) +Generate domain-specific helpers for `log/slog`. + ## 📌 About -When using `log/slog` in a production-grade project, it is useful to write helpers to avoid human error in the keys. +When using `log/slog` in a production-grade project, it is useful to write helpers to prevent typos in the keys: ```go slog.Info("a user has logged in", "user_id", 42) slog.Info("a user has logged out", "user_ip", 42) // oops :( ``` -Depending on your code style, these can be simple constants (if you prefer key-value arguments)... +Depending on your code style, these can be simple constants (if you prefer key-value pairs)... ```go const UserId = "user_id" ``` -...or constructors for `slog.Attr` (if you're a safety/performance advocate). +...or custom `slog.Attr` constructors (if you're a safety/performance advocate): ```go -func UserId(value int) slog.Attr { - return slog.Int("user_id", value) -} +func UserId(value int) slog.Attr { return slog.Int("user_id", value) } ``` -`sloggen` generates such code for you based on a simple config (a single source of truth), -which makes it easy to share domain-specific helpers between related (micro)services. +`sloggen` generates such helpers for you, so you don't have to write them manually. + +--- + +The default `log/slog` levels cover most use cases, but at some point you may want to introduce custom levels that better suit your app. +At first glance, this is as simple as defining a constant: + +```go +const LevelAlert = slog.Level(12) +``` + +However, custom levels are treated differently than the first-class citizens `Debug`/`Info`/`Warn`/`Error`: + +```go +slog.Log(nil, LevelAlert, "msg") // want "ALERT msg"; got "ERROR+4 msg" +``` + +`sloggen` solves this inconvenience by generating not only the levels themselves, but also the necessary helpers. + +Unfortunately, the only way to use such levels is the `Log` method, which is quite verbose. +`sloggen` can generate a custom `Logger` type so that custom levels can be used just like the builtin ones: + +```go +// before: +logger.Log(nil, LevelAlert, "msg", "key", "value") +// after: +logger.Alert("msg", "key", "value") +``` + +Additionally, there are options to choose the API style of the arguments (`...any` or `...slog.Attr`) and to add/remove `context.Context` as the first parameter. +This allows you to adjust the logging API to your own code style without sacrificing convenience. + +> 💡 Various API rules for `log/slog` can be enforced by the [`sloglint`][1] linter. Give it a try too! + +## 🚀 Features + +* Generate key constants and `slog.Attr` constructors +* Generate custom levels with helpers for parsing/printing +* Generate a custom `Logger` type with methods for custom levels +* Codegen-based, so no runtime dependency introduced ## 📦 Install -Create and fill in the `.slog.yml` config based on the example, -then add the following directive to any `.go` file and run `go generate ./...`. +Add the following directive to any `.go` file and run `go generate ./...`. + +```go +//go:generate go run go-simpler.org/sloggen@<version> [flags] +``` + +Where `<version>` is the version of `sloggen` itself (use `latest` for automatic updates) and `[flags]` is the list of [available options](#help). + +## 📋 Usage + +There are two ways to provide options to `sloggen`: CLI flags and a `.yml` config file. +The former works best for few options and requires only a single `//go:generate` directive. +For many options it may be more convenient to use a config file, since `go generate` does not support multiline commands. +The config file can also be reused between several (micro)services if they share the same domain. + +To get started, see the [`example_test.go`](example_test.go) file and the [`example`](example) directory. + +### Key constants + +The `-c` flag (or the `consts` field) is used to generate a key constant. +For example, `-c=used_id` results in: + +```go +const UserId = "user_id" +``` + +### Attribute constructors + +The `-a` flag (or the `attrs` field) is used to generate a custom `slog.Attr` constructor. +For example, `-a=used_id:int` results in: ```go -//go:generate go run go-simpler.org/sloggen@latest --config=.slog.yml +func UserId(value int) slog.Attr { return slog.Int("user_id", value) } ``` -To get started, see the `.slog.example.yml` file and the `example` directory. +### Custom levels + +The `-l` flag (or the `levels` field) is used to generate a custom `slog.Level`. +For example, `-l=alert:12` results in: + +```go +const LevelAlert = slog.Level(12) + +func ParseLevel(s string) (slog.Level, error) {...} +func RenameLevels(_ []string, attr slog.Attr) slog.Attr {...} +``` + +The `ParseLevel` function should be used to parse the level from a string (e.g. from an environment variable): + +```go +level, err := slogx.ParseLevel("ALERT") +``` + +The `RenameLevels` function should be used as `slog.HandlerOptions.ReplaceAttr` to print custom level names correctly: + +```go +logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: level, + ReplaceAttr: slogx.RenameLevels, +})) +``` + +### Custom Logger + +The `-logger` flag (or the `logger` field) is used to generate a custom `Logger` type with methods for custom levels. + +The `-api` flag (or the `logger.api` field) is used to choose the API style of the arguments: `any` for `...any` (key-value pairs) and `attr` for `...slog.Attr`. + +The `-ctx` flag (or the `logger.ctx` field) is used to add or remove `context.Context` as the first parameter. + +For example, `-l=alert:12 -logger -api=attr -ctx` results in: + +```go +type Logger struct{ handler slog.Handler } + +func New(h slog.Handler) *Logger { return &Logger{handler: h} } + +func (l *Logger) Alert(ctx context.Context, msg string, attrs ...slog.Attr) {...} +``` + +The generated `Logger` has all the utility methods of the original `slog.Logger`, including `Enabled()`, `With()` and `WithGroup()`. + +Since `Logger` is just a frontend, you can always fall back to `slog.Logger` (e.g. to pass it to a library) using the `Handler()` method: + +```go +slog.New(logger.Handler()) +``` + +### Help + +```shell +Usage: sloggen [flags] + +Flags: + -config <path> read config from the file instead of flags + -dir <path> change the working directory before generating files + -pkg <name> the name for the generated package (default: slogx) + -i <import> add import + -l <name:severity> add level + -c <key> add constant + -a <key:type> add attribute + -logger generate a custom Logger type + -api <any|attr> the API style for the Logger's methods (default: any) + -ctx add context.Context to the Logger's methods + -h, -help print this message and quit +``` + +For the description of the config file fields, see [`.slog.example.yml`](.slog.example.yml). + +[1]: https://github.com/go-simpler/sloglint diff --git a/example/example.go b/example/example.go index 5ddfe25..cab9dfd 100644 --- a/example/example.go +++ b/example/example.go @@ -10,7 +10,7 @@ import "strings" import "time" const LevelInfo = slog.Level(0) -const LevelAlert = slog.Level(1) +const LevelAlert = slog.Level(12) const RequestId = "request_id" diff --git a/example_test.go b/example_test.go index 6ab4f13..fdc065a 100644 --- a/example_test.go +++ b/example_test.go @@ -3,7 +3,7 @@ package main // NOTE: replace "go run main.go sloggen.go" with "go run go-simpler.org/sloggen@<version>" in your project. // using flags: -//go:generate go run main.go sloggen.go -pkg=example -i=time -l=info:0 -l=alert:1 -c=request_id -a=user_id:int -a=created_at:time.Time -a=err:error -logger -api=attr -ctx +//go:generate go run main.go sloggen.go -pkg=example -i=time -l=info:0 -l=alert:12 -c=request_id -a=user_id:int -a=created_at:time.Time -a=err:error -logger -api=attr -ctx // using config (see .slog.example.yml): //go:generate go run main.go sloggen.go -config=.slog.example.yml