slogformatter

package module
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 25, 2026 License: MIT Imports: 12 Imported by: 33

README

slog: Attribute formatting

tag Go Version GoDoc Build Status Go report Coverage Contributors License

Common formatters for slog library + helpers for building your own.

Handlers:

Common formatters:

Custom formatter:

  • Format: pass any attribute into a formatter
  • FormatByKind: pass attributes matching slog.Kind into a formatter
  • FormatByType: pass attributes matching generic type into a formatter
  • FormatByKey: pass attributes matching key into a formatter
  • FormatByFieldType: pass attributes matching both key and generic type into a formatter
  • FormatByGroup: pass attributes under a group into a formatter
  • FormatByGroupKey: pass attributes under a group and matching key, into a formatter
  • FormatByGroupKeyType: pass attributes under a group, matching key and matching a generic type, into a formatter

See also:

HTTP middlewares:

Loggers:

Log sinks:

🚀 Install

go get github.com/samber/slog-formatter

Compatibility: go >= 1.21

No breaking changes will be made to exported APIs before v2.0.0.

⚠️ Warnings:

  • in some case, you should consider implementing slog.LogValuer instead of using this library.
  • use this library carefully, log processing can be very costly (!)

🚀 Getting started

The following example has 3 formatters that anonymize data, format errors and format user. 👇

import (
	slogformatter "github.com/samber/slog-formatter"
	"log/slog"
)

formatter1 := slogformatter.FormatByKey("very_private_data", func(v slog.Value) slog.Value {
    return slog.StringValue("***********")
})
formatter2 := slogformatter.ErrorFormatter("error")
formatter3 := slogformatter.FormatByType(func(u User) slog.Value {
	return slog.StringValue(fmt.Sprintf("%s %s", u.firstname, u.lastname))
})

logger := slog.New(
    slogformatter.NewFormatterHandler(formatter1, formatter2, formatter3)(
        slog.NewTextHandler(os.Stdout, nil),
    ),
)

err := fmt.Errorf("an error")
logger.Error("a message",
    slog.Any("very_private_data", "abcd"),
    slog.Any("user", user),
    slog.Any("err", err))

// outputs:
// time=2023-04-10T14:00:0.000000+00:00 level=ERROR msg="a message" error.message="an error" error.type="*errors.errorString" user="John doe" very_private_data="********"

💡 Spec

GoDoc: https://pkg.go.dev/github.com/samber/slog-formatter

NewFormatterHandler

Returns a slog.Handler that applies formatters to.

import (
	slogformatter "github.com/samber/slog-formatter"
	"log/slog"
)

type User struct {
	email     string
	firstname string
	lastname  string
}

formatter1 := slogformatter.FormatByKey("very_private_data", func(v slog.Value) slog.Value {
    return slog.StringValue("***********")
})
formatter2 := slogformatter.ErrorFormatter("error")
formatter3 := slogformatter.FormatByType(func(u User) slog.Value {
	return slog.StringValue(fmt.Sprintf("%s %s", u.firstname, u.lastname))
})

logger := slog.New(
    slogformatter.NewFormatterHandler(formatter1, formatter2, formatter3)(
        slog.NewTextHandler(os.StdErr, nil),
    ),
)

err := fmt.Errorf("an error")
logger.Error("a message",
    slog.Any("very_private_data", "abcd"),
    slog.Any("user", user),
    slog.Any("err", err))

// outputs:
// time=2023-04-10T14:00:0.000000+00:00 level=ERROR msg="a message" error.message="an error" error.type="*errors.errorString" user="John doe" very_private_data="********"
NewFormatterMiddleware

Returns a slog-multi middleware that applies formatters to.

import (
	slogformatter "github.com/samber/slog-formatter"
	slogmulti "github.com/samber/slog-multi"
	"log/slog"
)

formatter1 := slogformatter.FormatByKey("very_private_data", func(v slog.Value) slog.Value {
    return slog.StringValue("***********")
})
formatter2 := slogformatter.ErrorFormatter("error")
formatter3 := slogformatter.FormatByType(func(u User) slog.Value {
	return slog.StringValue(fmt.Sprintf("%s %s", u.firstname, u.lastname))
})

formattingMiddleware := slogformatter.NewFormatterHandler(formatter1, formatter2, formatter3)
sink := slog.NewJSONHandler(os.Stderr, slog.HandlerOptions{})

logger := slog.New(
    slogmulti.
        Pipe(formattingMiddleware).
        Handler(sink),
)

err := fmt.Errorf("an error")
logger.Error("a message",
    slog.Any("very_private_data", "abcd"),
    slog.Any("user", user),
    slog.Any("err", err))

