Skip to content

Commit

Permalink
docs: add documentation (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
tmzane authored Oct 21, 2023
1 parent 2b21d4a commit 7886151
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 15 deletions.
2 changes: 1 addition & 1 deletion .slog.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ imports:
# default: []
levels:
- info: 0
- alert: 1
- alert: 12

# the list of keys to generate constants for.
# default: []
Expand Down
164 changes: 152 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,179 @@
[![goreportcard](https://goreportcard.com/badge/go-simpler.org/sloggen)](https://goreportcard.com/report/go-simpler.org/sloggen)
[![codecov](https://codecov.io/gh/go-simpler/sloggen/branch/main/graph/badge.svg)](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
2 changes: 1 addition & 1 deletion example/example.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 7886151

Please sign in to comment.