Skip to content

πŸͺ΅ Generate domain-specific wrappers for log/slog

License

Notifications You must be signed in to change notification settings

go-simpler/sloggen

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

62 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

sloggen

checks pkg.go.dev goreportcard codecov

Generate domain-specific wrappers for log/slog.

πŸ“Œ About

When using log/slog in a production-grade project, it is useful to write helpers to prevent typos in the keys:

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 pairs)...

const UserId = "user_id"

...or custom slog.Attr constructors (if you're a safety/performance advocate):

func UserId(value int) slog.Attr { return slog.Int("user_id", value) }

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:

const LevelAlert = slog.Level(12)

However, custom levels are treated differently than the first-class citizens Debug/Info/Warn/Error:

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:

// 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.

Tip

Various API rules for log/slog can be enforced by the sloglint 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

Add the following directive to any .go file and run go generate ./....

//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.

πŸ“‹ 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 file and the 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:

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:

func UserId(value int) slog.Attr { return slog.Int("user_id", value) }

Custom levels

The -l flag (or the levels field) is used to generate a custom slog.Level. For example, -l=alert:12 results in:

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):

level, err := slogx.ParseLevel("ALERT")

The RenameLevels function should be used as slog.HandlerOptions.ReplaceAttr to print custom level names correctly:

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:

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:

slog.New(logger.Handler())

Help

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.