// outputs:
// time=2023-04-10T14:00:0.000000+00:00 level=ERROR msg="a message" error.message="an error" error.type="*errors.errorString" user="John doe" very_private_data="********"
RecoverHandlerError

Returns a slog.Handler that recovers from panics or error of the chain of handlers.

import (
	slogformatter "github.com/samber/slog-formatter"
	slogmulti "github.com/samber/slog-multi"
	"log/slog"
)

recovery := slogformatter.RecoverHandlerError(
    func(ctx context.Context, record slog.Record, err error) {
        // will be called only if subsequent handlers fail or return an error
        log.Println(err.Error())
    },
)
sink := NewSinkHandler(...)

logger := slog.New(
    slogmulti.
        Pipe(recovery).
        Handler(sink),
)

err := fmt.Errorf("an error")
logger.Error("a message",
    slog.Any("very_private_data", "abcd"),
    slog.Any("user", user),
    slog.Any("err", err))

// outputs:
// time=2023-04-10T14:00:0.000000+00:00 level=ERROR msg="a message" error.message="an error" error.type="*errors.errorString" user="John doe" very_private_data="********"
TimeFormatter

Transforms a time.Time into a readable string.

slogformatter.NewFormatterHandler(
    slogformatter.TimeFormatter(time.DateTime, time.UTC),
)
UnixTimestampFormatter

Transforms a time.Time into a unix timestamp.

slogformatter.NewFormatterHandler(
    slogformatter.UnixTimestampFormatter(time.Millisecond),
)
TimezoneConverter

Set a time.Time to a different timezone.

slogformatter.NewFormatterHandler(
    slogformatter.TimezoneConverter(time.UTC),
)
ErrorFormatter

Transforms a Go error into a readable error.

import (
	slogformatter "github.com/samber/slog-formatter"
	"log/slog"
)

logger := slog.New(
    slogformatter.NewFormatterHandler(
        slogformatter.ErrorFormatter("error"),
    )(
        slog.NewTextHandler(os.Stdout, nil),
    ),
)

err := fmt.Errorf("an error")
logger.Error("a message", slog.Any("error", err))

// outputs:
// {
//   "time":"2023-04-10T14:00:0.000000+00:00",
//   "level": "ERROR",
//   "msg": "a message",
//   "error": {
//     "message": "an error",
//     "type": "*errors.errorString"
//     "stacktrace": "main.main()\n\t/Users/samber/src/github.com/samber/slog-formatter/example/example.go:108 +0x1c\n"
//   }
// }
HTTPRequestFormatter and HTTPResponseFormatter

Transforms *http.Request and *http.Response into readable objects.

import (
	slogformatter "github.com/samber/slog-formatter"
	"log/slog"
)

logger := slog.New(
    slogformatter.NewFormatterHandler(
        slogformatter.HTTPRequestFormatter(false),
        slogformatter.HTTPResponseFormatter(false),
    )(
        slog.NewJSONHandler(os.Stdout, nil),
    ),
)

req, _ := http.NewRequest(http.MethodGet, "https://api.screeb.app", nil)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-TOKEN", "1234567890")

res, _ := http.DefaultClient.Do(req)

logger.Error("a message",
    slog.Any("request", req),
    slog.Any("response", res))
PIIFormatter

Hides private Personal Identifiable Information (PII).

IDs are kept as is. Values longer than 5 characters have a plain text prefix.

import (
	slogformatter "github.com/samber/slog-formatter"
	"log/slog"
)

logger := slog.New(
    slogformatter.NewFormatterHandler(
        slogformatter.PIIFormatter("user"),
    )(
        slog.NewTextHandler(os.Stdout, nil),
    ),
)

logger.
    With(
        slog.Group(
            "user",
            slog.String("id", "bd57ffbd-8858-4cc4-a93b-426cef16de61"),
            slog.String("email", "foobar@example.com"),
            slog.Group(
                "address",
                slog.String("street", "1st street"),
                slog.String("city", "New York"),
                slog.String("country", "USA"),
                slog.Int("zip", 12345),
            ),
        ),
    ).
    Error("an error")

// outputs:
// {
//   "time":"2023-04-10T14:00:0.000000+00:00",
//   "level": "ERROR",
//   "msg": "an error",
//   "user": {
//     "id": "bd57ffbd-8858-4cc4-a93b-426cef16de61",
//     "email": "foob*******",
//     "address": {
//       "street": "1st *******",
//       "city": "New *******",
//       "country": "*******",
//       "zip": "*******"
//     }
//   }
// }
IPAddressFormatter

Transforms an IP address into "********".

import (
	slogformatter "github.com/samber/slog-formatter"
	"log/slog"
)

logger := slog.New(
    slogformatter.NewFormatterHandler(
        slogformatter.IPAddressFormatter("ip_address"),
    )(
        slog.NewTextHandler(os.Stdout, nil),
    ),
)

logger.
    With("ip_address", "1.2.3.4").
    Error("an error")

// outputs:
// {
//   "time":"2023-04-10T14:00:0.000000+00:00",
//   "level": "ERROR",
//   "msg": "an error",
//   "ip_address": "*******",
// }
FlattenFormatterMiddleware

A formatter middleware that flatten attributes recursively.

import (
	slogformatter "github.com/samber/slog-formatter"
	slogmulti "github.com/samber/slog-multi"
	"log/slog"
)

logger := slog.New(
    slogmulti.
        Pipe(slogformatter.FlattenFormatterMiddlewareOptions{Separator: ".", Prefix: "attrs", IgnorePath: false}.NewFlattenFormatterMiddlewareOptions()).
        Handler(slog.NewJSONHandler(os.Stdout, nil)),
)

logger.
    With("email", "samuel@acme.org").
    With("environment", "dev").
    WithGroup("group1").
    With("hello", "world").
    WithGroup("group2").
    With("hello", "world").
    Error("A message", "foo", "bar")

// outputs:
// {
//   "time": "2023-05-20T22:14:55.857065+02:00",
//   "level": "ERROR",
//   "msg": "A message",
//   "attrs.email": "samuel@acme.org",
//   "attrs.environment": "dev",
//   "attrs.group1.hello": "world",
//   "attrs.group1.group2.hello": "world",
//   "foo": "bar"
// }
Format

Pass every attributes into a formatter.

slogformatter.NewFormatterHandler(
    slogformatter.Format(func(groups []string, key string, value slog.Value) slog.Value {
        // hide everything under "user" group
        if lo.Contains(groups, "user") {
            return slog.StringValue("****")
        }

        return value
    }),
)
FormatByKind

Pass attributes matching slog.Kind into a formatter.

slogformatter.NewFormatterHandler(
    slogformatter.FormatByKind(slog.KindDuration, func(value slog.Value) slog.Value {
        return ...
    }),
)
FormatByType

Pass attributes matching generic type into a formatter.

slogformatter.NewFormatterHandler(
    // format a custom error type
    slogformatter.FormatByType[*customError](func(err *customError) slog.Value {
        return slog.GroupValue(
            slog.Int("code", err.code),
            slog.String("message", err.msg),
        )
    }),
    // format other errors
    slogformatter.FormatByType[error](func(err error) slog.Value {
        return slog.GroupValue(
            slog.Int("code", err.Error()),
            slog.String("type", reflect.TypeOf(err).String()),
        )
    }),
)

⚠️ Consider implementing slog.LogValuer when possible:

type customError struct {
    ...
}

func (customError) Error() string {
    ...
}

// implements slog.LogValuer
func (customError) LogValue() slog.Value {
	return slog.StringValue(...)
}
FormatByKey

Pass attributes matching key into a formatter.

slogformatter.NewFormatterHandler(
    slogformatter.FormatByKey("abcd", func(value slog.Value) slog.Value {
        return ...
    }),
)
FormatByFieldType

Pass attributes matching both key and generic type into a formatter.

slogformatter.NewFormatterHandler(
    slogformatter.FormatByFieldType[User]("user", func(u User) slog.Value {
        return ...
    }),
)
FormatByGroup

Pass attributes under a group into a formatter.

slogformatter.NewFormatterHandler(
    slogformatter.FormatByGroup([]{"user", "address"}, func(attr []slog.Attr) slog.Value {
        return ...
    }),
)
FormatByGroupKey

Pass attributes under a group and matching key, into a formatter.

slogformatter.NewFormatterHandler(
    slogformatter.FormatByGroupKey([]{"user", "address"}, "country", func(value slog.Value) slog.Value {
        return ...
    }),
)
FormatByGroupKeyType

Pass attributes under a group, matching key and matching a generic type, into a formatter.

slogformatter.NewFormatterHandler(
    slogformatter.FormatByGroupKeyType[string]([]{"user", "address"}, "country", func(value string) slog.Value {
        return ...
    }),
)

🤝 Contributing

Don't hesitate ;)

# Install some dev dependencies
make tools

# Run tests
make test
# or
make watch-test

👤 Contributors

Contributors

💫 Show your support

Give a ⭐️ if this project helped you!

GitHub Sponsors

📝 License

Copyright © 2023 Samuel Berthe.

This project is MIT licensed.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func FlattenAttrs added in v0.5.0

func FlattenAttrs(attrs []slog.Attr) []slog.Attr

FlattenAttrs flatten attributes recursively.

func FlattenAttrsWithPrefix added in v0.5.0

func FlattenAttrsWithPrefix(separator string, prefix string, attrs []slog.Attr) []slog.Attr

FlattenAttrsWithPrefix flatten attributes recursively, with prefix.

func NewFormatterHandler

func NewFormatterHandler(formatters ...Formatter) func(slog.Handler) slog.Handler

NewFormatterHandler returns a slog.Handler that applies formatters to.

func NewFormatterMiddleware

func NewFormatterMiddleware(formatters ...Formatter) slogmulti.Middleware

NewFormatterMiddleware returns slog-multi middleware.

func PrefixAttrKeys added in v0.5.0

func PrefixAttrKeys(prefix string, attrs []slog.Attr) []slog.Attr

PrefixAttrKeys prefix attribute keys.

func RecoverHandlerError added in v1.2.0

func RecoverHandlerError(recovery RecoveryFunc) func(slog.Handler) slog.Handler

RecoverHandlerError returns a slog.Handler that recovers from panics or error of the chain of handlers.

Types

type FlattenFormatterMiddleware added in v0.5.0

type FlattenFormatterMiddleware struct {
	// contains filtered or unexported fields
}

func (*FlattenFormatterMiddleware) Enabled added in v0.5.0

func (h *FlattenFormatterMiddleware) Enabled(ctx context.Context, level slog.Level) bool

Implements slog.Handler

func (*FlattenFormatterMiddleware) Handle added in v0.5.0

func (h *FlattenFormatterMiddleware) Handle(ctx context.Context, record slog.Record) error

Implements slog.Handler

func (*FlattenFormatterMiddleware) WithAttrs added in v0.5.0

func (h *FlattenFormatterMiddleware) WithAttrs(attrs []slog.Attr) slog.Handler

Implements slog.Handler

func (*FlattenFormatterMiddleware) WithGroup added in v0.5.0

func (h *FlattenFormatterMiddleware) WithGroup(name string) slog.Handler

Implements slog.Handler

type FlattenFormatterMiddlewareOptions added in v0.5.0

type FlattenFormatterMiddlewareOptions struct {
	// Ignore attribute path and therefore ignore attribute key prefix.
	// Some attribute keys may collide.
	IgnorePath bool
	// Separator between prefix and key.
	Separator string
	// Attribute key prefix.
	Prefix string
}

func (FlattenFormatterMiddlewareOptions) NewFlattenFormatterMiddlewareOptions added in v0.5.0

func (o FlattenFormatterMiddlewareOptions) NewFlattenFormatterMiddlewareOptions() slogmulti.Middleware

NewFlattenFormatterMiddlewareOptions returns a formatter middleware that flatten attributes recursively.

type Formatter

type Formatter func(groups []string, attr slog.Attr) (slog.Value, bool)

func ErrorFormatter

func ErrorFormatter(fieldName string) Formatter

ErrorFormatter transforms a go error into a readable error.

Example:

err := reader.Close()
err = fmt.Errorf("could not close reader: %v", err)
logger.With("error", reader.Close()).Log("error")

passed to ErrorFormatter("error"), will be transformed into:

"error": {
  "message": "could not close reader: file already closed",
  "type": "*io.ErrClosedPipe"
}

func Format

func Format(formatter func([]string, string, slog.Value) slog.Value) Formatter

Format returns a Formatter that applies formatter to each attribute, recursively traversing nested groups. The groups slice contains the keys of enclosing groups, from outermost to innermost.

func FormatByFieldType

func FormatByFieldType[T any](key string, formatter func(T) slog.Value) Formatter

FormatByFieldType pass attributes matching both key and generic type into a formatter.

func FormatByGroup

func FormatByGroup(groups []string, formatter func([]slog.Attr) slog.Value) Formatter

FormatByGroup pass attributes under a group into a formatter.

func FormatByGroupKey

func FormatByGroupKey(groups []string, key string, formatter func(slog.Value) slog.Value) Formatter

FormatByGroupKey pass attributes under a group and matching key, into a formatter.

func FormatByGroupKeyType

func FormatByGroupKeyType[T any](groups []string, key string, formatter func(T) slog.Value) Formatter

FormatByGroupKeyType pass attributes under a group, matching key and matching a generic type, into a formatter.

func FormatByKey

func FormatByKey(key string, formatter func(slog.Value) slog.Value) Formatter

FormatByKey pass attributes matching key into a formatter. This function performs recursive lookup through nested groups to find matching keys.

func FormatByKind added in v0.3.0

func FormatByKind(kind slog.Kind, formatter func(slog.Value) slog.Value) Formatter

FormatByKind pass attributes matching `slog.Kind` into a formatter. This function performs recursive lookup through nested groups to find matching kinds.

func FormatByType

func FormatByType[T any](formatter func(T) slog.Value) Formatter

FormatByType pass attributes matching generic type into a formatter. This function performs recursive lookup through nested groups to find matching types.

func HTTPRequestFormatter added in v0.2.0

func HTTPRequestFormatter(ignoreHeaders bool) Formatter

HTTPRequestFormatter transforms a *http.Request into a readable object.

func HTTPResponseFormatter added in v0.2.0

func HTTPResponseFormatter(ignoreHeaders bool) Formatter

HTTPResponseFormatter transforms a *http.Response into a readable object.

func IPAddressFormatter

func IPAddressFormatter(key string) Formatter

IPAddressFormatter transforms an IP address into "********".

Example:

"context": {
  "ip_address": "bd57ffbd-8858-4cc4-a93b-426cef16de61"
}

passed to IPAddressFormatter("ip_address"), will be transformed into:

"context": {
  "ip_address": "********",
}

func PIIFormatter

func PIIFormatter(key string) Formatter

PIIFormatter transforms any value under provided key into "********". IDs are kept as is.

Example:

  "user": {
    "id": "bd57ffbd-8858-4cc4-a93b-426cef16de61",
    "email": "foobar@example.com",
    "address": {
      "street": "1st street",
	     "city": "New York",
      "country": USA",
	     "zip": 123456
    }
  }

passed to PIIFormatter("user"), will be transformed into:

  "user": {
    "id": "bd57ffbd-8858-4cc4-a93b-426cef16de61",
    "email": "foob********",
    "address": {
      "street": "1st *******",
	     "city": "New *******",
  	   "country": "*******",
	     "zip": "*******"
    }
  }

func TimeFormatter added in v0.3.0

func TimeFormatter(timeFormat string, location *time.Location) Formatter

TimeFormatter transforms a `time.Time` into a readable string.

func TimezoneConverter added in v0.3.0

func TimezoneConverter(location *time.Location) Formatter

TimezoneConverter set a `time.Time` to a different timezone.

func UnixTimestampFormatter added in v0.3.0

func UnixTimestampFormatter(precision time.Duration) Formatter

UnixTimestampFormatter transforms a `time.Time` into a unix timestamp.

type FormatterHandler

type FormatterHandler struct {
	// contains filtered or unexported fields
}

func (*FormatterHandler) Enabled

func (h *FormatterHandler) Enabled(ctx context.Context, l slog.Level) bool

Enabled implements slog.Handler.

func (*FormatterHandler) Handle

func (h *FormatterHandler) Handle(ctx context.Context, r slog.Record) error

Handle implements slog.Handler.

func (*FormatterHandler) WithAttrs

func (h *FormatterHandler) WithAttrs(attrs []slog.Attr) slog.Handler

WithAttrs implements slog.Handler.

func (*FormatterHandler) WithGroup

func (h *FormatterHandler) WithGroup(name string) slog.Handler

WithGroup implements slog.Handler.

type HandlerErrorRecovery added in v1.2.0

type HandlerErrorRecovery struct {
	// contains filtered or unexported fields
}

func (*HandlerErrorRecovery) Enabled added in v1.2.0

func (h *HandlerErrorRecovery) Enabled(ctx context.Context, l slog.Level) bool

Enabled implements slog.Handler.

func (*HandlerErrorRecovery) Handle added in v1.2.0

func (h *HandlerErrorRecovery) Handle(ctx context.Context, record slog.Record) error

Handle implements slog.Handler.

func (*HandlerErrorRecovery) WithAttrs added in v1.2.0

func (h *HandlerErrorRecovery) WithAttrs(attrs []slog.Attr) slog.Handler

WithAttrs implements slog.Handler.

func (*HandlerErrorRecovery) WithGroup added in v1.2.0

func (h *HandlerErrorRecovery) WithGroup(name string) slog.Handler

WithGroup implements slog.Handler.

type LogValuerFunc

type LogValuerFunc func(any) (slog.Value, bool)

type RecoveryFunc added in v1.2.0

type RecoveryFunc func(ctx context.Context, record slog.Record, err error)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL